I Didn't Let AI Read All 70 Projects — Filling a Portfolio with Scripts and 6 Agents
Seventy portfolio cards need titles, summaries, highlights, tags, and screenshots. Instead of AI crawling, a script collected the facts and agents handled only what needs understanding.
In the last post I described indexing the 75GB ~/Developer folder on my MacBook into 70 .project.toml files. That post ended with “this portfolio site is built from those files” — but there’s a sizable gap between an index and a portfolio. The index knows “this project exists, uses this stack, was last committed on this date.” A portfolio card needs a human-readable title, a summary, highlights, tags, and screenshots. For all 70 public projects.
This post is a record of how I closed that gap. The conclusion up front: a script collected everything a machine can determine, and AI agents handled only the parts that need contextual understanding. I also got burned once along the way. That story comes later.
Why you shouldn’t feed AI everything
The easy path is obvious: open Claude Code and say “read my projects folder and fill in the portfolio.” I considered it, did the math, and dropped it.
- 75GB. Reading all the code is impossible to begin with, and “skim what matters” means I can’t control what got skimmed.
- Token cost. Deep-reading code per project means repeating it 70 times. And when one project changes later? Read it again.
- Speed. A few minutes of code comprehension per project adds up to hours of pure waiting.
- Context bleed. This one is the scariest. Process 70 similar projects in one context and project A’s description starts absorbing project B’s features. My folder has about ten Django backends. If they blend, I might not even notice.
So I split the roles by the same principle as last time. The difference: this time I pushed the boundary of “what a machine can determine” much more aggressively.
Stage 1: a script collects the facts
I wrote a script called discover.mjs. Its job is simple: for each public project, get a running screen, take screenshots, and extract the page text.
discover.mjs ──▶ site-snapshots/*.json ──▶ 6 agents (parallel)
(Playwright) (page text & meta) │
│ ▼
└──▶ shots/*.png ──▶ .project.toml [portfolio] ──▶ portfolio.json ──▶ Astro build
(screenshots) (written by a validator)
The execution URL is tried in order. A manually confirmed live_url wins; otherwise a guessed deploy URL (but only after an HTTP GET confirms it’s alive — you don’t want to screenshot a parked domain); otherwise the script boots the project locally. It detects Next.js, Astro, Vite, Django, and FastAPI from package.json and config files, then starts each with its framework’s default command on a port in the 8600 range.
Once a page is up, Playwright takes over. The main page is whatever URL remains after all redirects finish. Important subpages — at most four — are picked from the main page’s navigation links, then the sitemap, then filesystem routes, with login/admin/legal paths filtered by exclusion patterns. For every page it saves the title, meta description, h1, and body text (2,000 chars for the main page, 800 for subpages) as JSON. That JSON becomes the next stage’s input.
One key design decision: failure is a normal path. Backends that need a database or env vars will obviously fail to boot locally. A failed project doesn’t stop the run; it’s quarantined in a failure list with its reason. The full run came out like this:
| Result | Count | Detail |
|---|---|---|
| Success | 28 | 21 auto-booted locally, 5 via deploy URL, 2 manually configured |
| Failed — no boot plan | 31 | CLI tools, bots, libraries — projects with no screen |
| Failed — boot timeout | 10 | backends needing DB/env |
| Failed — empty page | 1 | booted but nothing rendered |
More than half failed, and that’s not a problem — it’s an accurate record. A project with no screen should have no screenshot on its card.
Stage 2: divide the work across 6 agents
Now the part that needs understanding. Producing each project’s title, summary_en, three highlights, and tags went to Claude Code subagents — but not unsupervised.
Work split. Projects were grouped by category and run across 6 agents in parallel, one pass per project. The agents know nothing about each other’s work — structurally the cheapest way to prevent context bleed.
Input limits. What an agent may read was pinned down, with priorities:
site-snapshots/<slug>.json— the actual service screen’s text. If it exists, it wins. No source describes a service more accurately than its own copy.- The top 100 lines of the README
- The route directory listing —
ls-level feature hints. No code reading. - Stack and tech metadata already in the index
Fixed output. Results are a JSON draft file, not free prose, and a separate validator script does the applying. It cross-checks that all 70 slugs are present, validates required fields, filters tags against a whitelist — and only then replaces the [portfolio] section of each .project.toml. Nothing outside that section is ever touched.
The rules that kept hallucination out
When you generate 70 projects’ worth of “facts,” the scariest failure is a plausible lie. Four rules:
Evidence priority. Actual screen text > README > route listing. A README documents intentions and can drift from reality; the copy on a deployed screen doesn’t lie.
No guessing — shrink instead. Highlights were requested as three “what/how/result” items, but with instructions to reduce the count when evidence is thin. The actual output: 201 highlights across 70 projects, an average of 2.9. That missing 0.1 is proof the rule fired — on weakly evidenced projects, agents genuinely did not pad to three.
Controlled tags. Tags aren’t free-form; they come from a 15-word vocabulary (SaaS, landing, commerce, automation, crawling, SEO, AI, chatbot, dashboard, B2B, mobile, game, tool, content, finance). Allow free tags and you get “webapp,” “web application,” and “web-app” as three different filters — garbage.
Machine validation before writing. The apply script is the final gate. Tags outside the whitelist are silently dropped; a missing required field aborts the whole apply.
It still got through
After all that, I moved on thinking “nice, done” — then noticed something odd on the site. One card’s thumbnail was a yellow Django “Page not found (404)” debug page. On inspection, 6 of the 28 successes were fake. Four API-only backends legitimately return 404 at /, so the debug page got captured; one project’s thumbnail was a login form after a redirect to /login; another was an “Authentication required” error screen.
The script had an empty-page guard — discard if there’s no title and under 200 characters of text. But a Django debug 404 has a title and plenty of body text (it helpfully lists every URL pattern). It sailed right through.
The fix is a two-line lesson. Check the HTTP status code (however plausible a 404 page looks, it’s still a 404), and if the post-redirect path is a login page, it’s not your main page. I added both guards and deleted the six fakes. Real successes: 22.
Actual results
- All 70 public projects have titles, English summaries, highlights, and tags — zero gaps
- 201 highlights (average 2.9); tags drawn only from the 15-word controlled vocabulary
- Screenshots for 22 projects after verification, up to 5 per project plus a full-scroll capture for detail pages
- The capture run is sequential (no concurrent boots) and took a bit over 30 minutes by file timestamps. I did other things meanwhile
- I didn’t measure token cost precisely. But it’s structurally capped — each agent’s input is a few thousand characters of screen text plus 100 lines of README, which isn’t even comparable to reading 75GB
- Manual interventions: one local-only app’s boot command, one domain’s
live_urlconfirmation, and cleaning up the 6 fake thumbnails. Across 70 projects, I’ll take that
What I learned
AI is not a crawler. Collection is cheaper, faster, and reproducible when a script does it. AI’s job is exactly one thing: reading collected facts and putting them into human language.
The input contract matters more than the code. What determined agent quality wasn’t prompt eloquence but the contract — what may be read, what must not be read, and what to do when evidence runs out. The highlight average being 2.9 instead of 3.0 is that contract’s report card.
Verification rules come before parallelization. Spinning up 6 parallel agents is quick work. But without a guard that catches a 404 debug page posing as a thumbnail, I’d be showing visitors a yellow error screen as a project’s cover image right now. An automation pipeline’s value isn’t throughput — it’s how many fake successes it catches.
Next is stage 3: picking a handful of flagship projects from the 70 and letting AI actually read the code this time, to write case studies. That’s another post.