CodeScanner
CodeScanner reads an existing directory and writes code_review.json: what is in the repository, in two layers. The scan layer is measured by code and never touched by the model — the inventory, the import graph and its load-bearing modules, public signatures, every manifest's declared dependencies, git state and activity, what the previous round planned, and how the tree has drifted from it. The review layer is the model's judgment — the project's architecture, stack, commands, entry points, conventions, and what is built — each as a fact with the file it came from, drawn from a bounded evidence sample the scan ranks. Every later agent plans against that file rather than against what it would guess.
It is the first agent of any round on existing code, and the first agent of every round after the first.
Reads
The repository, locally. Before any call is made, Spec4 walks the tree, measures it, and chooses the evidence the model receives from that measurement rather than from walk order, within a budget of 24,000 tokens (scan_context_tokens in the model settings overrides it). The evidence is assembled in four tiers, each with its own share of the budget:
| Tier | What's included | Limit |
|---|---|---|
| A — File tree | Every directory holding a file, with its file count and top three extensions, then the list of every non-test source file | Directories never capped; 400 source files |
| A — README | The root README, if there is one | 80 lines |
| A — Manifests | pyproject.toml, package.json, Cargo.toml, go.mod, pom.xml, Makefile, tsconfig.json, linter and type-checker configs, and the like |
3,000 characters per file, 10,000 in total |
| A — Deployment and CI signals | Dockerfile, docker-compose.yml, Procfile, fly.toml, render.yaml, Caddyfile, nginx.conf, deploy.sh, any *.service unit, .github/workflows/, Terraform directories, and the like |
Five files each, 1,500 characters per file |
| A — Project conventions | .env.example (or .env.sample, .env.template), CLAUDE.md, AGENTS.md, CONTRIBUTING.md; LICENSE and CLA.md named as present, never pasted |
1,500 characters; 80 lines each |
| A — Prior round | On a re-scan: the previous round's phases by number and title, the feature ids each declared, and the planned stack; then the changes since that round — commits, authors, coding-agent trailers, touched directories, the most-touched files, files added and removed, and the load-bearing candidates that entered or left | 50 ids; 25 files; 10 added and 10 removed |
| B — Signatures | Every load-bearing candidate and entry-point candidate as a signature block: one line per public symbol with the first line of its docstring | 6,000 tokens |
| C — Bodies | The entry-point candidates (main, app, index, server, cli, __main__), the top three candidates by fan-in, and the files most touched since the last round — whole when under 4,000 tokens, else the first 150 lines plus the file's signatures |
12,000 tokens, plus what B left unspent |
| D — Other packages | One signature block per product package no B or C file sits in, from its highest fan-in member | 3,000 tokens |
Within each tier, files reached from an entry point go first. A block that doesn't fit is skipped, so a smaller one later in the order can still use the room; an __init__.py under 20 lines never takes a slot. Test files are outside the graph and outside the samples. On Spec4's own tree this is 4 bodies, 17 signature files, and every src/ module named, at about 17,400 tokens of evidence.
Skipped: .git, node_modules, .venv, __pycache__, dist, build, target, .next, .spec4, and the other cache and vendor directories.
Before the call, the chat shows how many files were indexed and roughly how many tokens are about to be sent, and to which model. Nothing else leaves the machine. What was shown is recorded in scan.coverage, by the code that pastes it, so the review says what the model saw.
Writes
code_review.json, as an envelope with schema_version: 2 and two objects. The model emits a bare review block, validated on its own; the envelope is assembled and validated again at commit, and a review that fails validation is written to .spec4/failures/ and re-asked once.
The scan layer
Computed from the same walk the model is shown, and held until the model's block is committed. Each block is optional and closed, so a review from an earlier 2.x is still read:
| Block | What it records |
|---|---|
inventory |
File and line counts per extension, the first 2,000 paths, and every directory the walk pruned — so a consumer can tell "not there" from "not looked at". |
coverage |
What the model was shown: the README, manifests, CI, deployment, and convention files pasted, the signature files and bodies sampled, the source total they were drawn from, and the evidence size. |
git |
Head, branch, modified and untracked counts (.spec4/ excluded); the commits since the last round — after the latest IMPLEMENTED marker — with their date range, author names (never emails), per-directory touch counts, the 25 most-touched files, and whether any commit carries a coding-agent trailer; the last commit per top-level directory. Present when the project root is itself a repository. Any failure — no binary, a timeout, an empty repository — stores available: false and never fails the scan. |
module_graph |
Import edges between the project's own source files — Python through ast, JavaScript and TypeScript by relative specifier, Go through go.mod — stored as per-node fan-in and fan-out, the top ten modules by fan-in with at least two importers as load_bearing_candidates with their consumers, the external packages with what imports them, and how much of the graph the entry points reach. |
signatures |
For the candidates and the entry points: public top-level definitions and module-level variables with the first line of their docstring. 40 symbols per file, 32 KB in all, with truncated flags at both levels. |
dependencies |
What each pyproject.toml (including PEP 735 dependency groups), package.json, and go.mod declares, by manifest path. Nothing else in a manifest is read. |
prior_round |
The latest implemented round as an extract, never a pass-through: each phase's title, feature and capability declarations, and dependencies; the stack's languages, libraries, deployment targets, persistence, providers, and integrations; the vision's feature ids; the AI catalog reduced to ids and tiers. About 15 KB on BWS4, where the round's artifacts run to hundreds. |
plan_drift |
The prior plan's dependencies against the tree, in five buckets: planned_in_manifests; planned_only_imported (never declared, but imported — a smell); planned_tooling (declared by no manifest, but witnessed by a file in the tree — uv.lock, a systemd unit, a Caddyfile, a Dockerfile, a CI workflow, a tool's own config — or named by a manifest without being depended on, like requires-python or packageManager); planned_unmatched; and declared_not_planned. A presence test, never a version check. Which bucket means drift is the consumer's judgment. |
delta |
This scan against the prior one: files added, removed, and modified; dependencies added, removed, and bumped per manifest; candidates that entered or left; public symbols added, removed, or changed. And carried — how many prior entries the commit put back into the review, and against which baseline (below). |
producer |
The Spec4 version and schema that measured the scan, so a reader of delta can tell "measured by an older extractor" from "the tree changed". |
The review layer
project_type,languages,frameworks,build_system,commands,entrypoints,ui_summary, andnotesare required when the directory holds a software project.- Every fact carries a
source— the file it was read from — orinferred_from— the file it was deduced from. A fact with neither is not written. commandshas a closed set of keys:build,test,lint,typecheck,run,dev,deploy.entrypointslikewise:main,wsgi_app,cli_script,dev_server,ui_root. Anything else the project has goes innotes.other_notesas a line of text, so a downstream agent readingcommands.testalways finds the same thing.load_bearing: one entry per candidate the scan named, with its role and contract — inputs, outputs, invariants, side effects. Itsconsumersare filled from the graph at commit, replacing whatever the model emitted, so the stored list is always the measured one.implemented: what is built, each with astatusofcomplete,partial,stub, orscaffolded_only, its paths, and the ids of the prior round's features it realizes.integrations: each external service by name and kind — an LLM provider, a database, a cache, a queue, a web search, an MCP server, and the rest of StackAdvisor's vocabulary — with its client module, the names of its environment variables (never values), and its purpose.prior_round_reconciliation: one entry per phase of the previous round, with a verdict —evidence_found,evidence_partial,no_evidence_found, orunassessedfor a phase that declared no features — computed at commit fromimplementedagainst the features the phase declared. The model contributes only thegapfor a phase it judges short.unplanned_changes: three to ten entries, grouped by area, for what entered the codebase outside any plan; classifiedout_of_roundat commit when a path it names is among the most-touched or added files.divergences: where the built stack departs from the planned one —planned,found, and akindofrenamed,replaced,dropped,added, orreshaped. Never a dependency name;plan_drifthas those.- A key the model can't verify is omitted, never filled with "none" or "n/a".
notes.change_risksrecords where a change is likely to break something; StackAdvisor is told to read it before proposing a swap.notes.security_observationsandexisting_self_description(quoted from the README or a package description, never from a config file) are also there.- An empty directory, or one without code, gets
is_software_project: falseand a one-line summary.
Later agents don't receive the file as pasted JSON. Brainstormer, StackAdvisor, and Phaser each get a rendered view of exactly the blocks they read, each block under one line of guidance, with values verbatim and their source or inferred_from in brackets; a field the renderer has no special knowledge of still reaches them. The views carry measured facts beside the model's judgments: Phaser reads load_bearing, implemented, the reconciliation, the unplanned changes, and a coverage line; StackAdvisor reads integrations, divergences, the drift's planned_unmatched and declared_not_planned, and the dependency delta; Brainstormer reads implemented and the unplanned changes. Deployer reads integrations and is told to plan against the committed tree when the scan found it dirty; Agentifier's Scout reads through a view of its own. No raw review JSON reaches a downstream model, and none of them receives scan whole.
A probe, evals/code_scanner/review_reach.py, showed why the views exist: with the raw paste, fields a prompt named reached the plan and fields it didn't mostly didn't, and StackAdvisor recorded its prompt example's ESLint and Prettier in eight rounds out of eight for a codebase whose review said oxlint.
Excerpts of both layers are on the Artifacts page.
Asks
Nothing, by default. The scan and the draft are one call. After the review is drafted you can reply in the chat to correct or extend it — CodeScanner re-emits the review block — or press Modify later to reopen it. Re-scan Project, in the action row under a finished review, walks the tree again.
Web search, if you've given it a key, is available to CodeScanner for the standards and protocols it names.
A review block that fails schema validation is written to .spec4/failures/code_scanner_review_<n>.txt — the validator's errors first, then the raw reply — before the re-ask, and again if the re-ask fails too. Nothing in the chat is lost; the file is there so a failure can be read rather than remembered.
Buttons
| Button | When |
|---|---|
| Start | The directory has code and no review has been written this round. |
| Continue | This session has an unfinished conversation with CodeScanner. |
| Modify | code_review.json exists for this round. |
| Required | The previous round is implemented and the next hasn't started — only CodeScanner shows this, because every round after the first begins with a re-scan, even if the previous round was greenfield. Or the round's code_review.json was written by an earlier Spec4 and predates the current review format: a notice names the round's folder, nothing else in the round is lost, and every other agent is Not Ready until the re-scan. |
CodeScanner has no upstream artifact, so it is never Needs Update or Not Ready. A directory you answered New project to skips it and starts with Brainstormer.
Across rounds
Every round after the first begins with a re-scan, and the re-scan reads the code as it stands: what the coding agent built, what you changed by hand, and what the previous round's plan never mentioned. That is what makes the next round a plan against the code rather than against the last plan. Rounds
The review is not rewritten from scratch. The scan finds the previous round's review, measures the delta from it, and hands the model that review with the exact list of paths that changed since it. An entry whose source path isn't on the list is carried verbatim; the model re-derives only where the list names the path, adds for paths it adds, and drops for paths it removes, and it reuses the id of a capability the prior review already names. What it drops anyway from an untouched file — an environment variable, a dependency, an endpoint — is put back at commit, keyed by name and in the prior's order, and scan.delta.carried records how many were put back and against which baseline. On BWS4's first scan after its v9 round, under 2.9.2, the model dropped 10 of 22 environment variables and 21 of 46 dependencies whose source files hadn't changed; the carry restores both lists. The committed v10 review, scanned under 2.11.1 with the carry rule in the seed, records carried: {env_vars: 0, dependencies: 0, api_surface: 0} — the model kept every untouched entry itself. With no prior review, no delta, or a truncated file list, the carry is suspended and the seed says so.
The drift is measured, not recalled. The scan compares what the last round planned with what the manifests now declare and what files in the tree witness, and lists what changed as files, signatures, manifests, and load-bearing modules, so the agents read what changed as a list rather than as the model's recollection of it.
A review written under an older schema isn't read as current. The gate above fires, and the re-scan starts fresh — the fresh seed with the old review appended as context — rather than asking for a merge into a shape the current version doesn't read.