Repository navigation
The handoff-robustness release, plus everything merged since 0.26.0. A real Pi run on a local 27B model finished two and a half hours of validated work and then stopped on a YAML shape mistake in its handoff. Behind that one refusal, the handoff parser was silently dropping closures and question text. Completion now refuses what it would lose, Pi gives the phase one turn to repair a refused handoff, and the smoke workflow no longer hangs for six hours when it fails. Also shipping: the MCP server on SDK v2 serving the 2026-07-28 revision, and the first, EXPERIMENTAL, engineering-record surface (codecarto_change).
Added
-
EXPERIMENTAL:
codecarto_change, an MCP tool that records an engineering change and never executes anything.tools/listnow returns 23 tools, not 22. The new tool is added at the end of the list so the analysis tools keep their order, and its description beginsEXPERIMENTAL.It takes oneactiondiscriminator, which is one ofcreate,update,show,list,plan,start_attempt,capture_candidate,record_proof,record_review,gate,request_acceptance,ingest_observations. These actions let a host session run the whole supervised loop through tool calls alone: create → plan (with slices) → start an attempt → capture a candidate → record proofs → record a review → gate → ask for acceptance. Records are written under.codecarto/engineering/changes/<chg_…>/(change.json,brief.md,plan.md,slices/, and per attemptsnapshots/,proofs/,reviews/,requests/,approvals/). The tool's limits are deliberate:- No execution. Nothing runs: no exec or spawn, no target-code write, no network call.
executeandrunare refused with a reason.approveandacceptare refused with a reason too, andstatecannot be set directly. - Derived fields. Fields the record derives (
authority,attested_by,outcome,stability, digests,candidate_*,input_digest, …) are refused by name on the lifecycle actions. A proof recorded through a tool call is attestedcaller, reads asclaimed, and discharges nothing under theverifiedpolicy, whatever its payload says. - Snapshots.
start_attemptandcapture_candidatecapture the baseline and candidate snapshots themselves from the server'scwdand attest themadapter. A caller-suppliedsnapshot,manifest,coverage,repository, … is refused rather than ignored. Git HEAD is read from.gitwithout running git, sorepository.dirtyis alwaystrueon a git tree. A candidate can be re-captured until a proof names it. - Attempt inputs.
start_attemptderives its input digests from the storedbrief.md/plan.md. If either file changes after the attempt starts,gaterefuses withinput-stale. - Gate and acceptance.
gatere-reads the working tree, refuses a candidate the tree no longer matches (proof-stale), and reportsneeds-human-acceptancerather thanmay-accept.request_acceptanceasks a person through the client's own MCP elicitation form, and only for a host/client pair registered inVERIFIED_ACCEPTANCE_INTEGRATIONSat the exact client version. That registry ships empty, so today every client getsneeds-human-acceptanceand is never asked. The request is still stored. - Host observations.
ingest_observationsreads the Claude Code hook's inbox (below). No host-configuration channel reaches the MCP server yet, so every proof it ingests iscallerand discharges nothing, and the result says so. - Error codes. Refusals are JSON-RPC
-32602. Retries are safe withrequest_id, andupdateandplancompare-and-swap on therevisionthe caller last read.
The shape may change before it leaves experimental. See
docs/mcp-quickstart.md§ Experimental,docs/engineering/attempt-lifecycle.mdanddocs/engineering/acceptance-adapter.md(#434, #445, #446, #447; tracking #398, #409). - No execution. Nothing runs: no exec or spawn, no target-code write, no network call.
-
EXPERIMENTAL: the engineering record core,
core/engineering/, re-exported fromcore/index.ts. This is the record system behindcodecarto_change, and it adds new exports to the package's library API:- Contract (E01). A versioned v1 record and approval contract: types, ID grammar, canonical-JSON SHA-256 digests, and pure validators for records, bundles, operation requests, acceptance requests and approval receipts. Unknown fields are refused by own-property checks, and authority is derived from collector plus attestation, never read from a record. Acceptance is classified
verifiedorcooperativeagainst a registry and a storage boundary. The validators also enforce the acceptance-request TTL against the client's request timeout, the presentation's disclosure, and the registry shape. An optionalclass_sweepfield on review objections records the class a fix belongs to and every instance of it.docs/engineering/record-contract.mdis the normative text (#414, #415, #417, #425). - Store (E02). A file-backed store with two storage classes: change and slice records are compare-and-swapped on
revisionunder a per-change lock, and attempt records are create-only once their outcome leavesrunning. Paths are checked for containment twice. Corrupt records are reported beside healthy ones, never skipped (#426, #447). - Snapshots (E03). Snapshot identity puts
coverageinside the digest. Unreadable files becomeuncovered_relevant_input, which blocks acceptance. Secret files are excluded by name and their digests are never recorded (#420). - Planning (E04). A change brief and plan are built without a library. Planning refuses floating versions, empty or unknown scenario lists and dependency cycles, and it shows uncovered scenarios and stale findings instead of hiding them (#431).
- Proofs (E05). Proof ingestion refuses a contradictory result and refuses to overwrite a proof. Failed, blocked, claimed and unstable-snapshot proofs are kept but discharge nothing (#432).
- Gates (E06). The acceptance gate judges the work before it consults the host's capability, names a remedy for every blocker, and states on every outcome that it cannot establish semantic correctness (#433).
- Traverse (E08).
planTraverseStep, a bounded, resumable next-step planner that works from the records alone (#436, #447). - Lifting (E09, E10).
liftSlices, which reads a spec's or plan's Slices table into the record vocabulary (#438, #439).
This core does not run checks, so it cannot establish that a recorded result is true. It records who asserted what.
- Contract (E01). A versioned v1 record and approval contract: types, ID grammar, canonical-JSON SHA-256 digests, and pure validators for records, bundles, operation requests, acceptance requests and approval receipts. Unknown fields are refused by own-property checks, and authority is derived from collector plus attestation, never read from a record. Acceptance is classified
-
EXPERIMENTAL: the
traverseskill and three new templates, delivered to workspaces by init and scaffold refresh..codecarto/skills/traverse/SKILL.mddescribes the supervised change loop as procedure for an agent. It is prose, and it says so: enforcement lives incore/engineering/.templates/traverse-record.md,templates/change-brief.mdandtemplates/change-plan.mdare new.codecarto_list_skillsnow liststraverse. On MCP,codecarto_skill {name: "traverse"}is served without a completed pipeline. The exemption matches that one exact name, the waybroadsideis matched, and every other skill stays pipeline-gated. For a skill that is not post-pipeline, the prompt no longer says "The pipeline iscomplete" and drops "Do not modify source files outside .codecarto/". On Pi,/codecarto-skill traverseis still pipeline-gated and gets the post-pipeline prompt (#436). -
EXPERIMENTAL: a Claude Code hook that records host-observed Bash results, shipped in the repository but not in the npm package.
hosts/claude-code/observe.mjsis aPostToolUse/PostToolUseFailurehook, andhosts/claude-code/settings.template.jsonwires it up. For each Bash call the hook writes one observation file into.codecarto/engineering/inbox/(identity, command, exit code and output digests, never output text). For eachEdit/Write/MultiEdit/NotebookEditcall it writes an activity entry. Every file is createdO_CREAT|O_EXCL|O_NOFOLLOWwith mode 0600. The hook executes nothing from its payload, refuses a symlinked namespace, and always exits 0.ingestHostObservationsreads only regularinbox/*.jsonfiles and matches each command byte-for-byte to one obligation. A command that matches none or several obligations is reported, not ingested. Processed files are rotated toinbox/processed/.ProofObligationgains an optionalcommand.hosts/is not inpackage.jsonfiles;docs/engineering/host-observations.mdsays to install from a checkout at a reviewed commit, and the hook needs Node ≥ 22. The adversarial checks for this hook placement (D4/E5/E6) have not been performed, sotool_result_pathmust be declaredunprotected. Every proof from the hook is thencaller(#446). -
The analysis templates carry stable scenario and slice handles, an agent-addressability assessment, and a verification route per slice. Scaffold refresh brings them, with their pipeline completion criteria, into existing workspaces.
- Scenario and slice tables (E09).
reimplementation-spec.md,reimplementation-spec-opinionated.mdandproject-plan.mdgive each scenario aScenario IDand aTier. They add aSlicestable withSlice ID, deliverable, modules,Proves scenarios,Depends onand tier. In the two spec templates the table also has aVerification routecolumn (test,run,manual-procedure, ornone), and the opinionated variant adds aProof commandcolumn. - Agent Addressability (E10).
architecture-map.mdandreverse-engineering-bundle.mdgain anAgent Addressabilitytable: headless, inspectable state, and programmatic seam, each tagged with an evidence level. A missing seam is recorded as a portability hazard, not evidence of correctness. - Validation rows and completion criteria. The matching validation rows and completion criteria are added to every pipeline that carries the phase: architecture in seven pipelines, porting and reimplementation-spec in four each, and goal-synthesis-finalize in
pipeline-synthesis.yaml. The architecture, porting, reimplementation-spec and goal-synthesis-finalize SKILLs now ask for the new tables. - What enforces them. The tables are filled and checked by the phase session's own validation table. No completion code reads them.
liftSlices(above) is a library function that no command calls yet. Existing outputs still validate, because phase validation only refuses rows marked FAIL (#438, #439).
- Scenario and slice tables (E09).
Changed
-
The MCP server runs on SDK v2 and serves the
2026-07-28revision alongside the 2025 era.@modelcontextprotocol/sdk^1.29.0 is replaced by@modelcontextprotocol/server+/core2.0.0 (thelatestline; the v1 package is no longer in the tree, and 80 transitive packages the v1 line dragged in — express, hono, ajv, cors, … — go with it). The codemod rewrote the imports andMcpError/ErrorCode→ProtocolError/ProtocolErrorCode(the four JSON-RPC codes the server emits, -32600/-32601/-32602/-32603, are unchanged) andsetRequestHandler(Schema, …)→setRequestHandler("tools/list" | "tools/call", …); the two-handler dispatch and the 22 tools it served are untouched (codecarto_change, under Added, is the 23rd). The stdio entry is now the SDK'sserveStdio, which owns the era decision per connection: a client that opens withinitializeis pinned to a 2025-era instance and served exactly as before (every revision the v1 line accepted —2025-11-25,2025-06-18,2025-03-26,2024-11-05,2024-10-07— is still accepted and echoed;@modelcontextprotocol/server-legacyis SSE + OAuth and is not needed); a client that probes withserver/discoveris pinned to a2026-07-28instance, on which the SDK itself answers the probe (supportedVersions: ["2026-07-28"], capabilities, instructions), stampsresultType: "complete"andserverInfoonto every result, and emits the caching hints —tools/listis declaredttlMs: 86400000, cacheScope: "public"because the inventory is a module constant. A 2025-era response never carries any of the 2026 vocabulary.server.json's$schemastays at2025-12-11: it is the only registry schema published (…/schemas/2026-07-28/server.schema.jsonis 404 and the 2026-07-28 spec's registry docs reference2025-12-11).tests/mcp-protocol-2026-07-28.test.mjsdrives the shipped binary over stdio in both eras, andscripts/smoke-mcp.mjsnow runs 12 checks: the original nine throughinitialize, then three through the v2 client'sversionNegotiation: { mode: "auto" }probe. One wire-visible detail changed:ProtocolError.messageno longer carries the v1MCP error <code>:prefix (the code is inerror.code; four tests that matched the prefix now match the message body and keep asserting the code). Before this release the SDK-era swap went through CONTRIBUTING's surface verification on the packed tarball:codecarto_initthencodecarto_nextfrom Claude Code, Codex and Hermes, and a full phase on Pi with a real model. The returns are recorded in the release PR (#185). -
Ten test expectations no longer assume POSIX paths. The
windows-latestjob's first run reported ten failures; two were product defects (#393, #394) and the other eight were expectations written for/-separated paths: anoutputDir's last segment taken withsplit("/"), which does not split a backslash path; four comparisons against a rawlibrary.pathorsource_repoline, where a path containing backslashes is correctly emitted as a quoted YAML scalar with those backslashes escaped; a path interpolated into aRegExpsource, where backslashes read as escapes and\bbecomes a word boundary; a refusal message matched against a/-rooted pattern; and a joined path compared against a/-joined literal. Each now compares a parsed value, abasename, apath.joinon both sides, or a literal prefix — and onedoesNotMatchin the same family, which a quoted backslash path would have satisfied vacuously rather than failing, became a parsed-value comparison too. Fixing those eight exposed two more of the same kind — a third rawlibrary.pathcomparison and a second/-rootedsource_repopattern — because a test stops at its first failing assertion, so the run could only report the first one in each: the issue's list of eight was what was visible, not the whole set. A sweep for every instance of these shapes acrosstests/(raw-line comparisons, a path interpolated into a pattern,split("/")on a path) finds no others; what remains is writes that feed the parser, URLs, which are always/-separated, and patterns that already escape their input. No product code changed.continue-on-errorcame off thetest-windowsjob once a run reported it green (#427) (#395). -
Engineering runtime state is private to the workspace: ignored by git for new workspaces, never copied by init, and kept out of the npm package.
.codecarto/engineering/holds change briefs, attempts, proofs, review objections and approval receipts. The shipped.codecarto/.gitignoreandtemplates/gitignorenow ignoreengineering/.codecarto_init//codecarto-initcopy the directory without its contents.package.jsonfilesexcludes.codecarto/engineering/**. Upgrading: scaffold refresh never rewrites an existing workspace's.codecarto/.gitignore(it only creates one when absent). A workspace created before this release will not ignoreengineering/unless the line is added by hand (#426, #441). -
CI: the Windows suite now gates the run instead of only reporting.
continue-on-erroris removed from thetest-windowsjob. A Windows failure now fails the CI run (#427, closes #395). -
Published type declarations omit
@internalmembers.tsconfig.jsonsetsstripInternal: true. The test-only seams (the lock'sfsOps,buildServer({ acceptanceRegistry }), the working-tree capture seam) are absent fromdist/**/*.d.ts, and a consumer test compiles the declarations withskipLibCheckoff (#444, #445, #447).
Fixed
-
The release workflow builds before it runs the tests.
release.ymlrannpm testbeforenpm run build, while CI builds first. The MCP protocol and acceptance-adapter tests added since 0.26.0 drive the shipped binary indist/, so the firstv0.27.0tag push failed on 51 tests before any publish step ran. Nothing was published. The workflow now builds first, andtests/release-workflow.test.mjspins build-before-test in both workflows. -
Scaffold refresh never copies engineering records. Init and the npm pack both excluded
.codecarto/engineering/. Refresh walked every directory of the template it refreshes from, except its own exclusion list, and that list did not name the namespace. In a checkout install, that template is the developer's working.codecarto/, so their change briefs, proofs and approval receipts were copied into every workspace refreshed from it. Reproduced against a copy of the template with a planted history, where the refresh list carried the approval and proof files.engineering/is now protected the wayscratch/andbroadside/are, and the Pi refresh preview names it. npm installs were never affected, because the tarball excludes the namespace. -
The
traverseskill no longer promises a next-step actioncodecarto_changedoes not have. It told the agent to "askcodecarto_changefor the next step", but no action returns the planner's answer, andplanTraverseStepis only reachable from the core library. The skill now says so, and over MCP it directs the agent toshowandgate.docs/mcp-quickstart.mdnow lists all twelvecodecarto_changeactions instead of four, and says whatrequest_acceptancedoes today. README's tool table gains thecodecarto_changerow.acceptance-adapter.mdno longer calls attempt records create-only while they arerunning. -
The smoke harness exits when a step fails, and neither smoke workflow can hang for six hours. A failed step threw out of
main()before its MCP client was closed. The open stdio client kept the server child, and with it the harness process, alive, so the scheduled Smoke workflow sat until GitHub's six-hour limit on both Node legs every day from 2026-09-22. The harness now closes every client it opened (bounded at five seconds) and exits explicitly. Reproduced against the packed tarball with one tool removed fromEXPECTED_TOOLS: the old harness was still running when killed at 60 s, and the new one exits 1 in 3 s. The scheduled run now checks out the tag of the version it tests, defaulting to the latest published version rather thanpackage.json's: the harness asserts the exact tool inventory, andmain's harness lists tools that are not published yet (it expectedcodecarto_changefrom 0.26.0). Both workflows gained step and job timeouts (#449). -
Production dependencies audit clean again. The release workflow gates on
npm audit --omit=dev --audit-level=high, andmainhad stopped passing it. Two high advisories came through the Pi peer dependency's tree:undici8.10.0 (eleven advisories, fixed in 8.10.2) andbrace-expansion5.0.9 (three DoS advisories, fixed in 5.0.12). A targetednpm updatemoves the lockfile toundici8.11.2 andbrace-expansion5.0.12, six lines each way. Theundicioverride floor rises to^8.10.2so the lockfile cannot drift back. -
A malformed handoff entry fails completion instead of being dropped from it.
parseHandofffiltered what it could not read, so a handoff could complete while part of it never reachedworkflow/status.yaml, the opposite of GUIDE.md's "malformed collection shapes fail completion". Acarry_forward_closuresentry written as{ id: arch-CF1, … }, which is the shapeopen_question_closurestakes, was dropped, and the routed item stayed open for a phase that had already closed it. Anopen_questions,carry_forwardorpost_pipelineentry with its text under another key (question:,why_source_cannot_settle:) was stored as a bare id and kind. Both came from a real run on a local 27B model.carry_forward_closuresnow takes a bare id or a mapping with one. A closure with no id is refused, and a closure's other text (closure:,reason:) is kept as an owner note instead of being dropped. A structured entry whose text sits under a key completion does not read, instead ofdescription, is refused, and the refusal names those keys. An entry made only of recognized fields is still accepted, since it loses nothing. Anowner_notesordecisionsentry with nested structure is refused too. An unquoted note containing": ", which YAML reads as a one-key mapping and which was also silently dropped, is now read back as itskey: valuetext. Amendments get the same treatment: their closures take{ id, evidence }, and the evidence is recorded in the closeout's notes. An amendment's notes now reach its closeout even whencloseout_contentis supplied, as the template's own shape does; before, they were dropped while Pi's preview promised them.status.yamlkeeps its lenient reader; only model-written input is strict (#453). -
Pi: a handoff completion would refuse gets one repair turn before the phase ends. Completion judged the handoff only after the phase's sub-agent had been disposed, so a finished phase whose handoff had a YAML shape mistake stopped the run, and the model that wrote it never saw why. The observed case was 151 minutes and 629k tokens of PASS-validated findings stranded by
owner_noteswritten as one folded string. A newcheckPhaseHandoff(cwd, phaseId)incore/runs the same load and the same state-dependent gates as completion (parse and shape,phase_id, a required handoff that is missing, carry-forward targets, D1, D3). It writes nothing and returns completion's own refusal word for word. A workspace it cannot read is not treated as a refusal, since editing the handoff could not fix it. When the primary output exists and the check refuses, the still-live sub-agent gets one more prompt carrying the refusal verbatim and an instruction to fix only the handoff. It is one turn, not a loop: a handoff still refused after it stops the run as before. Both/codecarto-nextpaths get it throughrunSinglePhase. MCP is unaffected, becausecodecarto_completealready returns the refusal to the host's live agent (#454). -
A containment root that does not exist yet is canonicalized the way its target is.
isWithinPathResolvedresolved the target's existing prefix through symlinks but the root throughrealpathalone, which throws on a.codecarto/that is not there yet and fell back to the root as spelled. Wherever an ancestor needed expanding the two operands then disagreed — the 8.3 short name the Windows runner puts in%TEMP%(C:\Users\RUNNER~1\…), macOS'/var→/private/var— and a write to.codecarto/findings/…, the one place a phase session may write, was refused while that directory did not exist yet. Both operands now resolve throughresolveExistingPrefix, and the primitive takes the base a relative operand resolves against. The Pi orchestrator hook and the phase hook call that single primitive instead of each hand-rolling the resolve-canonicalize-compare sequence one of them had drifted from, and the MCPspec_pathcontainment check inherits the fix. What containment refuses is unchanged. Confirmed by thetest-windowsjob's first run (#394). -
Windows: a rename that loses a race retries instead of failing the write.
atomicWriteFilewrites a uniquely named sibling temp file and renames it over the destination; on Windows that rename fails withEPERMwhile another writer holds or is replacing the same file, so two concurrent writers — two MCP hosts, Pi and MCP, the usage log appending as a phase completes — lost a write with an error rather than serializing. Every canonical write in the framework goes through that function (status.yaml, the usage log, Broad-Sidestate.json, the library index). The rename now retries onEPERM,EBUSYandEACCESwithin a bounded, jittered budget and still propagates any other error on the first attempt; the temp-file cleanup is unchanged. Confirmed by thetest-windowsjob's first run (#393). -
Windows: only one waiter can report breaking a dead lock ticket. On the Windows runner, two waiters both recorded
brokeStalefor one dead ticket. Reaping a stale ticket now first creates an exclusive per-ticket claim marker (<lock>.claim.<ticket>), then renames the ticket to a unique tombstone (<lock>.reaped.<hex>). Only the marker's creator can report the break.EPERM/EBUSY/EACCESon that rename are retried while the ticket still exists and read as a lost race once it is gone. Any other error still surfaces after the waiter's own ticket and heartbeat are cleaned up. Old tombstones and markers are swept. OnlyENOENTfrom a ticketstatnow counts as the owner being gone (#444, closes #435). -
Test fixtures no longer pick up the developer's git environment or write into the live
.codecarto/. Git configuration injected through the environment (GIT_CONFIG_COUNT/_KEY_n/_VALUE_n, andGIT_CONFIG_PARAMETERS) used to reach fixture git commands. So didGIT_TEMPLATE_DIR(whose hooks ran inside fixture commits), the repository-location overrides (GIT_DIR,GIT_WORK_TREE,GIT_INDEX_FILE, …) and the programs git launches (editor, pager, askpass, ssh). One side-effect helper,tests/helpers/git-environment-isolation.mjs, now neutralizes them and is imported first in every git-using test file (#421, #442; closes #412, #423). The tarball-exclusion test packs a disposable copy and asserts that the live.codecarto/engineering/changes/is unchanged (#441, closes #437). A committednode_modulessymlink that brokenpm teston a clone without an install was removed, and.gitignorenow ignoresnode_modulesas either a directory or a symlink (#429). Test-only; no product code changed.
Documentation
- The engineering-evolution plan, contract and pilot record are in
docs/engineering/. It holds the vision, implementation plan and roadmap (#410), the preregistered pilots and evaluation rubric (#413), the Claude Code 2.1.263 feasibility spike (#416, with fixtures underspikes/, which is not packaged), E01's status and the D3 decision (#419, #425), the B01 pilot report (#424), the E09/E10 dogfood notes and the E11 end-to-end report (#438, #439, #440), and the host-observation, acceptance-adapter and attempt-lifecycle pages (#445, #446, #447). The README links them under Contributing as planned, not shipped, capability. The self-audit close-out (#397) and the FreeAgent evidence names (#430) are also updated.