Releases: HuginnIndustries/CodeCartographer
Release list
v0.22.2
One defect from the 0.22.1 live verification, fixed with its own live verification (#324).
Fixed
-
Two collects on one Broad-Side run submit each post-pass and the truncation retry once. A client's request timeout fired mid-wait, the client exited, the MCP server kept polling and submitted synthesis and triage on its own, and a second collect on the same run submitted its own pair — four paid post-passes for one run, with
state.jsonwritten by whichever process persisted last (#322). Before a collect spends on a slot of the run it now claims it under the state lock: an unclaimed slot is markedsubmittedbefore any network call; a slot another collect holds is adopted — polled when it has a batch id, reported as in flight elsewhere when it does not. The retry is recorded on the run (run.retry) so the claim covers it too, and a collect that polled nothing reads the saved results back and retries under the claim if the collect that saved them never did. Writes inside collect merge slot by slot, keeping whichever side is further along, instead of replacing the run wholesale. -
A server whose client is gone stops polling and submits nothing further.
runBroadsideCollecttakes anAbortSignal; the poll loop returns at once when it fires, waking from its interval rather than sleeping it out, and accepted batches keep running for the next collect to claim. The MCP server fires it on transport close and on the end of stdin — the SDK's stdio transport watchesdataanderroronly and never notices a client exiting. Verified live: a server whose client gave up after 30 s exited within 3 s (it used to outlive the client by a quarter of an hour), and the next collect submitted exactly one synthesis and one triage. -
The collect report names a post-pass still running, held by another collect, or failed with its reason. A run reported "completed" used to say nothing about a synthesis whose batch was still running when the wait ran out. Both post-passes are now polled together against the shared deadline; polled in turn, the first could spend the whole budget and leave the second a single poll.
-
wait_secondsis documented as bounded by the host's tool-call timeout, with what happens when the host gives up first (tool description, MCP quickstart, README).
v0.22.1
What running 0.22.0 against the real OpenRouter Batch API and a real Pi session turned up, in one PR (#320). Five defects, none of which a mocked fetcher had a reason to show; the first changes what every lens request asks a model for.
Changed
-
Every lens request asks for low reasoning effort instead of a token cap. The cap (
reasoning.max_tokensat a quarter of the lens budget) is not honoured everywhere: Gemini 3.x models take a thinking level, not a budget. Measured ongoogle/gemini-3.8-flash:batch, defect lens, same slice — under a 5,800-token cap the model reasoned 5,218 tokens and truncated; the #133 retry doubledmax_tokensand the thinking doubled with it (11,518 tokens, truncated again, twice the price for no JSON);effort: lowreasoned 0 tokens, finished cleanly, and cost a twelfth as much.effortis the one control OpenRouter translates for every provider, so it is the default (BROADSIDE_DEFAULT_REASONING). Still neverenabled: false, which that endpoint refuses outright.config.yaml'sreasoning:block still overrides it. -
A truncated slice is retried at low effort as well as a doubled cap. The cutoff is usually thinking, so the retry now replaces a configured token cap with
effort: low(OpenRouter refuses a request carrying both) and lowers a higher effort; an effort already at or below low, or an explicitenabled: false, is left alone. Verified live: a slice truncated at 5,756 reasoning tokens undereffort: highwas recovered by one retry batch with 0 reasoning tokens. -
A headless
/codecarto-broadsidesubmit behaves like the MCP surface. Underpi -pthere is no dialog and the confirm stub answered "no" to every estimate, so a run well undermax_costreported "Broad-Side cancelled. Nothing was submitted." Without a UI, an estimate within the cap is approved by the cap itself, one over it is refused with a message that says so, and the per-lens breakdown is printed to stderr either way.
Fixed
-
A
reasoning:block with botheffortandmax_tokensrefuses the run. OpenRouter accepts one or the other; a request carrying both is refused per request after the batch is accepted, so every lens failed at $0 with the reason buried in each result's error — and the shippedconfig.yamlcomment showed the two keys together. The loader now throwsBroadsideConfigErrorfor such a file, and the comment shows the three forms as alternatives. -
A skipped lens names the globs that matched nothing. The security lens reads
server/**,**/auth*,**/middleware/**; a JavaScript service whose server lives atsrc/server.jsmatched none of them and the report said only "skipped (0 request(s))", which read as an empty repository. The submit report and status now carry the reason (the globs, a language with no patterns, or an incremental run with none of the lens's files changed). #319 tracks the lens globs themselves. -
A run with no batch behind it is
failed, notin-flight. When every lens was skipped or refused, the run kept its initial status andstatuslisted it above the completed runs with synthesis and triage pending forever. -
The MCP wait output reports a lens's status once per change. A submit with
wait_secondsreturned onein_progress (0/1)line per poll per lens — twenty-six of them over a four-minute wait — before the result. #321. -
The Pi spend dialog mentions incremental only when it was requested, and names the real reason a requested run did not apply, in the sentence the MCP report already used. It used to print "Incremental was requested but the tree is dirty" on every dirty tree.
v0.22.0
The remaining open issues after the self-audit, in one pass: the last Broad-Side roadmap item (#141), the retry serialization it uncovered (#206), the Codex verification gap (#218), and the repository's own workspace state (#276). A minor because codecarto_broadside and /codecarto-broadside gain parameters and the models listing changes shape.
Added
-
Broad-Side model selection on both surfaces.
core/already routed lenses through config.yaml'slens_models, but neither surface took a model, so themodelsaction helped choose one that could then only be applied by hand-editing the file.codecarto_broadsidesubmit takesmodelandlens_models(validated before anything is priced);/codecarto-broadsidetakes--model=IDand a repeatable--lens-model=LENS:ID, split on the first colon since a batch id carries one. A lens set both on the call and in the file takes the call's. Either path reaches the existing pre-flight — priced from the catalog, refused without structured-output support, clamped to the model's ceiling. #141. -
The
modelslisting is advisory, and says so. OpenRouter's catalog returns a:batchid for models whose Batch API then refuses the job (does not have a :batch endpoint), with nothing in the entry to tell them apart. A submit now records what the provider said about each model it posted to inbroadside/batch-endpoints.json— accepted, or refused for that reason; a quota or network failure says nothing about the endpoint — and the listing tags rows[no batch endpoint, refused <date>]/[batch OK <date>], untried rows carrying no tag. The MCP result carriescatalogAdvisory: trueand theendpointsmap. #141. -
Codex can be verified headlessly.
codex execrefuses MCP tool calls under its forcedapproval_policy=never; the per-server keymcp_servers.<name>.default_tools_approval_mode = "approve"lifts it without disabling the shell sandbox. Verified against this build (codecarto_initthencodecarto_statusthrough Codex 0.154.0 over stdio, workspace on disk, tool text echoed). CONTRIBUTING carries the recipe and what a failed approval looks like;docs/client-surfaces.mdreplaces the four-release "unknown" row with the run. #218.
Changed
-
The truncation retry runs as one batch per model. Each truncated slice used to be submitted as its own single-request batch and polled to terminal before the next was submitted — the serialization #136 removed from the lens pass, still present here — so a model that truncated 11 of 13 slices turned one collect into eleven sequential round trips and eleven of the sixteen concurrent-job quota. Bumped requests are now grouped by the model they must run on, submitted as one batch per model, polled together against the caller's remaining deadline, and matched back by
custom_id; per-slice bookkeeping is unchanged, the batch's cost is added once, and theonStatuslabel isretry:<model>. #206. -
Broad-Side refusals say what to do. A rejected lens in the submit report was a bare
rejected; it now carries the reason, and the two refusals a run meets in practice are explained: the missing batch endpoint above, andjob-submission-count— the per-account limit on concurrent batch jobs, which one job per lens fills after a few runs on the same key. The quota can also fill after acceptance, in which case the batch completes with every request failed and the lens readcompleted, 0 result(s)with the reason buried in<id>.error.json; collect now reportsall N request(s) failed: <reason>on that lens. #141. -
This repository's
.codecarto/workspace files are pristine. The trackedBACKLOG.md,THREAD_LOG.md, andcloseouts/carried a 2026-05-02 framework session. Init never copied them and the tarball negates them, so no user received them; but a fresh clone reported an existing workspace, the GUIDE's first-time heuristic was wrong from the first call, and the self-audit appended to that session's log. They now equal their templates,closeouts/holds a.gitkeep, and the framework's own history lives indocs/history/. No ignore-rule change: a user's backlog, log, and closeouts are their project's history and stay tracked. #276.
v0.21.0
The third and last release from the self-audit (self-audit/, issues #223–#279): the six remaining highs, every low group, and the template notes from driving the pipeline. One issue stays open — #276, whether this repository should keep tracking its own .codecarto/ workspace state — because that is a maintainer's call, not a defect. A minor because three guardrails now refuse where they used to proceed: a pipeline that cannot finish is reported as stuck rather than complete, Broad-Side ships with a spend cap, and an unreadable Broad-Side config or state file stops the run instead of being read as empty.
Added
-
codecarto_nextandcodecarto_phasetakeunattended: true— the MCP spelling of Pi's--auto. The reimplementation-spec phase's Strategic Alignment Hook then defaults to language-agnostic and recordsselection: auto-defaultinstead of asking which variant to build; an autonomous host driving the audit had nobody to ask and improvised. The prompt is byte-identical to what Pi's--autobuilds. #270. -
Broad-Side
collectcan target a run:run_idon the MCP tool,--run=<id>on the slash command (statuslists the ids). With two runs in flight, collect always read the most recent, so an older run's paid results were unreachable once a newer submit existed. #268. -
The pipeline engine has a third answer.
resolvePipelineOutcomereturns eligible, complete, or stuck — a stuck pipeline names each blocked phase and the dependency keeping it there, whether that dependency is missing from the pipeline or itself blocked (a cycle).isPipelineCompleteanddescribeStuckPipelinealongside. #228. -
Both defect templates gain a
## Runtime probessection, and the severity rubric says what a probe does: it settles the evidence level and may move the severity either way, and never promotes a finding for having been confirmed. The mechanical scan's prerequisites name the manifest, compiler config, CI workflows, and ignore files its pass 6 is about; the semantic scan may read a trusted dependency's pinned source; the porting phase reads each scan's probes first. #275, #279.
Changed
-
A pipeline that cannot finish is stuck, not complete. A DAG whose remaining phase depended on a phase the file never declared reported "Phase: complete", "Progress: 1/2", and unlocked the post-pipeline skills and amendments. Status on both surfaces now shows
Pipeline state: stuckwith the sentence;codecarto_nextand/codecarto-nextrefuse (an error, since a host looping onnextwould read a text answer as done); skills, list-skills, and amendments refuse; completion and a switch leave the cursor on the first blocked phase rather than writing the terminal routing; the auto runner ends with astuckoutcome of its own. #228. -
Broad-Side ships with a spend cap of $1.00. The MCP surface cannot ask a human before spending and had no default limit, so a host calling submit with the stock config spent whatever the estimate came to. The cap applies wherever neither
config.yamlnor the call sets one; an explicitmax_cost: 0, in the file or on the call, is no limit; over the cap submit refuses with the per-lens breakdown unless forced or approved, exactly as a configured cap did. #231. -
An unreadable Broad-Side config or state file refuses the run. A
config.yamlthat existed but failed to parse was treated like an absent one — defaults, no cap, no lens routing, no message — so a typo removed the user's own guard; it now throwsBroadsideConfigError, submit/collect/models refuse on both surfaces, andstatusanswers with a warning. Astate.jsonthat could not be parsed was read as empty and the next checkpoint wrote that empty state over it, losing the batch ids of every paid, in-flight run; it now throwsBroadsideStateErrorafter preserving the file asstate.json.corrupt-<hash>, and nothing writes over it until someone looks. #232, #233. -
wait_seconds: 0means what the documentation said. An explicit 0 — and the default, which is 0 — became undefined on the way into core and, there, the 25-minute poll budget, so every collect without a wait polled for half an hour. Both wrappers pass 0 through; collect polls each in-flight batch once and returns. #230. -
Phase prompts after the first phase mark the three framework-owned reads. GUIDE.md and the handoff template are annotated as unchanged since the last phase and skimmable;
status.yamlas rewritten by completion. Same text on both surfaces. #274. -
Template notes. The spec template's Carry-Forward comment states completion's rule (a later active phase, or
post_pipeline) instead of naming targets completion refuses (#271); VALIDATE.md and GUIDE.md name UTC as the clock for every date, so a host's**Validated by:**line stops landing a day before its closeout (#272); the protocols phase owns the storage-format catalog and contracts names formats as behavior (#277); the porting template's Defect Synthesis lets completeness win over "one screen", grouping lows by root (#278).
Fixed
-
A digit-named repository bricked its workspace. The YAML emitter wrote
"2048","true","null","1.5"bare and the reader returned a number, a boolean, or nothing; a repository whose directory is all digits gotproject_name: 2048and the next load threwproject_name?.trim is not a function. The emitter quotes any string the reader would coerce (the reader is the oracle), the readers coerce scalar text instead of assuming it, and astatus.yamlwritten before the fix loads and is rewritten quoted on its next write. A seeded round-trip property test pins the pair. #225. -
The first decision row completion appends to DECISIONS.md is separated from the paragraph above it, so renderers stop folding it into the prose. #273.
-
User-fixable workspace errors are
InvalidRequest, notInternalError, on every MCP tool: an unparseablestatus.yaml, a missingpipeline:, a pipeline file that is not there. Hosts no longer treat a config problem as a server bug to retry. #263. -
The child session's phase-id match admits every id
assertSafePhaseIdadmits (.,_, capitals), so a custom pipeline using such ids keeps its bash block, write confinement, and checkpointing. Every git subprocess carries the 30 s timeout every fetch already had. Pi's library-init expands~with the shared helper. #263, #264. -
The catalog cache stamps each model separately (schema 3, with a schema check on read that the writer never had), so fetching one model no longer renews every other cached price's 24 h TTL. The batch statuses collect never polls again are one constant instead of three literal copies. #266.
-
The library list's
source_repofilter, docs drift, and the trusted-host posture.docs/client-surfaces.mdstates what the server trusts and does not; the README no longer claims a JavaScript-free dashboard or session-file links that never rendered; MANUAL names the right default pipeline and five evidence levels; the quickstart describes what each scan covers;INDEX.mdstops naming a CLI that does not exist; thecodecarto_completeandapi_keydescriptions say what happens. #265, #267.
v0.20.0
The second release from the self-audit (self-audit/, issues #223–#279). 0.19.6 shipped the four fixes the review ranked first; this one closes every medium in its queue, #235–#261, as seventeen PRs (#287–#303). A minor rather than a patch because three things changed on purpose: library-init no longer switches the MCP publish gate on, a repository Broad-Side cannot scan is refused before it costs anything, and a config file the loader cannot use now stops the library tools instead of being quietly dropped.
Added
-
Broad-Side redacts secrets before upload (#252). Files named like credential stores —
.env*,*.pem/*.key/*.p12,id_rsa*,.npmrc/.netrc,credentials.json,secrets.yaml,*.tfvars, service-account JSON — are left out of every lens by name, and well-known secret shapes in every other file (private-key blocks, AWS/GitHub/OpenAI/OpenRouter/Anthropic/Stripe/Slack/Google keys, JWTs, quoted values assigned to password/secret/token-style keys, passwords inside URLs) become[REDACTED:<kind>]before the slice is built. The marker keeps a hardcoded credential visible to the security lens; the value stays home. The submit report says what the pass did, the run records it, andredact_secrets: falseinbroadside/config.yamlturns the content pass off. Low-false-positive patterns only: a safety net for an accidental upload, not a secret scanner.core/secrets.ts. -
Config faults are reported. A
~/.codecarto/config.yamlor.codecarto/workflow/config.yamlthat fails to parse, a section that is not a mapping, or a key of the wrong type used to vanish without a message. The loader now records each fault inconfig.problems, at the granularity of the fault, andcodecarto_config//codecarto-configlist them; a phase run warns and continues; the synthesis preflight names them ahead of its "no library.path" advice. #242. -
Status names a complete phase whose report is not on disk.
status.yamlis committed and findings are gitignored by default, so a fresh clone said "6/7 complete" about reports it did not have.codecarto_statusand/codecarto-statusnow list every such phase and its path, and README's new "What to commit" section states the policy and the per-workspace opt-out. The default stays ignore-by-default. #259. -
A pipeline switch reports what it moved. The MCP result carries
currentPhaseanddangling; both surfaces print each re-routed carry-forward with its original target (see Changed). #237.
Changed
-
codecarto_library_initand/codecarto-library-initwrite only what they were asked for:library.path, andlibrary.namespacewhen given. They no longer writepublish_confirm: true, which had switched on the MCP refuse-unless-confirmed gate for every host that had never configured it — the opt-in #162 set up, defeated by the tool that creates the library. An existingpublish_confirm,orchestratorblock, or namespace is left as it was, and a config file that cannot be parsed is never rewritten. Setpublish_confirmyourself to gate the MCP server. #244. -
The library tools refuse while the configuration has problems.
codecarto_publish,codecarto_library_list,codecarto_library_reindex, and/codecarto-publishanswer from the config — its path, namespace, and confirm gate — so a file that could not be used in full is a refusal naming the file and key, not a silent fallback to whatever the other layer says. A relativelibrary.pathis one such problem: it resolved against wherever the server or Pi was launched, so the library moved with the launch directory; it is refused, and absolute or~-prefixed paths are unchanged. #242, #243. -
Broad-Side scans one snapshot and refuses what it cannot scan. The file list came from
git ls-tree HEADwhile contents came from the working tree, so a run mixed a committed list with uncommitted contents and never saw an untracked file; both now come from the working tree (ignore rules applied, files deleted on disk left out), and the run records the snapshot source beside its HEAD and dirty flag. Language detection takes every manifest present as a candidate —package.jsoncovers TypeScript and JavaScript, decided by the counts — and picks the one with the most source files, so a Python service with a docspackage.jsonis Python. A repository whose language the lenses cannot tell, or whose detected language has no source files, is refused before pricing or the network; it used to fall through to Go's globs and pay for empty batches. #248, #250. -
A pipeline switch recomputes the cursor and re-routes carry-forwards whose target it dropped. After completing a phase and switching,
status.yamlsaid "Begin architecture phase" beside a complete architecture record; the cursor is now derived from the engine, through the same code completion uses. A carry-forward targeting a phase the new pipeline lacks moves topost_pipelinewith its source phase and a note naming the dropped target, so an amendment can close it — the rule completion already applies to such a target in a handoff. #236, #237. -
Init on the packaged template itself is an existing workspace like any other. A checkout's
.codecarto/is the template init copies from and a live workspace at once; init used to skip both the refusal and the backup for it and resetstatus.yamlin place — the self-audit hit exactly that. MCP now refuses withoutforce: trueand Pi asks; a forced re-init moves the session state out file by file into.codecarto-backup-TIMESTAMP/, since the template cannot be renamed away, and leaves the framework files in place. #245. -
Text an earlier session wrote is quoted in the next prompt as data. Routed item descriptions, re-triage questions, upstream coverage bullets, and library headlines were spliced into phase prompts as plain lines — indistinguishable from instructions, able to break their list with a newline, unbounded. Each is now one line, capped at 400 chars with a note that the full text is in its file, inside
«…», under a header that says what«…»means. GUIDE.md's session-start step says the same. #253. -
PhaseRunResultno longer carries the child session, andPhaseRunCallbacks.onSessionCreatedis gone. Nothing read either; every childAgentSessionis now disposed once its work is done (see Fixed). #256.
Fixed
-
A skill name could reach outside
skills/. Both surfaces built.codecarto/skills/<name>/SKILL.mdfrom the supplied name and served it when that file existed; every findings directory ships a SKILL.md, so../findings/architecturespliced a phase skill into the post-pipeline prompt. Names now resolve against the installed list only. #235. -
A PARTIAL row that names the entry tracking it no longer becomes a second open question. VALIDATE.md already asks the evidence cell to name the routing entry; completion ignored it and registered a
needs-maintainer-decisionquestion for every PARTIAL row, so a routed gap was closed twice. A row that names nothing still becomes a question. Moving the step after the handoff also closes a collision: gap questions and id-less handoff questions were both numbered fromoq-<phase>-1, and the handoff's replaced the gap outright. #239. -
The library tools' optional
cwdis validated before it becomes a containment root.codecarto_publishjoined a rawcwdonto.codecartoas an allowed root forspec_path, read the spec against it, and only then refused a relative cwd; a relative path resolves against the MCP server process's working directory.cwdis now checked first — absolute and existing — by publish, list, and reindex alike. #241. -
The YAML reader accepts the layouts a model actually writes, and every parse error names the line and construct. A list at the same column as its key read as
nulland, at the top level, silently dropped every line after it; a plain scalar wrapped onto more-indented lines (a longcloseout_summary), a scalar starting on the line after its key, and a sequence of sequences all failed with "Invalid YAML indentation"; a tab in the indentation produced the key"". All parse now, folding like a>block, and a failure readsYAML line N: <what was expected> — "<the line>". Content after the top-level block is an error rather than unread. #246. -
**Overall:** PASS (6/6)passes validation. The verdict had to be exactlyPASSorPASS WITH GAPS; a count, a period, or bold around it failed the phase with the bare "Validation overall result is FAIL". The verdict is now whatever the value starts with, and an unreadable, missing, or FAIL line each produce an error that quotes it. #247. -
Broad-Side's estimate covers what it sends. The entry point and manifest were read whole into the architecture prompt while that lens was estimated at a flat 6,000 chars; both reads are capped at 20,000 chars with a marker, and the estimate sizes every request from the user prompt it would actually send. #249.
-
Broad-Side names an auth failure, a dead network, and a failing gateway. A 401 or 403 from the model catalog fell into the built-in pricing fallback (silently, for the default model) or "could not resolve per-token pricing"; the poller swallowed every fetch error until its budget ran out and returned "timeout". The catalog lookup now throws
BroadsideAuthErrornaming the status and the provider's message before anything is priced or posted, other failures name their cause, a timeout without one good poll carries its last error, and the collect report prints a lens's error on its line. #251. -
MCP re-renders the dashboard on a pipeline switch. Pi did and MCP did not, so the file showed the old pipeline until the next completion. The writer moved from the Pi extension into
core/— the MCP server had been importing it across wrappers — with a re-export shim at the o...
v0.19.6
The first release after running the deep-audit pipeline on this repository itself (self-audit/, issues #223–#279). This ships the four fixes ranked first in that review.
Fixed
-
A write inside
.codecarto/could land outside it. The Pi write sandbox resolved a not-yet-existing target lexically, so a symlinked directory inside the workspace that pointed elsewhere let a new file through, andlink/..was collapsed before the disk was consulted. Containment now follows every existing path component throughrealpathand appends the unborn tail (resolveExistingPrefix), in both the extension hook and the phase child-session hook. #223. -
codecarto-initfrom a checkout copied the framework's own findings into every new workspace, andnpminstalls shipped no.gitignore. This repository's.codecarto/is the template and CodeCartographer's live workspace at once, so a finished phase in the checkout seeded every new workspace with another project's report (validation then passed on it, and the test suite failed 18/696 once one phase was complete). Init and scaffold refresh now leave behind every path a packaged pipeline declares as an output, plus scratch contents, closeouts, the dashboard, status, usage, and stray lock files. npm never packs a file named.gitignore, so the rules ship astemplates/gitignoreand init and refresh write.gitignorefrom it when the workspace has none; the rules also gain the two deep-audit reports they were missing. #224, #229, #258, #269. -
Eleven writers used
<pid>.<Date.now()>temp names, and the status lock released whoever's lock was there. Two writers hitting one file inside a millisecond collided: onerenamefailed or clobbered the other, which lost concurrent usage-log appends and library publishes outright. Every framework file now lands through oneatomicWriteFilewith a unique suffix. The lock file carries a per-acquisition token and release removes it only while that token is present, so a holder whose stale lock was broken can no longer delete the new holder's lock. Usage appends run under the lock and refuse to rewrite a log that does not parse; publishes hold a library-wide lock so two concurrent publishes of one slug land as v1 and v2. #226, #227, #238, #240. -
Completion wrote its closeout, index line, and decision rows before
status.yamlwas committed. A commit that failed left every artifact asserting a completion that never happened.updateStatusAtomicallynow takes anafterCommitstep that runs once the rename has landed; completion and amendment write their artifacts there, and a failure after the commit is reported with the note that re-running regenerates them. #234.
v0.19.5
Fixed
- In a non-interactive Pi session, a command that worked and a command that silently refused looked identical. Under
pi -p,ctx.hasUIis false andctx.ui.notifyis a silent no-op, so/codecarto-status,/codecarto-usageand/codecarto-list-skills— whose only output is a notification — exited 0 having printed nothing. So did thecodecartoModeActiverefusal that every command goes through when no/codecarto-initor/codecarto-openhas run, which is the silence that let #180 go undiagnosed for months. It also meantstatusandusagecould not be verified headlessly at all: CONTRIBUTING said to check a side effect, and those two have none. Ninety-eight notification sites across the extension now go through onenotifyCtx(extensions/codecarto/notify.ts) that uses the TUI when there is one, writes a single[codecarto] <level>: <message>line to stderr when there is not, and drops the message only when the ctx is stale. Stderr rather than stdout because--mode jsonowns stdout for its event stream and prose there would corrupt it; verified that stdout stays a clean stream of JSON events while the notices arrive on stderr alone. The interactive TUI is untouched — the parity suite runs every command with a UI and pins that. Code that runs inside the phase sub-agent (phase-compaction.ts) deliberately keeps its ownif (ctx.hasUI)guards: it seeshasUI === falseeven while the parent TUI is on screen, and a stderr write there would put text into a live terminal. #219.
v0.19.4
Fixed
- Block scalars still failed inside a sequence, so 0.19.3 did not actually finish #211. The fix landed on mapping values (
key: >-) and not on sequence items (- >-), and the second is the shape an LLM reaches for when it writes a list of prose entries — adecisions:list with one folded entry per decision. A real run hit it the same day 0.19.3 shipped: the architecture phase ran for 25 turns and 73 tool uses, wrote aPASS WITH GAPSartifact, and then auto-completion died onInvalid YAML indentation near: The architecture map treats the legacy filesystem pipeline…. The phase stayedpending, the dashboard showed 0/7 with the artifact and token usage recorded beside it, and the auto run stopped one phase in.- |-was never supported either, so this was not a regression from #211 so much as a hole it left. The block-scalar body reader is now one shared routine used by both the mapping and sequence paths rather than logic inlined in one of them, so the two cannot diverge again.
Changed
/codecarto-next's flags now explain themselves.--autoand--llm-steerare independent — one decides how many phases run, the other decides what prompt each gets — and the combination that suits most full runs,--auto --llm-steer, was not guessable from a completion list that showed four bare flag names and a description that just listed them again. Completions now carry a sentence each (theAutocompleteItem.descriptionfield was there all along, unused),--strictis offered only once--autois present rather than inviting the one combination the parser rejects,/codecarto-initnames the full-run command at the moment someone needs it, and the Pi guide addendum documents all four flags with a table of the four sensible invocations. The addendum also tells the agent to volunteer that command after an init instead of waiting to be asked, and pre-empts two things that read as failures but are not: the first phase is never steered because there is no closeout to steer from, and a stopped auto run explains itself in its summary block rather than in the phase result.
v0.19.3
Fixed
-
Handoff YAML rejected folded block scalars, blaming indentation.
closeout_summary: >-— valid YAML, and the natural choice for a wrapped prose field — failed withInvalid YAML indentation near: <first line of the text>. The parser handled only|and|-, so>-fell through to the plain-scalar path and the block body underneath it then failed the indentation check, producing an error that pointed at whitespace when the actual cause was the scalar style. That sends you re-indenting instead of changing one character. It matters because a handoff is usually written by a model and a model reaching for a wrapped prose field reaches for>-; this was found during the 0.19.2 surface verification by writing a handoff by hand and hitting it immediately. The parser now supports both block styles and all three chomping indicators —|,|-,|+,>,>-,>+— with real folding semantics: a single line break between content lines becomes a space, a run of k blank lines becomes k newlines, and more-indented lines keep their breaks so a folded block can still hold an indented snippet. Literal blocks parse exactly as before;|+is newly supported alongside them. The shippedtemplates/phase-handoff.yamlnote, which said only literal blocks were supported, now names both. -
/codecarto-guideleft the model stalled instead of informed. It read the guide, tried to obey it, found nocodecarto_*tools, concluded the MCP server was unavailable, and asked which repository and which pipeline to use rather than doing anything. Two causes, both in the Pi wrapper. The guide arrived as a bare user message — the handler queued the document throughpi.sendUserMessagewith no preamble, so ~200 lines of imperative instructions landed as if the user had typed them, with no task attached; a model given instructions and no task either starts driving immediately or stalls asking what to do, and the reported session did both in turn. And the guide is written for MCP: it says to callcodecarto_status,codecarto_next,codecarto_validateandcodecarto_complete, and that "every tool takes an absolutecwd" — but the Pi extension registers no tools at all, only slash commands the user invokes, so there was nothing to call and their absence read as a broken server. The drive loop differs too, which made following the guide wrong twice over:/codecarto-nextexecutes the phase itself as an isolated sub-agent and then auto-validates and auto-completes it, so the guide's hand-written execute → handoff → validate → complete loop does not describe a Pi session at all. The guide text is unchanged —agent-skill/stays the single source andcore/guide.tsstill serves it verbatim to every surface — and the framing lives in the wrapper, where per-surface logic belongs: a reference-not-a-task header, the document embedded whole and unmodified, then a Pi-surface addendum covering the tool-to-command mapping (and the two library tools that have no Pi equivalent), the absentcwdargument, and the auto-validate/auto-complete loop. MCP needed no change; a tool result is framed by being one. Verified live with the same model and the same guide bytes, framing the only variable: before, it committed to thecodecarto_*loop and asked for a repo path and pipeline; after, "I've read the CodeCartographer guide — ready when you are", and asked how to run a phase it answers/codecarto-nextand explains that validation and completion are automatic here.
v0.19.2
Fixed
-
Phase sub-agents died instantly on any model from an extension-registered provider. A phase failed in 38ms with
No API key found for <provider>. Use /login to log into a provider…, while the same model worked in the orchestrator session that launched it, and/logincould not fix it because the parent's credentials were never the problem. All three child sessions codecarto creates — the phase runner, the next-phase rewriter, and the dashboard narrator — load withnoExtensions: trueso a globally installed codecarto does not register its commands and tool guards a second time inside its own sub-agent. But providers a different global extension contributes throughpi.registerProvider()reach the model runtime by exactly the same path: Pi registers them from the resource loader that loaded the extension. Stripping extensions therefore stripped the provider table too, so the child inherited the parent's selected model object and then had no provider definition to resolve it against.AgentSession.prompt()checkshasConfiguredAuthbefore its first turn, that check returned false for a provider the child had never heard of, and the phase was dead before a single token was sent — which is why the failure was instant and cost nothing. Built-in providers were unaffected, so this only bit users who had wired up their own. Child sessions now carry the parent's registered provider configs onto their own runtime, keeping the sub-agent on the model the user actually picked while preserving the extension isolation the flag is there for. -
Reasoning tokens were eating the output budget, truncating lens JSON and billing for thinking that was thrown away. Broad-Side sent no
reasoningfield at all, so every model applied its own default. A reasoning-capable model then spent itsmax_tokensthinking rather than answering: one measured run used 5,758 of a 6,000-token budget on reasoning and left ~230 tokens for the JSON, which truncated mid-structure on 11 of 13 slices. Those tokens bill at the full output rate, so the run paid for roughly 6,000 output tokens per slice to receive 230 usable ones — andestimateCosthad no way to anticipate it, since it budgets output on the assumption that output means JSON. #133's truncation repair could not rescue it either: doubling the cap scales the reasoning budget too, so 24 completed retry jobs moved that run from 2 parseable slices to 3. This was not a multi-model problem. The shipped default model does the same thing less consistently — reasoning from 0 to 5,757 tokens across the same 13 slices, three of them cut off atfinish_reason: length— so runs have been quietly losing slices to it all along. Lens requests now carry an explicit reasoning budget, capped at a quarter of the lens's output allowance so three quarters remain for the answer, which is precisely the splitestimateCostalready assumed. It is a cap rather than an off switch on purpose:google/gemini-3.8-flash:batchrefuses an entire batch with "Reasoning is mandatory for this endpoint and cannot be disabled", so disabling turns a partial result into none at all — verified live, 13 of 13 requests failed that way. Capping works either way. Measured on the same model, same commit, same 13 slices: 2 of 13 slices parseable before, 13 of 13 after, withfinish_reason: stopandreasoning_tokens: 0across the board, and 37 findings where the uncapped run produced 6. A lens can set its ownreasoning, andreasoning:in.codecarto/broadside/config.yamloverrides every lens. -
The shipped
config.yamlrecommended a model that cannot be submitted. Itslens_modelsexample namedanthropic/claude-opus-4.5:batchfor the security and defect lenses, and that id has no batch endpoint — submitting it returnsModel 'anthropic/claude-opus-4.5:batch' does not have a :batch endpoint. Every Anthropic and OpenAI batch id tried so far is rejected the same way, while Google's and DeepSeek's work, and nothing in OpenRouter's catalog distinguishes them beforehand. The example now names a verified-submittable model, and bothconfig.yamland the Broad-Side skill explain that themodelsaction lists ids the Batch API will refuse and that a rejected batch costs nothing, so a candidate should be probed on one lens before being relied on. -
/codecarto-completethrew its refusals instead of showing them. Completion refuses for reasons the framework words carefully — a missing phase handoff names the file to write and every field it needs; a carry-forward withoutderives_fromand a closure lacking runtime evidence explain what is missing and why. That guidance is the entire point of the refusal. Every other failure in this handler was caught and notified, but theautoCompletePhasecall was not, so those messages escaped as a rejection and the user never saw them. It was the only unguarded call of its kind in the extension;switch-pipeline,refresh-scaffold,amendand the post-phase path all wrap theirs. The irony was sharp:/codecarto-nextcatches this same throw and advises running/codecarto-completemanually, which then threw. Found by writing the first tests that invoke these handlers at all.