minipuft/claude-prompts
v5.1.1MIT
Model Context Protocol (MCP) server for versioned prompt templates, multi-step workflow chains, and quality gates. Symbolic syntax: >>prompt --> >>chain @framework :: 'gate'
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
- An optional evaluation evidence archive preserves immutable records and blobs outside disposable runtime state, with explicit unknown usage/cost and verified content references.
5.1.1 (2026-10-06)
Added
- scripts: a bundled gate naming a category no prompt directory has fails validation (#463) (a2cb7a3)
- scripts: the prompt-operand check covers the tutorials and the tools reference (#468) (7d30784)
Changed
- Contributors: the PR scripts, the
PR Conventionsworkflow and the commitlint rules are now installed from the fleet delivery contract, and drift from it fails validation.npm run validate:delivery-contract, part ofvalidate:all, fails when any managed file differs from the installed version; after a version bump,npx --no -- delivery-contract update --repo ..fromserver/brings them back in line (CONTRIBUTING.md§Pull Request Process). ThePlan:footer stays the only plan mention a PR body makes, and it is the join key across a multi-PR initiative:git log --grep='Plan: .*<slug>'lists every PR of one plan. ADecision: ADR-NNNNtrailer must name an ADR that exists underdocs/adr/, or the body check fails.
Documentation
- the config-contract plan is reference after the 5.1.0 publish (#465) (09f3de4)
- the first-prompt tutorial shows what surrounds the rendered template (#467) (4adcbc4)
- the gate-findings ideas are re-planned on main as five rows (#470) (c08ae03)
- the tutorial-rework plan is reference after the owner's walk of the page (#466) (b3133a7)
Maintenance
5.1.0 (2026-10-06)
Added
Changed
- Gate suggestions no longer recommend a gate because of a prompt's category. The
Suggested Gateslist in acreatereply and the listanalyze_gatesreturns used to add gates from a category-to-gate table compiled into the server, which matched only three of the nine shipped categories and disagreed with the gates those categories actually activate. A gate whoseactivation.prompt_categoriesnames a prompt's category is already attached to every run of that prompt, so suggesting it asked you to add something you already had. Suggestions now come from the prompt's text alone.
Fixed
resource_managernow describes every parameter intools/list. Its 75 parameters were published with a name and a type and nothing else, so a client had to guess whatsubject,unsetorchain_step_ordermeant. Each one now carries the description its contract already held.- A workspace that turned the gate system off runs no shell verification over Streamable HTTP. With several workspaces on one HTTP server,
system_controlgates disablesent under a workspace header switched gates off for that workspace, yet an inline:: verify:"…"command and a gate'sshell_verifycriterion still ran, because the check that refuses them read the switch of the workspace the server was launched in. It now reads the switch of the workspace that sent the request, so the command is refused there and still runs in a workspace that keeps gates on. STDIO, which serves one workspace, is unchanged. - A connected STDIO client now sees the new framework in every tool description after a framework switch. Each tool's description, and some of its parameter descriptions, carry the active framework's guidance. Over STDIO,
system_controlframework switch,enableanddisableleft all three tools listing the previous framework's text until the client reconnected, and a switch throughresource_managerredrew onlyprompt_engine's parameters. The nexttools/listnow shows the current framework on all three tools, or none once the framework system is off, and clients that listen for tool list changes are told. Streamable HTTP already rebuilt the list on every request and is unchanged. system_controldeclares theexecution_historystepsoperation. The tool already answeredoperation:"steps"with one line per step of a run, but its published contract listed onlylist, so a client reading the contract never saw it.- A gate your prompt's category activates now reviews with its own retry budget. When the planner had already picked such a gate, it reached the review as if the prompt had named it, and a gate's
retry_config.max_attemptsis only read for a gate the category activated, so the review fell back to the built-in 2 attempts. Bundled gates this reaches includeresearch-quality,technical-accuracyandtest-suite, which declare 3 attempts, andworkflow-growth, which declares 1. The gate's own budget is now read, on a single prompt and on a chain step. - A gate the server chose for your prompt's category now shows you its guidance, instead of asking you to attest text it never printed. A gate scoped with
activation.prompt_categorieswas selected correctly and listed in the**Gates**:line at the bottom of a response, and then its### <gate name>section was dropped from that same response. The cause was an eight-name list of "recognised" categories compiled into the server (analysis, education, development, research, debugging, documentation, content_processing, general), which shared only three names with the nine categories actually shipped: a prompt in any other category —workflow,examples,planning,guidance,knowledge-capture,codebase-setup— was relabelledgeneralbefore the guidance was rendered. The category a prompt declares is now used as it stands, so the same category decides selection and rendering. Two consequences to expect: a gate scoped to one of those six categories starts appearing, and a prompt in one of them no longer picks up gates scoped togeneral, which it was only ever receiving because of the relabelling. A prompt id is also no longer scanned for words likedebugorreadmeto guess a category it did not declare.
- gates: refuse shell verification in a workspace that turned gates off (#452) (b87b749)
- gates: render the gates a prompt's category activates on the run that selected them (#449) (02e012c)
- mcp-tools: a framework switch refreshes every tool description over STDIO (#456) (b03ca9e)
- mcp-tools: publish a description for every resource_manager parameter (#451) (db2eafe)
- scripts: a doc comment no longer counts as a reference in the unreached-methods check (#454) (8e80741)
Documentation
- the changelog has one Unreleased section, above the latest release (#462) (9daaefc)
- the first-prompt tutorial on main is the author-first rewrite (#461) (dbd7deb)
- the guides describe the bundle the server ships (#455) (49dbc2f)
5.0.0 (2026-10-06)
Added
>>interviewis a bundled prompt: an evidence-first stop a planning session calls when a choice is the owner's to make. It probes facts and reuses settled rulings first, then asks one focused question with a recommended answer, and routes toresearch_chain,design_museortech_recommendationwhen evidence is what is missing. Independent work continues while the question is open.- A Codex development launcher,
scripts/codex-server. It refreshes a localcodex-promptsplugin from this checkout so the engine and its hooks are tested together in one Codex session, with an engine-only mode and a published mode beside the default.docs/guides/codex-development.mdhas the prerequisites and the three configuration entries it expects. docs/reference/module-catalog.mdnow says which modules hold state and where each is extended. Two generated sections join the boundary table:Runtime staterenders everystate.dbtable and view fromtable-contracts.ts(owner module, durability posture, scope column, retention, reader count, and the contract's own open finding), andExtension pointsrenders theextension:block a module'smodule.yamlmay declare, whose symbol must be exported inside that module. The boundary table gains dependency-cruiser's instability metric, so a module extended by adding to it reads differently from one changed freely. All of it is regenerated and checked byvalidate:module-catalog; none of it is hand-edited.validate:pipeline-stage-tablefails when the documented pipeline stage order stops matching the code. Thestagesarray inpipeline-builder.tsis the contract; the stage table indocs/architecture/overview.md, the numbered stage files, every "N stages" numeral in the handbooks and the builder's own comments are checked against it. It runs invalidate:alland on the docs-only CI route, where a hand edit to the table lands, and imports only Node builtins so it needs nonode_modulesthere.- Creation-authoring contract guard:
npm run validate:authoring-contractschecks the three bundled builders against canonical creation fields, transport types, registered arguments, and actual mapped output, including falsy values and nested workflow fields. It runs invalidate:alland detects omitted fields, obsolete declarations, type drift, lost mappings, and premature automatic creation. - An inline gate in
prompt.yamlcan declareenforcement_mode(blocking,advisoryorinformational), the same choice agate.yamloffers. An inline gate with no mode still counts as blocking, and an unknown value refuses the prompt at load, naming the key. - A chain step can run detached: declare
await: runand the run moves on without waiting for its worker. The step's brief renders as before; the parent spawns the worker in the background (Claude Code's handoff saysrun_in_background: true), resumes with nouser_responseto continue, and hands the worker's result back later on any resume — thenode:line in itsHANDOFF RESULTblock routes it to the detached step, never to the step the run is on. A run cannot complete while a detached step it started has not reported; a result naming an unknown or already-reported step is refused by name, andcancel: truestill ends a run whose worker never reports. Available on YAML chain steps andworkflownodes;await: node, the default, blocks exactly as before. - A gate's reviewer can now be set through
resource_manager.evaluation: {mode, model, strict}on gatecreateorupdatewrites the gate'sevaluationblock —mode: "judge"to have a separate reviewer that sees only the output and the criteria,modelas a hint for which model that reviewer runs on,strictfor failures-first review. Until now the block could only be written by hand-editinggate.yaml. The block is written whole: omit it on an update and the gate keeps its block, and send{"mode": "self"}to return the gate to self-review. A misspelled key inside it is refused by name, with the nearest correct key. - A gate that withholds the step output on a FAIL can now be created through
resource_manager.block_response_on_fail: trueon gatecreateorupdatewrites theblockResponseOnFailkey. The pipeline has always honoured that key, and no parameter carried it, so declaring a blocking gate meant hand-editinggate.yaml— which this project's own rules forbid. It behaves likeseverityandenforcement_mode: omit it on an update and the gate keeps its current value, and sendfalseto clear it on a gate that had it. cpm create prompt,cpm link-gateandcpm link-gate --removerecord a version for the prompt they wrote. A prompt edited throughcpmcould not be rolled back before — no row described what the file became — so linking a gate and then changing your mind meant editing the YAML back by hand. The recorded state is the one the loader resolves, so a template held inuser-message.mdis recorded inlined exactly asresource_managerrecords it: the two surfaces' rows now compare equal, and a server edit of acpm-written prompt no longer files a spurious "Bridge: prior live state" row (nor acpmrollback of a server-written one). Linking a gate that is already linked, or unlinking one that is not, is still refused with nothing written.cpm config historylists every recorded version of the workspace config, andcpm config rollback <version> [--preview]restores one byte for byte — comments, key order and formatting included. A rollback is itself recorded, so it can be undone. It is refused, with nothing written, when the recorded bytes are missing, when they are no longer valid configuration for the installed build, or when restoring them would leave bothconfig.jsoncandconfig.jsonin one directory.cpm createandcpm togglerecord a version for the resource they wrote: a created gate or framework is version 1, and a toggled framework is recorded as an edit, in the same shape and orderresource_managerrecords them. A write that records nothing — a prompt, a style, or a workspace the server has never run in — now says so with the reason instead of reporting a bare success.cpm rollbacknow records the files it is about to overwrite, not only a projection of them. The checkpoint it saves of your current state carries that state's actual bytes, stored in the same content-addressed store and under the sametree_hashthe MCP server produces for identical files — so the two surfaces write one checkpoint format rather than two. The restored version itself is recorded as a projection, deliberately: at the moment the row is written the restore has not run yet, so the bytes on disk are the state being replaced, and hanging them on the row that claims the restored content would report full fidelity over the wrong file.cpm's other edit commands still record no version, and nothing reads the stored bytes back yet.- A resource edit now records the files behind it, not only a projection of them. Every version snapshot written by
resource_managerstores the resource's actual file bytes in a content-addressed store keyed per workspace, so a later rollback has the option of restoring them verbatim rather than re-rendering the file from the fields a loader read. Nothing abouthistory,compareorrollbackchanges yet — the store is additive andversion_history.snapshotis still what every reader reads. When a resource cannot be stored — a file over 1 MiB, a file that cannot be read, or a resource whose directory no longer exists — the version is still recorded and the server logs one warning naming the resource and the reason, so a version that will fall back to the projection is visible at the time it is written rather than at the rollback that needed it. - The server repairs version history that points at file bytes it can no longer find, and says so once at startup. A version row can record the exact file bytes it was taken from; if those bytes go missing — an older server build opening a newer database, or any other program writing
state.dbwithout foreign key constraints — the row would claim a recording that is not there, and a rollback would fail looking for it. The server now checks this on every start and, where it finds a break, returns the affected rows to restoring from their stored snapshot, exactly as they did before. One line naming the counts is logged, and nothing else: no history is discarded, and an intact database logs nothing at all. - A chain's
budgetand a prompt'sartifactsdeclaration are authorable throughresource_manager. Both joincreateandupdateas optional prompt parameters (additive, non-breaking), and both are clearable withunset. They were the last two keys aprompt.yamlcould declare that nothing could write: anupdatecarrying either answered "Prompt Updated" and changed nothing, and acreatecarrying both wrote neither.budgetuses the loader's field schema, so a structural cap value above the server default or a misspelled key is refused. Saved-chainmaxNodes/maxFanOutvalues are stored declarations, not narrowed execution limits; explicit Workflow IR submission performs those node-count and fan-out checks. Runtime policy retainsmaxInsertionsandpauseOnBlocking; cost remains recorded only.artifacts.fromArgumentmust still name an argument the prompt declares; an update that breaks that is refused and rolled back. - A chain's dependency
edgesare authorable throughresource_manager.edgesjoinscreateandupdateas an optional prompt parameter (additive, non-breaking), alongsidechain_steps. It had a reader and no writer: a chain whoseedgesno longer matched its steps was refused and rolled back, and the only way to correct the edge was to editprompt.yamlby hand — which this project's first rule forbids. Edges and steps are validated as one state, so send both in the same call when a step rewrite invalidates an edge, orunset:["edges"]to drop every edge and keep the authored order; omittingedgespreserves whatever the prompt already declares. The refusal now names that remedy.edgesis a prompt parameter, so a gate, framework or category call carrying it is refused by name. resource_managermanages prompt categories.resource_type: "category"accepts the same actions every other resource kind does, and writes thecategory.yamlthat gives a category its name, its description, and the two MCP defaults its prompts inherit. All five of those fields had readers in the loader long before anything could write one, so the only way to set them was to edit a file by hand — which this project's first rule forbids. The resource is the declaration, not the directory of prompts around it:createsucceeds on a directory that already holds prompts, anddeleteremoves the declaration while every prompt keeps serving under a name derived from the directory again. Nothing validates acategory.yamlwhen it is read, so the write is where it is checked — a malformed document is refused and rolled back rather than degrading silently to defaults.- A prompt that fails to load can now be repaired by the only tool allowed to author it. The loader records refused prompt files instead of discarding them, so
resource_managercan reach a file that is on disk and not in the catalog:inspectnames the file, its root, its category and the load error where it previously answeredPrompt not found,listreports every quarantined file, andupdatewrites the repair back to the file that is actually broken, clearing the record once it loads. - A broken prompt in your workspace no longer silences the one it was overriding, and no longer does so quietly. A workspace file that fails to load leaves the bundled definition serving, exactly as before — but
list,inspectand the startup log now say which root supplied the definition, which nearer file was skipped, and why. An edit that is inert is visible rather than invisible. - A gate or framework that fails to load can now be repaired by the only tool allowed to author it. The same treatment prompts got: the gate and framework loaders record refused files instead of discarding them, so
inspectnames the file, its root and the load error where it answeredGate '<id>' not found,listreports every refused file, andupdatewrites the whole definition back to the file that is actually broken and says whether it loads afterwards.reloadnow names the loader's own reason instead of telling you to check whether a file exists in the one case where it provably does. A broken copy in your workspace still leaves the bundled gate or framework serving — and now says so, names the root that is answering by path, and says when repairing the file would change what serves. Both surfaces say it:inspecton the shadowed id, and the quarantine section oflist, which previously announced no shadow for a framework at all. Nothing in a refused file's own body is returned: it is the content that failed validation. - The prompts the README tells you to run now ship with the server.
tech_evaluation_chain,research_chainandreviewwere the first command of every documented install path but were not in the package, so a fresh install answered each with prompt-not-found. They ship now, along with the eight prompts their chains call, indevelopmentand a newanalysiscategory: 51 bundled prompts across 9 categories. - A gate's
severityandenforcement_modeare now settable throughresource_manager. Both were in the gate file schema and read at runtime, but no tool parameter reached them, so every tool-authored gate silently took the loader defaults (severity: medium, enforcement derived from severity) and no caller could tell. Omitting them still preserves whatever an existing gate declares, so updates that say nothing about severity continue to leave it alone. The gate file keygate_type(framework|category|custom) stays unauthorable for now: the parameter namedgate_typealready writes the different keytype, and resolving that collision is a rename. - A framework's eleven advanced fields are now declared in the tool schema instead of riding an open passthrough.
framework_gates,template_suggestions,framework_elements,argument_suggestions,judge_prompt,processing_steps,execution_steps,execution_type_enhancements,template_enhancements,execution_flow, andquality_indicatorswere all accepted and written before this change — they were simply absent from the published schema, so no client could discover one without reading server source.framework_gateswas the sharpest case: creating a framework requires it, so the cheapest way to learn its shape was to omit it and read the error. Each parameter now carries its element shape and names which file it lands in (framework.yamlorphases.yaml). Existing calls are unaffected; a payload whose element shape was already wrong is now refused at the call rather than at load, where it was rejected anyway. inspectnow reports the gate and framework fields thatcreateandupdatewrite. A gate'sseverityandenforcement_mode, and a framework's eleven advanced fields, were settable but had no read-back: the only way to confirm what a write landed was to open the file on disk. Each is shown only when the resource actually declares it, so a gate that says nothing about severity shows no severity line rather than the loader's default — the distinction that makes the reported value worth reading. Framework values are summarized rather than dumped: arrays as counts, strings as a truncated first line.resource_managercan now remove a prompt field, not just set or preserve one.unset: ["system_message", "tools", …]clears any of 11 fields. Supplying a value sets it and omitting it preserves it, so removal previously had no spelling at all —system_message: ""wrote an empty body rather than dropping the key, and fields the writer carries forward off disk could not be cleared. Unsettingsystem_messagealso deletessystem-message.md.name,category,descriptionanduser_message_templateare refused by name: they stay settable, but a prompt missing one does not load.tool_operationseparates unbinding a script tool from deleting it. Omit it and a suppliedtoolsarray replaces the binding, leaving dropped ids'tools/{id}/files on disk;"add"unions with what is already bound;"remove"names ids intool_ids, unbinds them and deletes their directories, which is why it alone requiresconfirm: true.tool_idswithouttool_operation: "remove"is refused rather than ignored.- A blocking unknown can now interrupt a running chain instead of quietly bending it. A
blocking:trueobservation returns a structuredchain_interrupt(the unknown, affected steps, remaining plan, and accepted exits) in the response; declaringbudget.pauseOnBlockinginstead holds the run untilgate_action:"resume"orgate_action:"accept_alternative"clears it. prompt_engine(remainder:…)lets you rewrite the rest of a running chain. Submit replacement Workflow IR nodes withmode:"replace"(discards everything after the current node) ormode:"append"(extends it); achain_idcall whosecommandstarts with-->is the string-shorthand equivalent. Each contributed node accepts only{id, promptId, stepName, args, delegated}, and the server allows one accepted remainder per unknown id plus a per-run ceiling.delegatedandinlineGateCriteriaare now declarable step fields. A chain step can request context isolation without naming a model tier (delegated: true), and can carry raw::gate tokens instead of only pre-resolvedinlineGateIds; both reach YAMLchainSteps, the Workflow IR, and-->chains.- YAML chains, the Workflow IR, and
-->chains now share one step schema. A YAML chain step can declareargs(merged over the run's arguments for that step) and chain-leveledges(dependency constraints) andbudget; a step'sdelegationfield is no longer read at runtime and is now stripped rather than rejected, so existing YAML keeps loading. GET /api/v1/catalog/promptsnow lists the full served prompt catalog. Previously only the per-prompt route existed, so a catalog adapter could fetch a prompt it already knew the id for but never discover one; the new route returns metadata-only summaries under apromptskey and honors the same read-token guard as the item route.prompt_engine.inputsnow accepts typed structured arguments. Nested objects, arrays, quotes, and backslashes bypass the command-string grammar entirely; precedence is inline arguments, theninputs, then legacyoptions, then prompt defaults.- Prompt creation now supports preview, write receipts, and optimistic concurrency.
resource_manager(resource_type:"prompt", action:"validate")runs creation validation without writing; successful create/update calls return the affected files and current version, andexpected_versionrefuses a stale update before it writes. - Two new reference-grounded creative workflows are available.
>>design_museturns supplied visual evidence into an approval-gated design direction;>>practice_capturepreserves references, techniques, reactions, and unknowns, defaulting to evidence retention only — framework promotion is never applied without confirmation. - Source modules can now declare a
module.yamldescriptor naming their boundary and lifecycle. A guarded scaffold creates new boundaries, and validation checks descriptor coverage and catalog drift against structured JSON output. prompt_engine(chain_id, handoff:true)hands a running chain off to another client. It mints a single-use token; callingprompt_engine(claim_token:"…")from another client (Codex, OpenCode, a different Claude Code conversation) transfers ownership and resumes the run in the same call.script_toolgate criteria now actually execute. Declarescript_tool_idnaming a registered script tool; gate review runs it with JSON on stdin and reads back{ passed, reason?, details? }, so the script's own explanation appears in review feedback instead of a generic message.action: "preview"previews arollbackor adelete, not just a promptupdate. Name the target inpreview_action; the call writes no file, records no version, and needs noconfirm. Adeletepreview names what would be removed — for a prompt, the other prompts that reference it.resource_managerandsystem_controlnow advertisedestructiveHint, andprompt_engineadvertises that it is not destructive. Clients can use this to gate destructive actions behind operator confirmation.- Phase-guard section headers are now declared to the model, derived live from
phases.yaml. Chain steps and gated prompts render the header vocabulary and its declarable guard criteria (contains_any,contains_all,max_length) from the same source the guard evaluator grades against; a framework declaringguardson a phase with nosection_headeris refused at load. - The server now checks
config.jsonagainst its schema at startup — and the published package ships that schema, which was previously missing from the tarball. Before, an installed server had nothing to validate against, and a$schemareference inconfig.jsonpointed at nothing. The check reads the schema shipped beside the server, not the config's own$schemaline, so a config loaded from elsewhere viaMCP_CONFIG_PATHis still checked. A mismatch never stops the server: it prints one stderr warning naming the file, the section, and the key, e.g./gates: must NOT have additional properties (enabld), and stays quiet on repeated warnings for an unchanged file; a schema that can't be read is reported as not checked, never as an invalid config.system_controlwithaction: "config", operation: "validate"sent without aconfigobject now reports this same check — matched, each mismatch, schema unreadable, or not checked — instead of returning the entire configuration. config.schema.json, and the tablecpm config set/system_control(operation:"keys")validate against, are both generated from one TypeScript source.npm run generate:config-schemaderives the JSON Schema and a settable-key table — 67 dotted leaves, each carrying its type, enum, and numeric bounds (server.portaccepts 1024–65535, for example) — from theConfigFiletype, andnpm run validate:config-schemafails whichever one has drifted from it. A bound the old hand-written switch enforced is now read from the schema instead of duplicated in code.- The shipped
server/config.jsonnow holds only$schemaandversion. Every setting's default now lives in code rather than in the file a fresh install copies, so deleting a key from your ownconfig.jsonreverts it to the built-in default, not to whatever the packaged file happened to carry. hooks.expandedOutputstill defaults totruewhenconfig.jsonsays nothing. The shipped file previously carried that default explicitly; now that it ships with nohookssection, the hook's own fallback matches what every install already saw.system_control(action:"config", operation:"get")returns one key's effective value and where it came from. Sendconfig: { operation: "get", key: "<dot.path>" }; the reply reports the value as JSON and asource—file(the config file sets it),default(the built-in default; the file does not set it), orenvironment(an environment variable overrides the file and default). An undeclared key, or agetwith no key, is refused by name rather than answered with a listing. See Config Operations.- Your config file can now be
config.jsonc, with//and/* */comments and a trailing comma before}/]. Nothing else beyond JSON is accepted. A workspace resolvesconfig.jsoncfirst, thenconfig.json, then the packaged default; an explicit--config/MCP_CONFIG_PATHpath is read in whichever dialect its own extension names.config.jsonis still read, and still strictly — a comment inside one is a parse error.cpm initnow writesconfig.jsonc:$schemaand"version": 5are live, and every other setting is listed commented-out with its description, default and permitted values, generated from the schema. Every example line ends with a comma, so uncommenting one setting (together with the braces of the section it sits in) is enough on its own.initstill skips, unchanged, when either file already exists. - The generated
config.schema.jsonnow carries a fetchable$schemaaddress.cpm init, the server's--init, andcpm config resetwrite"$schema": "https://cdn.jsdelivr.net/npm/claude-prompts@5/config.schema.json"instead of the relative"./config.schema.json", so an editor with JSON Schema support validates a config file wherever it lives, not only next to an installed copy of the schema. The URL resolves once the first5.xrelease is published; a file still carrying the old relative value keeps loading exactly as before, since neither the server norcpmever reads a config's own$schemaline. - A reference page now owns the configuration file.
docs/reference/configuration.mddocuments the file lookup order, how a single value resolves across defaults, the file, environment variables and CLI flags, what the$schemaline validates, the CLI and MCP read/write surface, and the Python hooks' independent lookup chain — collecting what was previously scattered across roughly ten guides with no single owner. cpm rollbackaccepts--no-validate, likerename,moveandlink-gate. A restore is validated against the file it replaces, and until now nothing could get a restore past a validation error the command does not exempt.--no-validatewrites and records the restore unvalidated and prints no warnings; without it, a restore that brings back an invalid definition is still refused.
- chains: a chain step can run detached and report back by token (#362) (3ff285d)
- chains: a detached step is reviewed on its late report (#377) (2d079b4)
- chains: cross-client chain handoff — mint and claim a run across servers (49efdcf)
- chains: mid-chain unknown surfacing and adaptive consolidation (#254) (19ce795)
- cli: config changes are versions cpm can restore (#347) (73498a8)
- cli: every cpm write records a version and a config file has one history (#351) (3112918)
- config: a new config file names a schema address an editor can fetch (#340) (8c991b1)
- config: a workspace config can be config.jsonc, every setting a commented example (#338) (b4fc3d4)
- config: configuration is read-only over MCP (#312) (b31d920)
- config: every config key resolves the same from a 4.x file, a 5.0 file, or over MCP (#325) (8f9960e)
- config: one generated authority describes the 5.0 config.json (#322) (914b068)
- config: the server reports config.json schema mismatches (#288) (e84bf39)
- execution: a delegated step's resume must carry the worker's handoff token (#271) (02852fd)
- execution: delegated steps hand off a self-contained execution brief (8287132)
- execution: record delegation_skipped at step capture (S8) (4983c87)
- frameworks: land the phase-guard declaration contract and schema v24 (9970764)
- gates: gates attach by artifact; evidence, export, and schema follow (#300) (ea3865a)
- gates: gates become checks and reminders; verdicts bind to recorded evidence (#297) (056d5de)
- gates: run script_tool criteria, and refuse ones that cannot run (078eb7e)
- mcp-tools: a blocking gate is authored through the tool and a refusal names its path (#356) (a6efc8f)
- mcp-tools: a gate's judge routing is authored through the tool (#361) (1ff3d9e)
- mcp-tools: add argument_updates parameter for prompt updates (ce93c8a)
- mcp-tools: every mutation intent gets a verb the tool can refuse (#262) (a0af327)
- mcp-tools: every system_control parameter is one its handler reads (#366) (a018958)
- mcp-tools: inspect reports what the resource file actually declares (#273) (56138c3)
- mcp-tools: make prompt authoring verifiable (390ae26)
- mcp-tools: move session cancel to prompt_engine, add cross-workspace history reads (efe9a60)
- mcp-tools: reach skills-sync through system_control and adopt its orphans (5e08469)
- mcp-tools: the resource surface matches what the server reads and writes (#265) (320d3ce)
- prompts: add authenticated catalog detail endpoint (#244) (4d822bd)
- prompts: add reference-grounded creative workflows (f218b11)
- prompts: align the bundled creation authors with the current creation fields (#441) (3460644)
- prompts: expose workflow catalog metadata (f6e7adc)
- prompts: strategicImplement plans and judges; strategic_worker owns each row (#275) (81748d6)
- prompts: the bundled deep analysis chain asks how far its findings can be trusted (#277) (aaecac4)
- resources: a recorded version keeps its files byte for byte (#345) (03dd158)
- resources: a rollback restores files byte for byte (#346) (404d57b)
- resources: compile template chains faithfully and ship gate manifests for OpenCode parity (2e7f965)
- resources: editing a resource keeps its comments (#343) (f711401)
- scripts: add validate:registry-coherence so a write that never registers reds (457d312)
- scripts: architecture docs fail CI when they leave the code (#415) (790c505)
- scripts: assert tool-parameter coverage in validate:conformance-coverage (7513d52)
- scripts: have the validation runner record its own verdict and attribute failures (b1cd430)
- scripts: wire git hooks in linked worktrees, and detect when they are not (#239) (29961b2)
- semantic: govern guidance and module boundaries (6f4b230)
Changed
implementation_planno longer asks forcode-quality. Its last step produces a plan, soplan-qualityis the gate that reviews it;code-qualitygrades generated code, which that step does not write.- A gate a chain step's own prompt supplies now stays on that step. In
>>outline --> >>draft --> >>publishwhere onlydraft's prompt listscode-qualityingateConfiguration.include,code-qualityused to be rendered and reviewed again onpublish, so the last step was graded against a gate it never asked for. It is now rendered and reviewed ondraftonly. The same holds for a gate a step's category activates. When two steps' prompts both include a gate, each step holds it on its own. A registered gate a request names ingates, and framework gates, still reach every step. - A chain prompt's own gates are now reviewed on its last step. A gate the chain prompt declares for itself (one its
gateConfiguration.includenames, or one its category activates) was never rendered or reviewed on any step of the chain. It is now held in the last step's review, beside that step's own gates, in one review, and on no other step, so it grades the chain's finished output once. The bundledimplementation_planincludesplan-quality: its fifth step's review now holds it, where before it held nothing from the prompt, and its first four steps are unchanged.plan-qualitydeclares noenforcement_mode, so in a chain it counts as blocking, as any gate that declares none does. When aremainderappends steps, the gates move to the new last step; an investigation step inserted before the last step does not take them. In launch mode, a chain prompt's native MCP prompt message lists the gates itsincludenames and says they are reviewed on the chain's final step. resource_managerrefusesconfirmon a gate, framework or categoryupdate, instead of accepting it and ignoring it.confirmexists to approve a deletion, and only a call that deletes something you sent no replacement for takes one:delete,rollback, and a promptupdatewithtool_operation:"remove", which deletes the tool'stools/{id}/directory. A gate, framework or categoryupdatedeletes nothing, so there was nothing to confirm and the refusal now namesconfirminstead of reporting success for a parameter nothing read. Dropconfirmfrom those calls; promptupdatestill takes it.- Compatibility change: Startup refuses newer or malformed
state.dbschema authority before journal or schema changes. Use a compatible engine or an isolatedMCP_RUNTIME_ROOT; take a coherent backup and test a forward upgrade on a copy before recovering a shared runtime. - The PR template no longer carries a README Charter Compliance section. No gate read it, and every PR from #410 to #415 answered it "Not applicable". The README charter is enforced by
validate:readme, which CI runs on every route that can changeREADME.md. cpm rollbackrestores files byte for byte too, and gains--preview. It resolves what to put back through the same plannerresource_manager rollbackuses, so the two surfaces restore the same bytes rather than each deciding for themselves: a version recorded since this release has its files written back verbatim, a file the resource has now that the version never recorded is kept and named, and a version that recorded no file tree still merges its recorded fields over the entry file as before — which is whatnot_restoredcontinues to report.cpm rollback <type> <id> <version> --previewprints exactly the plan an apply would run and writes nothing at all;--jsongainspreview,files_written,files_unchangedandfiles_left_in_placebeside its existing fields. A recorded path that resolves outside the resource's own directory, and a version whose recorded bytes are missing from the store, are refused with nothing written. A prompt still records its snapshot throughcpm's own projection ofprompt.yaml— that is a separate question from its files, which restore byte-exactly like every other type's.- A rollback now restores a resource's files byte for byte, and never deletes one. A version recorded since this release carries the resource's actual bytes, so
resource_manager(action:"rollback")writes them back verbatim instead of re-rendering the file from the fields a snapshot projected: comments, key order, flow style, line endings, a byte-order mark and every non-ASCII character survive the round trip unchanged. Files whose recorded bytes already match are not written at all, so their timestamps do not move. A file the resource has now that the target version never recorded is kept, not removed, and the reply lists it by path — which means the resource may then not be byte-identical to that version, and the reply says so.preview_action:"rollback"prints exactly the plan the rollback applies. Two states refuse and write nothing rather than restoring something else: a version row whose recorded bytes are missing from the store, and a recorded path that resolves outside the resource's own directory. Versions recorded before this release, and rows that could not record their bytes, keep restoring the previous way. - A gate, framework or style in your workspace now wins over the same id in the server's resources directory, the way a prompt already did. Prompts have followed "same id in your workspace wins" since the overlay was introduced, and the resource index agreed — but gates, frameworks and styles resolved the other way, so
<workspace>/gates/code-quality/was loaded, indexed, and then quietly outranked by<workspace>/resources/gates/code-quality/. All five resource kinds now use one order: workspace overlays first, then the writable resources directory, then the definitions shipped with the server. What a write lands in is unchanged, so a write to the resources directory for an id an overlay also defines will not change what is served — remove the overlay copy, or edit it instead. - The bundled
deep_analysischain now asks how far its findings can be trusted. Its synthesis step adds a critical assessment — the strength of the evidence behind each key insight, the limits or likely bias of its sources, and whether it rests on evidence or inference — and its implications name who the findings affect. - Delegated chain steps now run on the host's default general agent; the plugin no longer ships
agents/chain-executor.md. With noagentTypedeclared, a Claude Code handoff namesgeneral-purpose; other clients (Codex, Gemini, OpenCode, Cursor) get noagent_typeparameter and use their own default instead. Agent names now pass through exactly as written — migration: a chain that explicitly setagentType: chain-executorshould remove the key or name a real host agent. - The bundled
>>create_prompt,>>create_gate, and>>create_frameworkauthors expose current creation fields and prepare drafts before writing. Prompt preparation calls canonical non-mutating validation; gate/framework preparation returns non-executing create parameters for the client to submit when authorized.create_promptguides reusable workflows with dependency edges, budgets, artifacts, delegation settings, and owned child scaffolds; canonical names take precedence over supported legacy aliases. >>strategicImplementnow compiles a tier-gated plan directly into oneprompt_engineworkflow submission, and>>tier_executeis removed with no alias. Plan row ids become node ids, the Depends column becomes dependency edges, and gate ids named in a row's Verify column become that node's inline gates — so review fires on the row that earned it instead of batching to the end of a tier.>>implementation_planalready emits rows in this format.- Destructive
resource_manageractions are now denied by one shared guard before dispatch, replacing six separate checks.prompt deletestill returns its own refusal, naming the dependent prompts that would break. - The
config.jsonshipped with the package now logs atinfoinstead ofdebug. SetLOG_LEVEL=debugfor verbose logs. It also drops thegates.modekey, which no runtime reader ever consulted. - A
config.jsonwith noversionkey is read as a 4.x file and translated to the 5.0 shape in memory, on load. The seven flatframeworks.*Frequency/*Target/styleGuidancekeys become nestedframeworks.injection.{systemPrompt,gateGuidance,styleGuidance}.{enabled,frequency,target},advanced.sessions.*becomes rootchainSessions.*,versioning.max_versions/auto_versionbecomemaxVersions/autoVersion, and every<section>.mode: "on"|"off"becomes its boolean;server.version,server.transport(when"stdio"),gates.enforcePendingVerdict,resources.prompts.defaultRegistration, and the wholeanalysissection are dropped. The file on disk is never rewritten and no action is needed — the server prints one notice per process naming every rename and drop. A file already declaring"version": 5is read as written. This translation is removed in 6.0.0. system_control(action:"config", operation:"list")now shows every effective value, defaults included, not just what the file overrides.cpm config set, and a persistedgates/frameworktoggle fromsystem_control, now edit your config file in place instead of rewriting the whole document. The edit changes only the one key's own characters, so comments, key order and every other line's formatting survive. Setting a key that exists only as a commented-out example in the generated template inserts the live key and leaves the commented example where it was; nothing tries to uncomment it.cpm config reset --forcenow writes back whichever dialect your workspace already used: the commented template to aconfig.jsonc, or the minimal{$schema, version}document to aconfig.json. Either way it never renames the file.- Every config message now names the file it acted on.
cpm config geton a missing key reportsKey '<key>' not found in <file>;cpm config validatereports<file> is validor<file> validation failed:;system_control(action:"config", operation:"validate")reports✅ <file> matches its schema.; a persisted toggle reportsPersisted <key>=<value> to <file>.;cpm validate --confignames its result by the file's own name. Help text saysconfig.jsonc. execution_records.delegation_skippedis replaced byhandoff_evidence(schema v30). The column now records one of four reasons (ok,trailer,node-line,node-mismatch) for every delegated step's resume,NULLfor a step that was never delegated.execution_recordsis ephemeral, so the schema bump drops and recreates the table on next server start with no migration needed.AGENTS.md(the Codex/OpenCode project guidance) now carries the## Pull Request Boundarysection and a Domain Ownership Matrix row for delegation handoff evidence, and drops## Validation Gates. The validation-gates section was CI-routing history those readers don't act on; the PR command sequence is one they run on every PR and previously reached them only throughCONTRIBUTING.md.CONTRIBUTING.mdstill documents the validation-gates contract in full.
- chains: remove unreachable cap-widening branch from workflow-ir validator (e557766)
- execution: delegate to the host's general agent; stop shipping chain-executor (#248) (5c2d7f1)
- gates: a gate review is a record of one node's output (#380) (03eff88)
- gates: delete the superseded criteria validator, name coverage for what it does (779e3e8)
- gates: every path that loads a gate carries every declared field (#378) (aeade2e)
- gates: every review is read and written by the node it grades (#382) (16ce9b1)
- gates: gate definitions load parsed, one settings type, one check line (#306) (4f28604)
- gates: move the chain gate walk into its own module (#447) (8d2c77e)
- mcp-tools: delete a prompt classifier and a contract adapter nothing calls (#335) (472dee8)
- mcp-tools: drop unreachable saveVersion failure branch (d2d7724)
- prompts: consolidate plan execution around workflow-IR compilation (f62a185)
- server: delete the unreachable did-you-mean duplicate (c17864d)
Fixed
- The judge's resource menu recognises a framework defined in your workspace as a framework. It matched four built-in framework ids by name, so a workspace framework's prompt was offered as an ordinary style and a guidance prompt whose id merely contained one of those names was dropped. It now asks the framework registry.
config.schema.jsondescribes the default ofhooks.expandedOutputcorrectly. The description said an absent value resolves tofalse; the server and the hooks both resolve it totrue, as the schema's own default already stated.- A gate written on one chain step now stays on that step. In
>>a :: "cite sources" --> >>b --> >>c, the criterion used to appear again in step b's and step c's Inline Gates and held each of their reviews ("These inline gates triggered the review"), so a later step was graded against a gate its author wrote for an earlier one. The same held for a named:: id:"…"on a segment and for a chain prompt step'sinlineGateIds. Each such gate now renders and is reviewed on the step that wrote it only. A registered gate a request names ingatesand framework gates still reach every step, and a gate withtarget_step_id,target_step_numberorapply_to_stepsstill reaches the steps it names. An investigation step inserted for a blocking unknown now inherits a step's own gates only from the step the unknown names intarget_step_id; an unknown naming no step passes on no step's own gates. resource_managernow lists the parameters its handlers actually use on each command, and a category create refusesskip_version.rollbackrequiredconfirm: truewhile its command never named it,historyandcomparereadsource_workspacewithout declaring it, and the prompt, gate and framework creates honouredskip_versionthat no create command listed;system_controlstatuschose itshealth,diagnosticsandframework_statusviews from an undeclaredoperation. Each is now declared on its command. A category create, which records no version, used to acceptskip_version, ignore it and answer success; it now refuses it by name. The publishedinputSchemais unchanged.- A chain no longer stalls on its first step when the gate system is switched off. After
system_controlgates disable, a chain still selected its default gates, opened a review on its first step and asked for agate_verdict, whichprompt_enginerefuses while the gate system is off, so no answer could ever move the run. Gate selection now honours the runtime switch, for the request's own workspace, exactly as it already honouredgates.enabled: falsein config: the chain renders no gates and advances on its answers, and a single prompt renders no gate guidance, as the disable message says. - A chain step's gate review now shows the step author's system message. A gated step's review render (on a gated chain, the step's first render) quoted the step's task but dropped its system message, which the step's normal render shows as a
>line; only a delegated step's worker brief carried it. The review now quotes the author's message once, under the framework block it already showed. A FAIL retry still abbreviates the task to a pointer at the render above. - The Framework Compliance gate's generic lines now reach every framework, not only the one listed last. Its three framework-agnostic lines ("Structure responses according to framework principles" and the two after it) rendered under SCAMPER alone, because the guidance listed SCAMPER last and the filter treated a line after a framework's line as part of it. Every framework's section now carries its own line followed by the three generic lines, and the order the guidance lists frameworks in no longer changes what any of them receives.
- A gate's guidance now filters to the active framework however the guidance capitalizes the framework's name. A gate whose guidance named frameworks only as authored (
- ReACT:,- Radiant:) rendered every framework's line, unheaded, under any framework, because one check compared the upper-cased framework ids case-sensitively. Under ReACT it now renders ReACT's line alone under**ReACT Framework Guidelines:**, as the shipped Framework Compliance gate already did. - Switching the active framework mid-run no longer changes the framework of a run already under way. A run started under one framework kept it on its planned steps after
system_controlswitched the active framework, but the steps it added later (an investigation step or aremainderstep) rendered the new framework, and the phase guard graded answers against the new framework's sections, holding a run whose answers followed the framework it started under. A run now records the framework its first call decided and keeps it on every step; a run started after the switch takes the new framework. - A gate sent with no verdict while its step's review is open now joins that review. A resume carrying only
gatesfor the step under review was refused as never able to fire, although the same gate sent with a FAIL joined the review. It now joins the same way: the review re-renders listing the gate, the next verdict grades it, and no retry attempt is spent, since no verdict was given. An id the review already holds changes nothing, and a gate aimed at a step the run has passed is still refused. - Advisory single prompts request a gate verdict only when an actual pending review exists; first-render guidance does not imply review enforcement.
- Initial Codex delegated briefs preserve the worker identity and system instructions.
- Codex delegation keeps model tiers as guidance and inherits the host model instead of mapping them to unverified model IDs.
- A selected response style now reaches every chain step its frequency covers, not only the first call. With a style selected (for example
#analytical) and its frequency set to inject on later steps, each step rendered after a PASS carried the framework system prompt but never the style's guidance. A resumed step's render now carries the style's**Response Style:**line wherever the style frequency injects it for that step, and none where it skips. - Every chain step that carries gates now names them and asks for its verdict the same way. A gated step rendered after an ungated one could drop its gates' guidance under the gate-guidance frequency while still asking for a verdict, a gated step that was not the last printed no
GATE_REVIEW: PASS|FAILline, and theNext:line namedgate_verdictonly on a review. Each render of a gated step now carries its gates' guidance, the gate coverage and verdict lines, and aNext:line naminggate_verdict; a step with no gates asks for no verdict, including the last step, which printed the verdict lines before. - A chain step whose answer misses its required sections holds, even when the answer arrives with a PASS verdict. Answering a gated step with
gate_verdictPASS and an answer missing the framework's required sections returned that step's "Structural Review Required" together with the next step's instructions, and the run had already moved on: one retry then closed the review and also completed the next step. The answer is now graded before the run advances, so the reply carries the structural review alone, the run stays on the answered step, and the retry advances exactly one step. The same holds for the final step, which no longer walks past the end of the run with a review open. - A
gate_actionsent with nothing to act on is refused by name. A call carryinggate_action"retry", "skip" or "abort" on a run holding no gate review and no shell check answered without an error and re-rendered the current step, so a client read its retry as applied, or its abort as a stopped run. It is now refused withgate_action "<action>" has nothing to act on: this run holds no gate review and no shell check, namingcancel: truefor an abort, and records nothing; an exhausted review and a pending shell check still take the action. - A review that has spent its attempts no longer asks for a fix against criteria it does not show. Once a chain step's review reached its retry limit, its reply withheld the gates' guidance and criteria but still said "Review … against the gate criteria", "Fix them before checking framework standards", listed the fix hints and quoted the last review. That reply now carries the "Retry Limit Reached" moves (
gate_actionretry, skip or abort) and nothing that asks for another answer; a review with attempts left still shows its criteria, hints and last review. - The reply that completes a run no longer offers to continue it. A gated single prompt, or one carrying
:: verify:"…", answered on its last call with "Execution complete." followed byContinue: chain_id=…, user_response=…, a move the finished run could only refuse. That reply now offers theRe-run:line alone; a reply on a run still under way keeps itsContinue:line. - ReACT's framework-compliance guidance now renders under its own heading, like every other framework's. Under CAGEERF, 5W1H or SCAMPER the compliance section opens with
**<Framework> Framework Guidelines:**over the framework's line; under ReACT the same section printed the bare- ReACT: …line with no heading, because the heading was matched against the upper-cased framework id (REACT) case-sensitively. The match now ignores case and keeps the name as the guidance writes it, so ReACT renders**ReACT Framework Guidelines:**. - A run keeps its framework on the steps it adds after the framework system is disabled. Disabling the system mid-run left the run's planned steps under its framework, but an investigation step a blocking unknown inserted, and a step a
remainderadded, rendered none, and the phase guard stopped grading their sections, so one run answered to two rules. A step added mid-run now renders the framework the run decided at its first call, and is graded on it, whatever the toggle says later. - A run started with the framework system disabled renders no framework.
system_control(action:"framework", operation:"disable")stopped only the steps a run added mid-flight: every planned step of a run started afterwards still rendered the active framework's block, its required sections and a framework-compliance review, and a planned answer missing those sections was sent back for them. A symbolic chain command counted as a framework override whether or not it named one, and the run's framework decision read the active framework without the toggle. With the system disabled and no@override, a new run now renders no framework on any step and grades no phase; a run already under way when the system is disabled keeps its framework on its planned steps. - A gated chain step that follows ungated steps now shows its own gate guidance. Gate guidance defaults to the first step only, and that count ran over every step, so in a chain whose first gated step came after ungated ones the gated step rendered none of its gate's guidance. The gate-guidance frequency now counts gated steps only; the framework system prompt and style guidance still count every step.
- A gate aimed at a step that a
replaceremainder dropped no longer reaches the step inserted in its place. When a call answering an inserted investigation step replaced the rest of the run, a request gate whosetarget_step_idnamed a dropped step was still inherited by the inserted step's review and graded there. A gate whose target is no longer one of the run's live steps, skipped or replaced away, now fires nowhere; a gate on a step that survives is still inherited. - A gate sent with a FAIL while its step's review is open now joins that review. A resume that sent a gate for the step under review, with a FAIL verdict alone or on a second FAIL, was refused because the open review kept the gates it opened with. The gate now joins the review: the FAIL's re-render shows its criteria, the next verdict grades it, and sending an id the review already holds changes nothing. A gate aimed at a step the run has passed is still refused.
- A skills-sync
exportto every client no longer re-sorts a client's registration list it added nothing to. One export writes every client's registrations in one pass, and each list in that pass was sorted whether or not it gained a key, so registering a resource for one client reordered another client's hand-ordered list inskills-sync.yaml. A list that gains nothing is now left exactly as written; a list that gains a key is sorted as before. - A gate review shown again after a FAIL carries the framework block when the step's injection says it should. A step's first review carried the framework system prompt, but every later review of the same step dropped it, because the review render skipped the block on any retry. It now follows the injection decision alone, as a normal step render does: with the default
frequency: 3a FAIL on step 1 re-renders step 1's review with the block, and a FAIL on step 2 re-renders its review without one. - A chain step's framework block follows the step the reply renders, not the step it answers. The framework system prompt was decided before a resume advanced the run, so passing step 1 rendered step 2 with step 1's decision: under the default
frequency: 3step 2 carried the framework block and step 4 did not. The system prompt and style guidance are now decided for the rendered step, so the frequency and target apply to it. Gate guidance still follows the step the call answered, so a step's own gate criteria keep reaching its render; a resume that fails a step and re-renders it is unchanged. - A framework override keeps applying to steps added mid-run while the framework system is disabled. Under
^ReACT(or@ReACT) with the framework system switched off, the planned steps rendered ReACT, but the investigation step a blocking unknown inserts rendered no framework block and a step aremainderadded rendered none of ReACT's required sections. Those steps now render the override too. Without an override they still render no framework while the system is disabled. - A resume that submits a FAIL can add a gate to the step it is about to retry. A
gatesentry whosetarget_step_idnamed the current step was refused as "names the step this call answers", although a FAIL verdict re-renders that step for another attempt. When the call answers the step and fails it, the gate is now accepted and the retry's review lists it. A gate aimed at a step the run has passed is still refused, and so is one sent with a verdict alone once the step's review is already open, since that review keeps the gates it opened with; that refusal now says so. - A refused resume says its run was not moved, instead of "no run was created". A resume carrying a request gate on a step the run had already passed was refused with the same
❌ Workflow rejected … Nothing was executed and no run was created.line a refused first call gets, which read as if the run were gone. The refusal now names the run and says its state is what it was before the call; a refused first call still says no run was created. - A skills-sync
exportorsyncthat registers a resource keeps the comments, key order and flow-style lists inskills-sync.yaml. Registering a key rewrote the whole file from its parsed value behind a fixed header, so the first export that added a registration dropped every comment an operator had written and re-rendered the rest in the dumper's own style. It now writes through the same source-preserving writer thepulluses. Adding an entry to a list is a structural edit, so the untouched flow lists can gain padding inside their brackets and a closing comment can gain a blank line before it; every comment and the authored key order survive. - A skills-sync
pullchanges only thenameordescriptionline it pulled inprompt.yaml. It rewrote the whole file from its parsed value, so pulling one edited description dropped every comment in the file and re-rendered the rest in the dumper's own style, quoting and flow-style lists like[alpha, beta]included. The pull now writes through the same source-preserving writerresource_managerandcpmuse, which replaces the one value and leaves every other byte as the author wrote it. - A re-opened blocking unknown can have the rest of its run rewritten again. An unknown that got a
remainder, was resolved, and was then declared again had every laterremainderrefused withcap-reached, because the one-remainder-per-unknown cap counted the remainder accepted before it was resolved. The cap now counts per declaration, as the investigation-step cap does: once the run has walked past the earlier remainder, the re-opened unknown can get a new one. A second remainder for an unknown that was never resolved is still refused. - A re-opened blocking unknown can get a new
remaindereven while its earlier appended one is still ahead of the run. Anappendremainder lands at the end of the run, and the one-remainder-per-unknown cap read its position there as when it was added, so a remainder accepted before the unknown was resolved still counted after the unknown was declared again, and every newremainderfor it was refused withcap-reacheduntil the run walked past the old one. Each remainder node now records the step the run stood on when it was accepted, and the cap reads that. A second remainder for an unknown that was never resolved is still refused. - A re-opened blocking unknown gets a new investigation step instead of reading as already investigated. An unknown that had its investigation step, was resolved, and was then declared again stayed out of
uninvestigated_unknown_idsand the interrupt'sOpen with no investigation stepline, and declaring it once more inserted nothing, because the step from before its resolution still counted against the one-step-per-unknown cap. A re-open is a new declaration: only a step inserted since the unknown's current discovery counts, so it is listed until it gets one, and declaring it again inserts it within the per-run cap. resource_managerno longer reads a prompt version of0as a version never sent.rollbackwithversion: 0andcomparewithfrom_version: 0orto_version: 0were refused "Missing required fields … version", the reply a call that omitted the version gets, because the check treated every falsy value as absent. A required field is now missing only when it is absent,nullor an empty string, which is what the gate, framework and category handlers already did;version: 0now answers "Version 0 not found".cpm rollbackrefuses a restore that brings back an inline gate definition the server would drop. The restore wrote the target version's content with no validation at all, so rolling back to a version whose prompt carried agateConfiguration.inline_gate_definitionsentry with noscopeput a gate on disk that the server then dropped on every load. The restore is now validated against the file it replaces, on both the byte-exact and the merge path: one it would bring back is refused, the files are left byte-identical and no version is recorded. A definition the current file already carries does not block the rollback and is printed as a warning;--jsoncarries warnings inwarnings.skills-sync clonerefuses a prompt whose inline gate definition the server would drop. A companion step'sprompt.yamlcarrying agateConfiguration.inline_gate_definitionsentry with noscope(or any other field the loader requires) was imported as valid, and the server then dropped that gate on every load. The import is now refused with an error naming the definition and the field, and nothing is written. Re-importing with--forceover a file that already carried the same definition still succeeds, and prints it as a warning.cpm rename,cpm moveandcpm link-gateprint the warnings validation found on the prompt, instead of succeeding silently. A prompt that already carried an inline gate definition the server drops (a missingscope, say) was renamed, moved or linked with no word about it, because the definition was reported as a warning and no success path printed warnings. The three commands now print every validation warning on stderr the waycpm validatedoes, and--jsoncarries them as awarningsarray.- A chain interrupt names the unknown its inserted step investigates when the call re-opens a resolved one. A call that declared a new blocking unknown and then re-opened one resolved earlier got an investigation step for the new one, while the "Blocking Unknown" section and
structuredContent.chain_interrupt.unknownnamed the re-opened one, because it sat earlier in the run's ledger. A re-opened unknown now counts as declared by the call that re-opens it, in that call's order, so both halves name the first unknown the call declared. cpm validatefails a prompt whose inline gate definition the server would drop. AgateConfiguration.inline_gate_definitionsentry missingscope(or any other field the loader requires) passedcpm validateandcpm init, while the server dropped that gate on every load. Both now report it as an error naming the definition and the field.cpm rename,cpm moveandcpm link-gatestill proceed on a prompt that already carried one, and refuse only one their own edit introduced.- A run another server claimed no longer stays counted on the server that gave it up. After a handoff claim, the evicting server dropped the session but kept the run in its run history and its chain membership, so the
metrics-pipelineresource still counted it insessions.totalChains(and dividedaverageStepsPerChainby it). The eviction now removes the run from both, as clearing a session does. - An arrow-chain started through the JSON command form completes with its
Re-run:line.{"command": "<arrow-chain>"}finished with noRe-run:, while the same chain typed directly named the command that starts it again. The JSON form's completion now names its inner command, which starts the same run. - A chain interrupt names the blocking unknowns that got no investigation step. One call inserts an investigation step for the first blocking unknown it declares; the others stayed open with nothing saying so, and the reply handed over one step while the run held several unknowns. The interrupt section now carries an
Open with no investigation stepline naming them, andstructuredContent.chain_interruptlists the same ids inuninvestigated_unknown_ids. Declare one again on a later call to insert its step. - A chain interrupt is about the same unknown its inserted step investigates. When one call declared two blocking unknowns, the run inserted a step investigating the first while the "Blocking Unknown" section and
structuredContent.chain_interrupt.unknownnamed the second, so the reply pointed at one unknown and handed over work on the other. Both now name the first blocking unknown the call declared; the others stay open and are named on theOpen with no investigation stepline. cpm create prompt's commented inline gate example now loads once uncommented. The scaffold'sinline_gate_definitionsexample had noscopeand noguidance, both of which the loader requires, so an author who uncommented it got a prompt whose gate was dropped on every load. The example now carries both, and it loads as written.- A chain interrupt's structured payload lists every open blocking unknown, not only the most recent. With two blocking unknowns open,
structuredContent.chain_interruptnamed one inunknownwhile the reply's text named both, so a client reading the machine half missed the other. Every interrupt payload now also carriesopen_blocking_unknowns, each open blocking unknown as{id, statement}in the order the run discovered them;unknownnames the first blocking unknown of the latest call that declared one, the one its inserted step investigates. resource_managerrefuses an inline gate the loader would drop, instead of writing it. Acreateorupdatecarrying an inline gate definition withoutscope,descriptionorguidance(or with a wrongtype) wrote the prompt and then reported "post-write verification FAILED", while the loader dropped that gate on every load, so the prompt ran without it.validate,create,updateand an updatepreviewnow refuse the write, naming the definition and each field the loader requires, and nothing is written.- Two workspaces running the same chain run no longer read or overwrite each other's step results. When one server held a run of the same chain for two workspaces under one run chain id, a step result one workspace recorded became the other's
{{previous_step_result}}and{{stepN_result}}, replacing its own. Step results and named outputs are now kept per session, so each workspace renders only what its own run recorded, and clearing one workspace's sessions releases exactly those sessions' results. - A finished run names every blocking unknown it left open, not only the last one. A run that completed with two blocking unknowns still unresolved named only the most recently raised one as unresolved, so the other went unmentioned. The completing reply now lists each.
resource_manager createno longer reports a failed verification for an inline gate that omitspass_criteria. The prompt was written and its gate loaded withpass_criteria: [], the documented default, but the reply read "Prompt Created, but post-write verification FAILED (mismatched: gateConfiguration)" because the check compared the request as sent. It now applies the loader's defaults before comparing. A definition missing a field that has no default, such asscope, still reports the mismatch: the loader drops that gate, so it is not served.- Clearing a chain's sessions in one workspace no longer erases another workspace's step results. When one server held runs of the same chain for two workspaces,
system_control(action:"session", operation:"clear", session_id:"chain-x#1")removed only the calling workspace's sessions but dropped the saved step results and run history of the whole chain, so the other workspace's next step rendered{{previous_step_result}}and{{stepN_result}}empty. A clear now releases the step results of exactly the sessions it removed, so another workspace's sessions keep theirs. - A request gate or named inline gate can no longer rewrite a built-in gate's criteria, and one that tries is refused in the reply.
gates: [{id: "content-structure", criteria: ["…"]}]or>>p :: content-structure:"…"registered a temporary gate under the built-in gate's id, and every later run on the server was reviewed against those criteria instead of the built-in ones until the server restarted. Such a call is now refused before any run starts or advances, with a[gate-id-canonical]line naming the gate; sending{id: "content-structure"}alone still uses the built-in gate. - A request gate carrying
nameanddescriptionbeside itsidandcriteriakeeps them.prompt_engineread any gate holding bothnameanddescriptionas the{name, description}quick gate and dropped the rest, sogates: [{id: "rq", name: "rq", description: "d", criteria: ["X"]}]registered under a generated id and reviewed only againstd. A gate is now a quick gate only when it carries exactly those two keys. - A finished run no longer offers to resolve a blocking unknown. A run that completed while a blocking unknown was still open ended with "Chain execution complete" followed by the "Blocking Unknown" section and its actions (answer the step,
remainder,gate_action:abort, cancel), none of which a finished run accepts. The reply now names the unknown as unresolved and offers no actions, andstructuredContent.chain_interrupt.resume.verbsis empty. - A gate criterion or
verify:command holding a quote character is refused instead of cut short.>>analyze :: "it's fine"ran with the criterionitand the rest,s fine", passed on as the prompt's arguments, with nothing to say so; a:: verify:'grep "x" out.txt'was cut the same way. The gate text is delimited by"or'and has no escape, so the command is now refused when it is parsed, with an error naming::and the quote character. - A blocking unknown's "Remaining plan" no longer lists the step the reply hands you. On the call that inserted an investigation step, and on each later call that moved the run forward, the list started at the step being rendered, so it named that step as still to come; a retry of the same step already started after it. Both now list only the steps after the one in the reply, in the text and in
structuredContent.chain_interrupt.remaining_nodes. - A finished arrow-chain run offers the command that started it as its
Re-run:line. A run started as>>sv_a topic:"T1"arrow-chain>>sv_bfinished with noRe-run:line, because no single prompt names the run. It now ends with the command exactly as it was typed, which starts the same steps with the same arguments, including when a blocking unknown inserted a step along the way. A run submitted as aworkflowstill ends without the line: it was never a command. - A finished run offers a
Re-run:line only when that line starts the same run. A chain prompt run with a gate, such as>>sv_pair :: "cite sources", finished withRe-run: >>sv_pair topic:"", which dropped the gate. The line now includes the gate and any other operator the command was given. A run started as an arrow-chain or a workflow still ends without the line: no single command names such a run, and naming only one of its steps would start a different run. A reply that could not name its prompt used to print>>promptas a stand-in. It now prints noRe-run:line. - A
Re-run:line re-runs the command that produced it. An argument value holding a quote or a backslash was re-quoted as typed, sotopic:'say "hi"'came back astopic:"say "hi""and re-ran withtopicset tosay. A:: verify:"npm test"gate came back as:: shell-verify-<number>:"Shell verification: npm test", which re-runs as a gate the model grades, with no shell command, and it dropped any:fast,loop:,max:ortimeout:option that was given. Argument values are now escaped the way the argument parser reads them, and a verify gate is written back as:: verify:"…"with the options that were typed. - A prompt's inline gate definition can no longer take over a built-in gate. With
gates.executeInlineGateDefinitionson, a definition declaringid: content-structure— or namedContent Structurewith no id — registered under the built-in gate's id, and from then on every run on the server, including runs of prompts that declared nothing, was reviewed against that definition's criteria instead of the built-in gate's. A temporary gate may not shadow a canonical gate id: such a definition is now refused with a warning naming the id, and the built-in gate keeps its criteria. - An investigation step a blocking unknown inserts follows the run's framework override. Under
^ReACT(or any operator override), theinvestigate_unknownstep the run inserts showed the server's active framework guidance instead of the one the run chose. It now shows the run's framework, as the steps aremainderadds already do. resource_managerrefuses a prompt whose inline gate definition would shadow a canonical gate. A definition declaringid: content-structure, or namedContent Structurewith noid, was accepted byvalidate,createandupdate, and the server then skipped it at run time with only a log line — so the author never heard why their criteria never applied. The write is now refused before anything is written, and the refusal names the canonical id.- A clear whose save fails no longer throws away the run's history. Clearing a session, clearing a chain's runs, or starting a run that pushed an old one out of the kept history cleared that run's step results and argument history and told the server the run had ended before the save was attempted. When the save then failed, the run came back but its history did not, and anything released at run end (its temporary gates) was already gone. Those now happen only after the save succeeds, and the same holds for recording a step's captured result, so a failed call leaves nothing half-done and its retry does it once.
- A change whose save fails no longer stays in the server's memory. When writing a run's state to the database failed, the call reported the error but the server kept the change in memory anyway, so the next call answered from a state the database never received: a second
cancelof the same run replied that it was already cancelled while the run stayed active, and a rejectedremainderleft the unsaved plan in place. A failed save now puts the run back as it was, so a retry does the work again. - A step a
remainderadds follows the run's framework override. Under^ReACT(or@ReACT), a step contributed mid-run was shown the active framework's section headers —## Contextunder CAGEERF — while its answer was graded on ReACT's phases, so a reply missing every ReACT section passed. It is now shown ReACT's guidance and headers and graded on them, as a planned step is; a run with no override is unchanged. - Optional typed authoring inputs keep omission distinct from supplied values. Omitted optional object, array, number, and boolean arguments remain absent; explicit empty arrays reach script adapters, and declared defaults such as
false,0, and{}remain available. Partial-input creation prompts can render design guidance without manufactured empty-string type errors. - A step a blocking unknown inserts is no longer read as the planned step it displaced. Several lookups found the run's current step by its position whenever the node had no step of its own, so the investigation step inserted at position 2 was handed position 2's planned step: its first review quoted that step's arguments where the investigation's statement and ledger id belong, its review took that step's
retries, and its answer was published under that step'soutputMappingname — which in turn published every later step's answer under the name of the step after it. Each step now reads only its own definition; an inserted step reviews against the default retry limit and publishes no named output, and a run with no insertion is unchanged. - An investigation step's review no longer names a gate of a step the same call's
remainderremoved. Answering an inserted investigation step together with areplaceremainder opened that step's review before the remainder was applied, so it still carried the gates of the steps the replace had just dropped —>>a --> >>b :: some-gatereviewed the investigation againstsome-gatealthoughbwas gone. The review is now worked out again against the run the remainder leaves. - A step a
remainderadds is held to the active framework's required sections, as a planned step is. Under CAGEERF a planned step's reply lists the section headers it must emit, and an answer missing them opens a structural review; the same prompt added by aremainderwas shown the framework but not the headers, so its answer was never checked for them. It now lists them and is checked the same way; under%cleanit lists none, as before. - A gated single prompt that grows past its one step renders each added step's own prompt. A run started as
>>prompt :: "criterion"has no chain steps, so every call rendered the starting prompt: once a blocking unknown inserted an investigation step, or aremainderadded steps, each of them was shown the first prompt's body instead of its own, while the run's records named the right prompt. Such a run now renders by its steps, as a chain does; a gated single prompt that nothing grew renders exactly as before. - A prompt's inline gate definition with no
idis one gate for the whole run, not a new one on every call. Withgates.executeInlineGateDefinitionson, such a definition registered a fresh gate each time its prompt was walked — once per step carrying the prompt, on every call — so a three-step chain of one prompt held nine copies after three calls and a failed step was reviewed against three of them. It now registers under the slug of its name (name: Section Contractbecomessection-contract), which a resume and a second step find and reuse, as a chain prompt's own definitions already did; two definitions in one prompt sharing a name becomesection-contractandsection-contract-2. - Answering an investigation step that a run inserted ahead of a delegated step is no longer refused for a missing handoff trailer. With
>>a ==> >>b, a blocking unknown on step 1 inserts an investigation step before the delegatedb, and its answer was refused with "Delegated node n2: the resume carries no trailer" — the check read the step at the inserted node's position in the command, which wasb. The check now reads the step the run is standing on, so the investigation is answered as itself, and a delegated step added by aremainder(delegated: true) is now checked for its ownHANDOFF RESULTtrailer, where before its answer was never checked. - A gated single prompt's execution history starts when the prompt is rendered, not when it is answered. A one-node run (
>>prompt :: "criterion") wrote noworkingrecord on the call that started it, sosystem_control execution_historyshowed its step only once the answer arrived, while every chain step showed aworkingrow from its render. The start call now recordsworkingfor the run's step, naming its prompt; a gate retry adds no second row, as on a chain. - The "Step N is delegated" note counts the steps a run inserted. After a blocking unknown inserted an investigation step ahead of a delegated step, the note on that reply read
Step 2 ("sv_b") is delegatedbesideProgress 2/3, naming the delegated step by its position in the command rather than in the run. It now readsStep 3, the number the progress line and the step's own brief use. - A chain-run save that fails now fails the call that made it, instead of being logged while the call reports success. The chain store logged and continued past every persist failure, so a
UNIQUEviolation in the hook projection made each later save of the server fail the same way, silently, while every tool call answered normally and the rows on disk stopped changing. A save now throws to the call — the client gets an error naming the SQLite failure — unless the database was locked by another connection past the busy timeout, which is still logged because the next save rewrites every run. Saves also run one at a time: two overlapping saves (a background one and a tool call's) used to collide inside one transaction and drop the second's changes without a word. - A run lists the chain-scoped gates its first call registered. With
gates.executeInlineGateDefinitionson, a prompt'sscope: chaininline definition registered on the call that starts a run was filed in one bucket shared by every run's first call on that server, sogates chain <chain_id>read "Chain-Scoped Temporary Gates: none" on the server that started the run while the run's review held the gate; only a server that claimed the run listed it. The run now files those gates under its own chain id when it takes them over. - A chain prompt's own inline gate definitions gate the steps that name them. The bundled
research_chaindefinesSource Citationson the chain prompt and names it on step 2, andtech_evaluation_chaindoes the same withVerified Claims, but only a step prompt's definitions ever registered: withgates.executeInlineGateDefinitionson, step 2's review never named the gate, whether the chain ran bare or as a remainder. The definition now registers for the step that names it, which reviews it as it would a registered gate it names. - Every step a remainder adds is gated as a planned step is, in the order the run walks it. A step added mid-run by a remainder or the arrow operator got none of the category or framework default gates when its chain declared no step gates, so a FAIL on it was refused with "carries no gates" while the same prompt as a planned step was reviewed. A
replaceremainder's steps were also reviewed against the gates of the planned steps it removed, and a remainder added to a run that began as one gated prompt opened no review of its steps' own gates, reporting the FAIL as an advisory warning instead. - A server serving two workspaces keeps saving runs when both start the same chain. Run numbers are counted per workspace, so each workspace's first run of a chain is
#1. The hook projection allowed only one row per server and chain id, so the second run's save failed. That failure was logged and dropped, and it rolled back the whole save: while both runs lived, no run of that server was written tostate.db, and none survived a handoff or a restart. The projection now keys each row by the run's own workspace scope, and its unusedrun_numbercolumn is gone (schema v33). - A run handed off twice keeps reviewing its own named gate. When a server that already ran its own gate of the same name claimed a run, the run's gate moved to a fresh id (
g146-2) and its open review followed it. A second handoff lost that step: the next server re-derived the move from the run's recorded ids alone, so the review kept namingg146-2. On a server that held ag146-2of its own, a FAIL graded that other run's criteria; on one that held none, the run's own criteria never rendered. The move a claim applies is now stored with the run, and each later claim builds on it. - A remainder naming a chain prompt whose steps declare gates runs those steps with their gates, instead of being refused. A remainder (or a
chain_idcall whose command starts with the arrow operator) that named such a chain prompt was refused by name, because a node added mid-run could not carry its step's gates: the bundledresearch_chainandtech_evaluation_chaincould not be appended at all. The appended steps now carry their gates, including across a handoff claim, and a FAIL on one opens its review. - A claimed run keeps what its remainder steps declared. The handoff claim rebuilt a run's steps without reading whether a remainder step was delegated or which arguments it took, so every step of a claimed run came back as delegated with no arguments, and the next answer to a remainder step was refused for a missing
HANDOFF RESULTtrailer. - An execution record names the prompt its step ran, and a step a failed review holds is recorded as awaiting that review. After
>>sv_a :: "sv-block"answered with a FAIL,system_control execution_historylistedcompleted step 1with no prompt: the record written with the answer saidcompletedeven while the review held the step, and it named no prompt for a single-prompt run, for a mid-run investigation step (which it recorded as the planned step at the same position), or for a step a remainder contributed. The step now readsinput_requireduntil the review clears, and every record names its prompt, including the run's closing record. - A chain step rendered as a gate review follows the call's framework injection decision. The review render ignored the decision the normal step render honours, so it carried the framework's system prompt on every review whatever the configured frequency, while the same step rendered normally followed it. Both renders now read the one decision, and the injection
targetcounts a step rendered for its gate review as a step render: under the shippedtarget: "steps"a step carries the framework block on the steps the frequency selects whether it renders normally or as a review, and a target ofgatesstill adds it on review renders only. - A resumed chain call refuses a gate on the step it answers, and a gate the run already holds stays accepted after its step.
prompt_engine(chain_id, user_response, gates:[{target_step_id:"b"}])on a run standing atbwas accepted, but the call answersband rendersc, so the gate never fired; it is now refused naming the step to target instead (target "c" or later). A gate first sent on a resume and re-sent under the sameidonce its step had passed was refused, though the run held it; it is now the held gate, as a start call's gate already was.target_step_numberpointing at a passed or answered step is refused the same waytarget_step_idis. Every such refusal now arrives in theWorkflow rejectedshape an undeclared target gets, as[gate-target-passed] node "…", and leaves the run exactly where it was. - A review opened before a handoff grades the claimed run's own gate. When the claiming server already ran a gate of the same name, the claim restored the run's gate under a fresh id (
g-2), but a gate review the first server had opened still namedg: a FAIL on it rendered and graded the other run's criteria. The claim now rewrites the run's open reviews to the restored id, andgates chain <id>andsystem_control session inspectreport that id too. The run's stored command keeps the id it was started with. - Two servers sharing one
state.dbno longer give different runs the samechain_id. Each server numbered runs from its own memory, so two live servers both startedchain-x#1; claiming one of them onto the other left the claiming server with two runs under one name, and achain_idresume advanced whichever it found first. A new run is now numbered past every run the database holds for that chain in the same workspace, whichever server owns it, so the second server's run ischain-x#2. A claim of a run whosechain_idthe claiming server already uses for a different run (one started before this fix) is refused by name and leaves the run with its owner. - A claimed run appears once in the hooks' session table. After
prompt_engine(claim_token),chain_sessionskept the first server's row for the run beside the claiming server's until some later server start cleaned it up. The bundled hooks already skipped the first row once its server had exited, but any other reader saw the run twice. The claiming server now deletes the first server's row when it writes its own. - A claimed run keeps its own named gate when the claiming server runs a gate of the same name. After
prompt_engine(claim_token), a run declaring:: g:"…"rendered and reviewed the criteria of another run on the claiming server that had declaredgtoo, because its gate was restored only under the id it was recorded with and that id was already taken. It now registers under a fresh id (g-2) on the claiming server and its steps reference that one; the other run keeps its own. - A resumed chain call refuses a gate on a step the run has already passed.
prompt_engine(chain_id, user_response, gates:[{target_step_id:"a"}])on a run standing at stepbwas accepted and the run advanced, but the gate could never fire: its step had already been answered. It is now refused withtarget_step_id "a" names a step the run has already passed, and the run does not advance. A gate on the current step or a later one is accepted as before, and so is a gate the start call sent, re-sent under itsidon a later call. - A resumed run whose workflow or arrow-chain node named a chain prompt accepts a gate on that node again. The start call of
workflow {x: <chain prompt>, y: …}accepts agatesentry targetingxand points it atx's last step, but a laterchain_idcall refused the same target withgate-target-missingand accepted the expanded step id (x-b) instead. A resume now checks itsgatesagainst the node ids the run declared and retargets them the same way; an expanded step id is refused on a resume as it is at the start. - A run whose first call failed now ends
failed, and an operator'scancelafter a later failure reads as cancelled. A start call that threw cancelled its run, and the refusal of a laterchain_id("run<id>failed on its start call") keyed on the run's newest record being the failure, so cancelling a run right after a later call threw was refused as a start-call failure too. The first run now ends with statusfailed(its temporary gates are still released), the refusal reads that status, and a cancelled run answers with the usual already-complete reply showingStatus: cancelled. - A resumed chain call refuses a gate targeting a step its run does not declare.
prompt_engine(chain_id, user_response, gates:[{target_step_id:"nope"}])was accepted: the run advanced and the gate never rendered, while a new run refused the same target. The resume now checks eachtarget_step_idagainst the run's step ids (n1for a one-step run), refuses an unknown one with[gate-target-missing]naming it, and leaves the run exactly where it was. - A chain call that fails mid-run is reported, and a run whose first call failed can no longer be resumed. A start call that threw after its run was created left the run
workingwith nothing ever shown for it, so a laterchain_idresumed it. That run is now cancelled, its temporary gates are released, and a laterchain_idis refused with "run<id>failed on its start call: ". A later call that throws keeps its runworking, as before, and the next reply on that run now begins "The previous call failed: ", once. - A run claimed by another server keeps its temporary gates. After
prompt_engine(claim_token)moved a run to a second server, its next step rendered only the gates its prompts declare: named inline gates (:: name:"…"), anonymous criteria (:: "…") and the request'sgatesstopped rendering and reviewing, because they were registered only in the first server's memory. The claiming server now re-registers each of them from the run's stored blueprint on the first call it serves. A server stopped and started again still leaves its runs nothing to resume; that is unchanged. - A command sent beside a
chain_idis refused inside the server too, not only by the tool schema. Theprompt_engineschema already refusedcommandplus achain_idthat is not an append, but a caller handing the request to the pipeline directly skipped it: the command was parsed and executed while the run named bychain_idwas ignored. The pipeline now refuses that request with the schema's own message before anything executes. - A request gate on a direct or single-operator command must target a step the command declares.
>>chainor>>chain :: "…"with agatesentry whosetarget_step_idnamed no step ("nope") opened a run and never rendered the gate, and on a single prompt the target was ignored and the gate rendered anyway. Both are now refusedgate-target-missingbefore any run is created, as the arrow-chain and workflow sources refuse it. A single prompt declares one step,n1. - A request gate on an arrow-chain must target a step the command declares. A
gatesentry whosetarget_step_idnamed no segment ("nope") was accepted, opened a run and never rendered; it is now refusedgate-target-missingbefore any run is created, as a workflow submission already refused its owngates. Agatesparameter sent beside aworkflowis checked the same way. The address space is the declared ids (n1,n2, …): a target naming a chain-prompt segment's expanded step (n1-b), which used to bind that step, is now refused too; target the segment (n1) to review its last step. - A chain call that fails partway keeps its run's gates. When a call failed with an error after its chain run had been created or resumed, the gates that call had registered were discarded, but the run itself stayed open, so resuming it with its
chain_idcontinued without them. The run now keeps the gates whether the call finished or failed. - A run that re-sends a gate id it had to register under a fresh id keeps that gate. When another live run already held
rg, a run gotrg-2, and then every resume re-sendinggates: [{ id: "rg" }]registered another copy (rg-3,rg-4, …), each with the same criteria and each reviewed at its target step. A run now resolves an id it declared to the gate it registered for it, so a resume re-sending the gate registers nothing; the same holds for a prompt'sinline_gate_definitionsentry with a declaredid, and for a named::gate when a run's command is parsed again. - A named
::gate declared in two arrow-chain segments binds each segment to its own criteria.>>a :: g:"ONE"followed by>>b :: g:"TWO"registered both gates (g,g-2), but both segments bound the second, so step 1 rendered and reviewedTWO; step 1 now bindsgwithONEand step 2 bindsg-2withTWO. - An arrow-chain naming a chain prompt is checked as the steps it runs. A segment naming a chain prompt ran every one of its steps with no node cap, so
>>sv_big(32 steps) followed by one more segment opened a 33-step run; it is now refused by name with the same❌ Workflow rejected … Nothing was executed and no run was created.reply a workflow submission gets. A requestgatesentry whosetarget_step_idnames such a segment (n1) now binds the segment's last step instead of matching nothing. - A temporary gate id belongs to the run that holds it. A second live run sending
gates: [{ id }]with an id another run held rendered and reviewed the other run's criteria; it now registers its own gate (<id>-2). A prompt'sinline_gate_definitionsentry with a declaredidfailed to register on every call after the first ("Temporary gate ID already exists"), so a later step declaring it rendered no gate and refused its FAIL; re-registering an id the run already holds is now the same gate, and a differing body merges over it. - A request or workflow gate targeting a later node reviews that node. On a workflow of four single prompts, a
gatesentry targeting the second node rendered there but a FAIL was refused as gateless, and one targeting the third or fourth node never rendered: a resume carries nogates, and the start call's were never read again. Every call of a run now enhances and reviews its step against the request gates the run was started with. - A run's temporary gates last exactly as long as the run. Criteria sent with
::, named::gates and requestgateson a chain were never removed when the run ended: a completed or cancelled run kept holding their ids (a later run declaring the same named gate gotmygate-2), and a finished run's step criteria could still be reached for up to an hour. A single-prompt call with requestgatesand no run kept them too. A run now releases every temporary gate it registered when it completes, is cancelled, pruned or cleared, and a call with no run releases its own when it replies. - A named
::gate on an arrow-chain segment reviews that segment's step. In>>athen>>b :: mygate:"criteria"(an arrow-chain),mygatebound no step, so no review listed it and its criteria appeared in no reply; on a chain-prompt segment its steps never saw it either. It now binds the step of the segment it follows, and every step of a chain-prompt segment, as a named gate after a lone chain prompt already does. A:: verify:check is unchanged. - A named
::gate reused in a later run grades that run's criteria. Starting>>my_chain :: mygate:"criteria"a second time in one server with new criteria rendered and reviewed the first run's criteria, and on a single prompt the second run's gate was silently absent. Each run now binds the gate it declared: a later run's gate registers asmygate-2(then-3, …) while a run still in progress keeps its ownmygate. - A workflow whose first node names a chain prompt gets a run id naming that prompt. Its
chain_idreadchain-<the chain's first step>#N, while the same prompt started as an arrow-chain readchain-<chain prompt>#N; both now name the prompt the client named. - A remainder naming a chain prompt runs that prompt's steps. A
remaindernode (or achain_idarrow-append) naming a chain prompt ran the chain prompt's own template as one step. It now becomes the prompt's steps,<node>-<step>ids, each rendering its own template with the node's arguments. A chain prompt whose steps declare gates or other fields a contributed step cannot carry is refused by name instead of running without them, and the node cap now counts every node the run holds after the write, with chain prompts counted as their steps. - A workflow node naming a chain prompt is checked as the steps it runs. The 32-node cap counted such a node as one, so a workflow could start a run of any length; it now counts the chain prompt's steps and refuses by name (
[cap-exceeded] node "x": Expanding chain prompt "…" yields N nodes…) before any run exists. A run-level gate whosetarget_step_idnames that node now binds its last step instead of matching nothing. - A
::gate after a chain prompt now reviews every step.>>my_chain :: "cite sources",>>my_chain :: code-qualityor>>my_chain :: mygate:"criteria"registered a gate at command level that no step's review ever read, so the criterion appeared in no reply and graded nothing. Each step now carries the gate beside its own, so every step's review lists it. A:: verify:check is unchanged: it stays the run's one command-level check. - A chain prompt named inside an arrow-chain or a submitted
workflowruns its declared steps.>>sv_chainfollowed by>>sv_b, or aworkflownode whosepromptIdis a chain prompt, ran as one step that rendered the chain prompt's own template with none of its step gates, so the declared steps never ran and a FAIL on the next step was refused for carrying no gates. The node now expands in place into the prompt's steps, exactly as a bare>>sv_chainruns them: node ids<node>-<step>(n1-a,x-b), each step with its own template and gates, the segment's arguments passed to every step, a::criterion on the segment binding each of them, and edges into or out of the node attached to its first or last step. - A FAIL on a step with no gates, and a
gate_actionon a review with attempts left, are refused by name. A FAIL verdict on a chain step (or a%cleanrun) that carries no gates opened a review grading nothing, held the step as blocking, and exhausted it with "The following gates failed after 2 attempts: ****"; it is now refused, names the step, and records nothing, while a PASS sent with the answer moves on as the answer alone would.gate_action: "skip","retry"or"abort"sent while a gate review still had attempts left came back as the same review with no error, neither applied nor refused; it now answers that the action is accepted only on an exhausted review and gives the attempt count. A pending shell check still takes agate_actionat any attempt. - A single prompt's advisory gate warning names the gates that failed. Answering a gated prompt such as
>>reviewwith a FAIL verdict warnedGate failed: <reason>, naming no gate, and sent nogate/failednotification: the review the verdict opened graded no gates. It now grades the prompt's resolved gates, as a chain step's review does, so the warning and the notifications name each one. - A chain prompt followed by an inline gate or verification runs its steps.
>>my_chain :: verify:"npm test", or any::gate after a chain prompt, ran the chain prompt's own template once per step: each step showed the chain's text instead of its own prompt, and the gates declared on its steps never applied, so a failing step opened a review naming no gate. The chain now runs exactly as it does without the operator, each step rendering its own prompt under its own gates, and a:: verify:check still grades every step's answer. - A single prompt's exhausted gate review offers
gate_action, not a verdict. Once a blocking gate on a single prompt had failed its last attempt, the reply still ended with thegate_verdict=template and the "attest reminders" guidance, and the verdict it invited was refused. It now names the moves the review accepts:gate_actionretryorskip, orcancel: true. - A gate FAIL that opens a review on a prompt with no chain steps now replies with the review instead of an error. A single prompt whose gate blocks, or a chain prompt started with an operator such as
:: verify:, answered the FAIL with "Error: Execution results missing before formatting": the step render stood aside for the review, and the review render only knows how to render a chain step. The prompt is rendered again with the review's verdict request. - A chain step's exhausted gate review no longer shows the verdict guidance. Once a review had spent its attempts, its reply still ended the gate criteria with "Attest reminders in the verdict's
remindersfield" — an invitation to send agate_verdictthat an exhausted review refuses. The reply now shows only the "Retry Limit Reached" moves (retry,skip,abort) itsNext:line already named. - A delegated step's brief states that step's own request, not the first step's. Under "Original Request Intent" every step of a
workflowsubmission or a-->/==>chain listed the arguments the run was started with, which are the first node's — so each worker's brief opened with row 1's task and row id. A step now lists its own arguments there, and the run's arguments only when the step has none. The first step, which showed no such section, now shows its own arguments too. - Every step of a chain now runs its
:: verify:check, not only the first. A:: verify:on a chain command was cleared by its first pass, so every later step's answer was accepted with the command never run. Once a step's check passes or is skipped, the check stands again for the next step with a fresh attempt budget (andloop:truekeeps the Stop hook armed), and the chain completes only when the last step's check passes. - A render call never spends a
:: verify:attempt, and re-showing a spent check's escalation no longer logs a failure. A command sent with auser_responseran the check on the render, before any step was answered, so the render came back as a failed attempt, or the check passed and was gone before the first real answer. And another answer after the attempts were spent logged "Shell verification FAILED" and cleared theloop:trueStop-hook state again, though nothing had run. - A
:: verify:check and a gate review on the same chain step no longer undo each other. A check that passed while the step's gate review was open was dropped, so the step the review's PASS moved to, and every step after it, ran with the command never run; it now stays armed for the next answer. An open review of an earlier step now holds a step whose check passes, as it already held the step's answer. Onegate_action: "skip"sent while both a spent review and a pending check held the step skipped both; it now answers the review only, and the reply says the check still holds the step. A step whose review was skipped is announcedfailedwhen its check later passes, notpassed. - A worker subagent's own
prompt_enginecalls no longer overwrite its parent's chain state, and are no longer held by the parent's pending gate review. A subagent's hooks arrive under the parent session's id, so a worker running its own chain replaced the parent's tracked chain (or cleared it when the worker's chain finished, or armed a delegation the parent never rendered), and a worker resuming its own run was denied while the parent's review was pending. The Claude Code hooks now leave a subagent's calls to the server, which still enforces the worker's own gates; a worker submitting a FAIL verdict still gets the same retry guidance. - The Claude Code stop hook now waits on a chain run that is held after its last step, and a finished run no longer comes back as a stale step. A run whose last answer was still under a
:: verify:check, or still owed a detached report, let the conversation stop, because the hook only counted steps left to render. It now keeps the conversation open for as long as the server still reports the run, and says what the run is waiting on: the gate review, the shell check, the remaining steps, or the held run. Once a run finished, the hooks could still bring back the last step they had recorded, so a completed chain resurfaced as a step to continue; a finished run now reads as no chain at all. - A chain step checked by
:: verify:now stays on that step until its check passes. A reply whose check failed was still recorded as the step's answer and moved the chain on, announcingchain/step_complete, so each fix after a bounce landed on a later step than the one it fixed. The step is now held while its check is pending, as it is under an open gate review: a failing reply moves nothing and announces nothing, and the reply whose check passes moves the chain to the next step with onestep_complete. - Skipping a
:: verify:check whose attempts are spent now continues the chain with the answer already given.gate_action: "skip"offered to "bypass verification and continue", but it only dropped the check and left the chain on the same step. It now accepts the step's answer and moves the chain to the next step, announcing itschain/step_completeon that call. A skip sent before the step has an answer is refused and says so ("nothing to skip past on step N; answer it first"), and the check stays in place. - A
:: verify:command no longer runs on the call that first renders the prompt. The command checks an answer, but it ran as soon as the prompt was rendered, before anything had been done, so a first render usually came back as "Shell Verification FAILED (Attempt 1/N)" and spent an attempt on nothing. It now runs only on a call that carries auser_response; the render shows the prompt and leaves every attempt for the answers. - A
:: verify:check whose attempts are spent now stays spent until you chooseretry,skiporabort. Anotheruser_responseafter the escalation ran the command again and counted past the limit ("Attempts: 6/5", "7/5", …); it now shows the escalation again and runs nothing. Agate_actionsent before the attempts are spent is now acted on too: it used to be ignored and the command re-run, so askipsent with an answer re-checked the answer instead of moving on. - Hooks keep seeing a chain run that is held open after its last step. A run that walked past its final step but still waited on something (a failing
:: verify:check, an open gate review, or a detached step that has not reported) was dropped fromchain_sessions, so hooks reading the server's state saw no active chain (or only the stale step they had recorded earlier) while the server kept the run open. Hooks now see a run exactly while the server considers it not complete, which also covers a run standing on its last step before that step is answered. - A chain that has run every step but is still held open now says what it is waiting for. Resuming such a run answered "the run stays open until its detached review(s) are answered" whatever held it, including a failing
:: verify:check or an open gate review on a chain with no detached steps. It now names each hold that is open and the step it belongs to, with the call that answers it:user_response(orgate_action) for a verification,gate_verdict(orgate_actiononce its retries are spent) for a gate review, and the worker'sHANDOFF RESULTfor a detached step. - Skipping a gate review whose retries are spent now continues the chain with the answer already given.
gate_action: "skip"promised to skip the gate check and continue the chain, but it only closed the review: the run stayed on the same step, re-rendered it, and the next answer opened a fresh review at attempt 1. A skip now accepts the step's recorded answer as it stands, moves the run to the next step, and announces the skipped step'schain/step_completeon that call. A skip on a step that has no answer yet is refused by name ("nothing to skip past on step N; answer it first") and records nothing;retryis unchanged. - A reply whose gate review has spent its retries now names the moves it accepts. It ended with a
Next:line and a "Gate Review Required" block asking for anothergate_verdict, which that review refuses. The reply now ends withNext: chain_id="…", gate_action="retry" | gate_action="skip" | cancel: trueand carries no verdict template, and a blocked reply on the same call does the same; a review still inside its budget keeps asking for the verdict. - A step's
chain/step_completenotification now arrives when the chain moves past that step, not when its answer is first submitted for review. An answer sent with a FAIL inside the retry budget announced the step complete although the chain stayed on it, and the PASS that later moved the chain on announced nothing. The notification (and theonStepCompletehook) now fires once, on the call that moves the chain past the step: the PASS, agate_action: "skip", or the answer itself when no review holds it. - An advisory or informational gate FAIL sent without the step's answer now spends an attempt of the step's review instead of restarting its count. It closed the review, and the next call opened a fresh one at attempt 1, so a client could send FAIL after FAIL and never reach the retry limit. The review now stays open at its count, the reply shows it with the verdict template, and a FAIL past the budget exhausts it the way a blocking gate does, leaving
gate_actionto decide. The same FAIL sent with the answer still moves the chain on with its warning. - The reply that completes a chain now shows the warning of an advisory gate FAIL sent with the last answer. The FAIL was recorded and the chain completed, but the "Chain complete" reply dropped the "Advisory Gate Warnings" block that the same FAIL shows on any earlier step, and a phase guard in warn mode lost its warning the same way. Both now render on the completing reply.
- A
gate_verdictsent on its own for an earlier step's review is now recorded against that step, not the step the chain waits on. With a delegated step waiting for its worker and the step before it still under review, a PASS sent alone wrote acompletedexecution record for the delegated step, which had produced nothing. The verdict's record now names the step whose review it answered, with that review's outcome, and the waiting step gets no record until its answer arrives. - The Claude Code delegation hook no longer blocks a worker subagent's own shell commands and edits while its parent's delegation is pending. A subagent's tool calls reach the hooks under its parent's session, so the "Delegation pending: use Task tool" denial meant for the parent also fired inside the worker the parent had just launched, and the worker's first
Bashcall was refused. The hook now recognizes a call made inside a subagent and lets it through without touching the parent's state; the parent's own edits stay blocked until it launches the worker in the foreground, and a worker launching a helper of its own does not release the parent's hold. - A prompt that turns framework gates off is no longer graded for the framework's sections, so a relayed worker handoff no longer opens a structural review the delegated step cannot answer. A prompt declaring
gateConfiguration.framework_gates: false(such asstrategic_worker) was still told to emit the active framework's section headers and graded on them, forbidden words included, so a planner relaying its worker's handoff verbatim opened a missing-sections review the delegated step could not satisfy. Such a prompt now declares no sections and nothing grades them; the framework's guidance is still shown. - A
gate_verdictorgate_actionsent on its own while a delegated step waits for its worker now answers the earlier step's review instead of being refused. When a delegated step's brief had been handed out and the step before it still had an open review, sending the verdict orgate_actionalone was refused with "the resume carries no worker reply". It now answers that earlier review, and the delegated step stays unanswered until its worker's reply arrives. - A
gate_verdictsent with no answer no longer moves a chain past a step nobody answered. With the default gates, a PASS sent on its own right after a step was completed passed the next step too, though that step's output was never sent; under an advisory gate, a FAIL sent on its own did the same and could finish the chain. A PASS like that is now refused, naming the step to answer first. A FAIL is still recorded, and an advisory one still shows its warning, but the run stays on the step. Sending the answer first and the verdict on a later call, or both together, works as before; a verdict alone still closes a review of a step that was already answered. - A step's answer is graded for its required sections even while an earlier step's structural review is still open, and its pass summary joins that step's own review. With a missing-sections review still open on step 1, a one-line answer to step 2 was never checked, and a sectioned answer's
Structural Verification: PASSsummary was added to step 1's review when step 2 had none. Step 2's answer is now graded against its own review: a missing section opens step 2's review, and a passing answer adds its summary only to a review already open on step 2. - A step's answer sent together with the verdict that closes an earlier step's structural review is still graded for its required sections. A
gate_verdictPASS on step 1's missing-sections review, sent with a one-line answer to step 2, moved the run on without checking step 2 at all. Step 2's answer is now graded on that same call: a missing section opens step 2's own review, while a re-answer of step 1 itself is still not graded twice. - A gate FAIL past the retry budget now offers the retry prompt instead of a dead end. With the default gates, a FAIL after the last attempt kept counting past the budget while the reply still read
attempt 2/2, and nogate_actionchoice appeared. The FAIL that spends the last attempt now answers with "Retry Limit Reached" and thegate_actionchoices; a furthergate_verdictis refused and records nothing untilgate_action: "retry"reopens the review or"skip"closes it. notifications/chain/completenow arrives after the final verdict, never on the call that sends the final answer back for review. A final answer missing its sections announcedchain/completeon the same call that opened its structural review, and the verdict that closed the review announced nothing. The run now completes on the call that closes its last open review, andchain/completeis its last notification.- A failing shell verification on a chain's last step holds the run open until the fix is verified, and
notifications/chain/completearrives on the call that passes. An inline:: verify:command that failed after the final answer left the runworkingwith nochain/complete, and every later call reported it complete withStatus: working, so the fix could never be verified. The run now stays open while the verification fails, the nextuser_responsere-runs the command, and the call whose re-run passes completes the run and carrieschain/completeonce. Aborting the verification cancels the run. - A structural finding on a step joins that step's review, never another open node's review.
- A gate verdict with no
HANDOFF RESULTtrailer, sent while gate reviews are open on two steps the run no longer stands on, is now refused naming both steps. It used to fail the call. Answering one step's review also no longer deletes another step's open review, so each review stays open until its own verdict closes it. - A detached step's late report that is missing its framework's required sections now opens one review naming the gates and the missing sections. The late report's structure went ungraded. A replacement report is graded again and keeps the attempts already spent. An advisory FAIL on a detached step's review says it was cleared, names the attempt it spent, and says the recorded result stands.
- Gate reviews are kept per node, so a detached step's review and the current step's review no longer share one slot. Each review records the node whose output it grades, and a
gate_verdictanswers that node's review: a verdict on a structural review of a step the run has already left closes that review and leaves the run on the step it moved to. - A
gate_verdictsent to a chain run that has walked past its last step, with no gate review open, is now refused with a message saying there is nothing to answer. It used to be ignored without a word. - A workspace gate's
shell_verifycheck can run a script that ships beside itsgate.yaml. A command such assh check.shresolved against the server's working directory instead of the gate's own directory, so the check failed with "cannot open check.sh" on every running server even though the gate loaded cleanly. resource_managerrefuses anidon categoryreloadinstead of ignoring it. A category reload always rebuilds every category, so anidwas accepted, ignored, and answered with a success that named no category.reloadis now declared per resource type, and a common parameter the contract declares for an action on other types only is refused by name, naming the types that read it. Dropidfrom a category reload;idon a prompt, gate or framework reload is unchanged.- A detached (
await: run) step's gates now review the result its worker reports, not the empty resume that moves past it. The move-on used to open the step's gate review and accept a verdict on output that did not exist yet, and the late result then went unreviewed. A late result now opens its own review in the acknowledgement. Answer it withgate_verdictand auser_responseof just the result'sHANDOFF RESULTtrailer; it coexists with the review of the step the run stands on. A FAIL asks for a replacement result that replaces the first. Shell and script checks read the recorded result. The run cannot complete while that review is open. - A gate declared
enforcement_mode: advisorynow lets the chain advance when its review fails. The mode was written togate.yamland shown byinspect, but the pipeline treated every gated chain step as blocking, so an advisory gate's FAIL held the run exactly like a blocking one. A FAIL verdict that names the failing gates inper_gateis now decided by those gates alone: if every failed gate is advisory or informational the run moves to the next step, and the reply names each one underAdvisory Gate Warnings; if any failed gate is blocking, or declares no mode, the run holds. A verdict that names no gate (an overall-only object or the legacy string) falls back to the strictest gate on the step, so an advisory gate sharing a step with the bundledcontent-structureorframework-compliancegates, which declare no mode, still holds on such a verdict. Two bundled gates change behaviour:pr-security(declaredblocking) now holds a single prompt, which before was always advisory, andpr-performance(declaredadvisory) now lets a chain advance where it used to hold. Editing a gate file no longer reset an undeclared mode toinformational, or dropped itsevaluationandblockResponseOnFailuntil the next restart. - A gated step whose answer is missing its sections now gets one review naming the gate and the missing sections, instead of losing the gate's review. The section check replaced the gate's open review with its own, so the reply dropped the gate and its criteria and restarted the retry count on the section check's budget. The missing sections now join the gate's review: one reply, headed "Structural + Gate Review Required", lists the gate's criteria and the sections to add, counts attempts against the gate's own
retry_config, and takes onegate_verdict. - A chain whose final answer fails its section check now waits for the verdict instead of claiming completion. That reply said
✓ Chain completeand asked for agate_verdictin the same breath, and the verdict was then refused because the run had finished. The reply now says→ Final step N/N — awaiting gate verdict, the verdict is accepted, and the reply to it says complete. A completed run's footer, and a final step waiting on its verdict, no longer carry aNext:line; mid-run replies keep theirs. system_control execution_historycan now show each step of a run at its latest record. A verdict submitted on its own call is appended as a new row for its step, and nothing read it back by step:listshows the whole series, so a failed step still showedcompletedabove its verdict.operation:"steps"with asession_id(the session or chain id) returns one line per step, from its newest row.- A chain step held by a blocking gate is now graded on the section headers its gate review showed it. That review is the step's only render, and nothing recorded what it declared, so the phase guard graded the step against every header any step in the run had been shown. The reviewed step's record now carries its own headers.
- A FAIL verdict sent together with the step's answer now spends one retry attempt, not two. When no review was open yet,
prompt_enginerecorded the samegate_verdicttwice in that one call, so the first FAIL already readattempt 2/2and left no retry. It is now recorded once; a second FAIL on a later call still counts as the second attempt. - A blocking gate FAIL now announces its hook and notification events before the verdict call returns, in a fixed order.
retryExhausted,responseBlockedandfailedwere fired without being awaited, so they could interleave, arrive after the reply, and lose a failure outside the emitter's own catch. They now arrive in that order, inside the call. prompt_enginerenders with the framework the request's workspace selected. Under Streamable HTTP, asystem_control framework switchsent underx-workspace-id: Achanged whatstatusreported for A and nothing else: every render, under every header, carried the launch workspace's framework guidance. A render under A now carries A's framework; another header, a request with no header, and STDIO keep the launch workspace's. The advertisedprompt_enginedescription follows the same scope, as doesresource_manager framework switch/list/inspect.- A workspace the server has never seen starts with the framework system as configured. A new header workspace started with the framework system disabled whatever the configuration said, so
system_control statusunder it read Disabled while the launch workspace was enabled. It now starts from the same configured enabled flag andframeworks.defaultFrameworkthe launch workspace starts from. - Version history and
system_control session listanswer for the request's workspace. Under Streamable HTTP, a prompt, gate, framework or category changed underx-workspace-id: Arecorded its version history under the launch workspace, soresource_manager … historyunder B listed it; andsession list(andinspect) under B showed chain runs started under A. Version history now records under the request's workspace and reads back only there —source_workspacestill reads another workspace's on purpose — and a session listing shows only the caller's own runs. With no header, and over STDIO, both behave as before. - The framework switch history is kept per workspace.
system_control analytics historylisted every workspace's switches, because the history was one list for the whole process. Each workspace now keeps its own, persisted with its framework state so it survives a restart (the most recent 50 switches). - A
resource_managerparameter sent on an action that does not read it is refused by name. Ownership was checked per resource type only, so a gate'sseveritysent withaction:"inspect"or"list"was accepted, read by nothing, and answered success. The refusal now names the actions that do read it ('severity' is not read by resource_type:"gate" action:"inspect" — only by action:"create" and "update".), taken from the contract's per-command parameter lists, which were completed to match what each handler reads.framework createnow honoursenabled, which it accepted and dropped —enabled:falsecreated an enabled framework. - A misspelled key anywhere inside a tool parameter is refused by its full path with the nearest declared key. Every strict object reachable from
resource_manager,prompt_engineandsystem_control—pass_criteria,budget,arguments,edges,injectionand the rest — answered zod's bareUnrecognized key: "tpye", and a union such as an inline gate ingatesansweredgates.0: Invalid input. They now answer'pass_criteria[0].tpye' is not a declared key — did you mean 'type'?, asevaluationandgate_verdictalready did. A prompt YAML load error names the key the same way for the objects the loader shares (injection,composer,artifacts, argumentvalidation, a step'svisibility). The published tool schemas are unchanged. resource_managerreports a chain prompt aschain. The create and update replies andanalyze_typesaidsinglefor every prompt, including a chain, while the gate suggestions in the same reply were reasoned about a chain. The classification now reads the prompt's chain steps, the same answer the gate suggestions use.system_control(action:"analytics")reports the server's real uptime and current memory. Both were copied into the report only when tool descriptions hot-reloaded, so a server that never reloaded saidUptime: 0sand showed its startup heap for as long as it ran. They are now read when the report is built.- A chain whose first step is delegated now starts. The opening
>>chaincall of a chain whose first step is delegated —await: run,delegated: true,==>or asubagentModel— was refused with "the resume carries no worker reply", a message meant for a resume, so the chain could never run. The opening call now renders that step's brief. - A gate with
evaluation.mode: judgenow hands the client the judge promptdocs/guides/judge-mode.mddescribes. The server loaded every gate without itsevaluationblock, so a judge-routed gate was reviewed exactly like a self-reviewed one and the reply was byte-identical to itsmode: selftwin. A chain step's gate review now carries an Independent judge required section above the verdict template: the context-isolated judge prompt, the gates it covers, and the model hint when one is set, with a placeholder where the client puts its step output before handing the prompt to a separate sub-agent. A blocked reply carries it too. Reviews whose gates are allselfare unchanged, and a single prompt run withgatesstill does not route to a judge. - A framework's system prompt now has one source,
systemPromptGuidanceinframework.yaml.resource_manager framework create/updateandcpm create frameworkalso wrote asystem-prompt.mdbeside it — the scaffold's copy held different starter text — and the runtime never read that file, so an edit to it changed nothing the model saw. No writer creates the file now, the three bundled copies are gone, andframework inspectreads the inline key, so frameworks without the file (CAGEERF, ReACT, 5W1H, SCAMPER, RADIANT) now report a quality score; before, the read-back looked for a key noframework.yamlcarries andinspectshowed none.cpm rollbackof a framework now restores its system prompt, which it previously skipped. Asystem-prompt.mdalready in a workspace framework is left in place and still ignored by the loader; it is no longer recorded in version history or restored by a rollback — copy any text you want served intosystemPromptGuidance.skills-syncexport likewise reads only the inline key. system_control analyticsno longer reports four counters nothing ever wrote.Total Executions,Success Rate,Failed ExecutionsandAverage Execution Timewere read from an in-memory object with no writer anywhere in the server, so every report saidTotal Executions: 0however much the server had run — andGate Adoption Ratedivided a real count of reviewed steps by that zero, which is why it read0%on every server.statusprinted the same zeroes and disagreed withanalyticsabout what0 of 0meant:0%in one,100%in the other. Both actions now report what the execution ledger holds for the calling workspace: steps recorded, completed, failed, average step duration, and aGate Review Coveragewhose two counts come from that same set of rows. The ledger records chain steps, so a single-prompt run is counted nowhere and the report says so. Uptime and memory are still shown, under a heading naming them as facts about the server process rather than the workspace, and anExecution Mode Distributionsection that rendered as an empty heading on every server is gone.analytics operation:"reset"now reports only what it clears — the framework switch metrics and the process trends — instead of printing four untouched counters before and after.- A prompt's complexity now reads the same everywhere it is reported. The gate suggester scored complexity with its own formula, weighing the raw length of a prompt's template, while the prompt analyzer weighed its template variables, chain steps and system message — so the two disagreed about 25 of the 51 bundled prompts, in both directions, and a create- or update-prompt reply could call one prompt
highin its gate reasoning andlowin its analysis line. One scorer now answers for both. A long prose prompt with few arguments is no longer called complex on length alone, so theCode Review Standardstemporary gate is suggested for fewer prompts, and a short multi-step chain is now called complex where it was not. - A
system_controlgate or framework toggle now says whether the change survives a restart, and a framework toggle'spersist: trueactually writes. Withoutpersista toggle answered with a plain success and said nothing about persistence — the one case worth mentioning, since the change is held in memory for that workspace and is gone at the next restart. The reply now says so and names the setting a persisted call would write. Withpersistit names the config version the write recorded and thecpm config rollback <n>that undoes it, or says why no version was recorded. Separately, the framework handler droppedpersiston the way in:action:"framework", operation:"enable", persist:truewas accepted, reported success, and leftconfig.jsonuntouched. It persists now, like the gate toggle always did. - The
handoff-artifactsgate now finds its own check script wherever the server was launched from. The gate named the script by a path relative to the server's working directory, which is whatever directory your MCP client started the server in — so outside a run from the package's ownserver/folder the check failed with a module-not-found error rather than grading the handoff, and a version checkpoint of the gate recorded every file except the script, so a rollback restored a gate that could not run. The script is now named relative to the gate's own directory and resolved from the root the gate was loaded from. An operator allowlist entry must cover the resolved command: usenode *, since a barenodeauthorises the commandnodealone. - A misspelled key inside
gate_verdictnow says which key and where, instead ofInvalid input.gate_verdictaccepts a structured object or a legacy string, and a failure against a two-shape parameter is reported as one nested issue the MCP SDK does not unpack — so{"per_gate": [{"pased": true}]}came back as exactlygate_verdict: Invalid input, on the gate-review submission, where the lost key also leavespassedabsent and an absentpassedreads as FAIL. The reply now reads'gate_verdict.per_gate[0].pased' is not a declared key — did you mean 'passed'?. A malformed legacy string still gets the verdict-format message, and a value that is neither shape is told what the parameter takes. The published tool schema is unchanged. - A blocked gate reply now says which attempt you are on and offers the structured verdict form. A FAIL on a gate declaring
blockResponseOnFailwithholds the step output and returns the gate review — but the attempt counter and the schema-validatedgate_verdicttemplate come from the ordinary review footer, which that reply returns before. So a retry loop bounded at five attempts showed no remaining count, and offered only the legacyGATE_REVIEW: PASS - ...string, the one submission shape that can fail to parse. The reply now carries**Attempt 2 of 5** — 3 attempts remain after this one., read from the same counter the footer reads, followed by the same JSONgate_verdicttemplate keyed to the gates under review; the string form is still accepted and still offered after it. - An update that supplies only
gate_typenow writes the file instead of reporting a success over an unchanged one.resource_managerdecides which of a gate's files a write touches from the keys the call supplied, andgate_typewas missing from that list, soaction:"update"withgate_typealone answered✅ Gate 'x' updated successfully / 📁 Files updatedwhilegate.yamlstayed byte-identical. The list is now derived from the writer's own key set, so every settable gate key is covered. - A gate verdict sent on its own call now reaches the execution history. The server's retry prompt asks a client to answer a step and submit its verdict on a later call, and in that shape the verdict was recorded nowhere: the step's row was written by the earlier call, so the verdict call appended nothing at all and
system_control execution_historyshowed the step with no verdict against it. Each such verdict now gets its own record for that step, carrying its per-gate entries, the gate it answered and whether the review cleared — appended beside the step's original row, which is left exactly as it was written. A verdict submitted with no review open carries no per-gate entries, because the numbered gate list those entries index into is only rendered when there is a review to advertise it. - A chain's final
step_completenotification now arrives beforechain/complete. On the last step of a gated run the order was reversed: the PASS verdict advanced the run past its final node — which is what latches the run terminal and announces it — while the step that verdict answered was still uncaptured, sochain/completewas delivered roughly 25ms ahead of thechain/step_completefor that step. A client that tore its handler down on the terminal event never saw the final step, and the same reversal applied to an advisory or informational FAIL on a last step. The advance is now decided by the verdict and performed after the step is captured and announced, sochain/completeis the last event of a run on every path. Both events still arrive on the call that caused them, so a Streamable HTTP client must read every message on the response stream. - A phase-guard retry hint now says what actually failed. Every failed phase produced the same line —
Ensure your response includes the required "## Context" section— including when the section WAS present and had simply fallen short of itsmin_length,contains_anyormax_lengthrule. The hint then contradicted the feedback printed directly above it, which named the measured length, and a model acting on the hint re-added a header it had already written. A section that is genuinely absent still gets that line; every other failure now reads in its own terms, naming the section and what was measured against what was required. - A chain step's
excludenow removes a gate the run asked for. A prompt declaringgateConfiguration.exclude: [prose-hygiene]and run on its own does not get that gate, but the byte-identical declaration on a chain step did: a chain collects gates as it walks, and the set a step was given came straight off that collection without being checked against the step's own declarations, so a gate that arrived from thegatesparameter or from an earlier step could not be excluded anywhere. Each step's set is now filtered by that step's own rules before it renders, soexcluderemoves the gate for that step while a sibling that did not exclude it keeps it. A gate the caller named in thegatesparameter still outranks a prompt author'sexclude, on a chain step exactly as on a single prompt. - A chain step that opts out of the framework is no longer graded against it. With
injection.system-prompt.enabled: falsea step is not shown the framework's system prompt, but it was still handed the**Required Sections**block naming that framework's headers, and the phase guard still blocked it for omitting them — headers it was told about by a sibling step's render, not its own. Such a step now declares nothing and is graded against nothing, while every other step in the run keeps both its block and its guards. - A structural review now quotes the step it actually graded. A phase-guard review grades the output of the step just captured, but it is created after the run has advanced to the next step, and it recorded no reviewed-step identity — so the review showed the NEXT step's task under
## Original Task Instructionsabove the previous step's missing sections, and a client following it revised the wrong step. The review now carries the graded step's node id and ordinal and renders that step. When the review is about step N while the run stands on step N+1, the response also keeps the render of N+1 instead of discarding it, so a delegated N+1 still arrives with the execution brief and handoff token its resume requires. - A config file now has one version history, whichever surface wrote it.
cpm config set,cpm enable/cpm disableand asystem_controlgate or framework toggle withpersist: trueall write the sameconfig.json, but each recorded its history under the working directory of the process that made the change — and a server launched from its install path does not share the operator's. One file grew two histories, each numbered from 1, andcpm config historylisted only whichever surface wrote last, so a change you could see in the file was missing from the list you roll back from. A config file's history is now keyed by that file's own directory: a toggle over MCP and acpm config setare consecutive versions of one history, and two workspaces sharing onestate.dbstill keep their histories apart. Config versions recorded before this release are not carried over — they were never reachable from more than one of these surfaces — and your next config change records the current file as version 1 before changing it. /healthand the MCPinitializeresponse now report the server's actual version instead of a hard-coded1.0.0.DEFAULT_SERVER_CONFIG.versionwas the literal string'1.0.0'for the entire life of the 4.x release line, so both surfaces — and theservice.versiontelemetry resource attribute, which reads the same config value — reported it regardless of whatpackage.jsonsaid. The build already injectedprocess.env.BUILD_VERSIONfrompackage.json; nothing read it. It is now the source, with a package.json read as the fallback outside a build (ts-jest, ts-node) and a loud failure if neither resolves, rather than another silent default. (#287)resource_managernow refuses a chain whose step names an unregistered prompt. It previously saved the chain with a warning, and the run failed at that step one invocation later; nested step ids (deep_analysis/initial_scan) were skipped by the check entirely, which is the canonical form for a chain's own steps. The refusal names each bad step by position (step 2 references unknown promptId 'run_smoke_tests'), writes nothing and consumes no version; steps of the form<chainId>/<step>stay accepted, because the same call scaffolds them. It applies to the steps a call sends — an update that sendsedges,unsetor any other parameter and no chain step still lands on a chain that was already broken, so the edit that repairs one is never itself refused. Startup and hot reload now log one warning per broken chain instead of loading it silently, andvalidate:promptsfails CI on one in the bundled tree.- A prompt that lists
framework-complianceingateConfiguration.excludeno longer gets it anyway. The exclusion was applied and then undone: having removed the gate, the server re-added it as the default framework gate, because that fallback checked only whether a framework was active and injected and never looked at the prompt'sexcludelist. Excluding it alongsideframework_gates: falseappeared to work, which is why the case survived — the second setting was doing the suppressing. Any gate named inexcludeis now withheld on the strength of that entry alone. - A prompt that turns the framework off with
injection.system-prompt.enabled: falseis no longer told to emit that framework's section headers. The response still carried the framework's**Required Sections** — emit these headers verbatim; they are graded structurallyblock, for a prompt that had been given no framework and, on this path, is graded by nothing — so it asked for structure on behalf of a contract that was not in force. The block is now omitted whenever the framework system prompt is suppressed. Chain steps are unchanged: a chain's header vocabulary is shared across the whole run. - Rolling a chain back no longer loses its
edges, and rolling a prompt back no longer leaves its script tools at the current version. A chain'sedgesand the files under a prompt'stools/{id}/were outside the version snapshot's projection, so a rollback restored the template and left both at whatever they currently were — the reply said so for the script tools and said nothing at all aboutedges. Both now come back, because a version recorded since this release restores the resource's files rather than a projection of them. cpm rollbackof a gate or a framework no longer adds a phantom "out-of-band edit" version. The CLI recorded the state a rollback replaced in a different shape from the one the MCP server records — for a gate, the rawgate.yamlmap against the server's projection with the guidance body inline — and the two can never compare equal, so every rollback of a resource the server had last written inserted an extraBridge: prior live state (era transition or out-of-band edit)version describing a change nobody made. Both surfaces now read one projection per resource type, so rolling back a server-written gate or framework adds exactly one version. A genuine out-of-band edit still gets its bridge row. Prompts are unchanged for now: building a prompt's recorded shape needs the prompt loader, which does not fit in the CLI bundle.cpm rollbacknow records the state it restored, instead of leaving the file it wrote described by no version. The command wrote both version rows before restoring the file, so the newest row —Rollback to vN— carried the version's projection with no record of the bytes the restore actually produced:cpm historylisted a state that a later rollback could not reproduce byte-exactly, and the version row was written whether or not the file write that followed succeeded. The restore now happens between the two rows, the way the MCP server has always recorded an edit: the prior state is saved with the bytes then on disk, the file is written, and the produced state is recorded with the bytes the write produced. A rollback that cannot write the file records no restored version, and a rollback whose version row cannot be written leaves the file byte-identical to what it was. Rolling back to the state already current still records nothing and says so.cpmnow trims version history to theversioning.maxVersionsyou configured, instead of always keeping 50. The CLI bound a hardcoded default into every history write, while the MCP server used the configured value — so a workspace set to keep three versions kept three after aresource_manageredit and fifty after acpm rollback, against the samestate.dband the same resource. Both surfaces now trim through one implementation, which keeps the newest N and is given the bound rather than choosing one;cpmreads it from your workspace config file, honouring both the 5.0versioning.maxVersionsspelling and the 4.xversioning.max_versionsone. A workspace that configures nothing is unchanged.- A response blocked by a gate now carries the gate review instructions instead of only naming the gate. A gate declaring
blockResponseOnFail: truesuppressed the step output as designed, but the reply it left behind was a stub — the blocking gate's id, and "resubmit withgate_verdict". The criteria to fix, the guidance text and the required response format were rendered and then dropped, because the blocked reply read a field only the single-prompt path fills, so every blocked step of a chain returned 291 characters with nothing to act on. The rendered review now travels with the block, and a block that genuinely has no instructions to show says so rather than returning a shorter reply. The suppressed step output is still not included. - A Streamable HTTP client now receives the gate, chain and framework notifications.
notifications/gate/failed,.../response_blocked,.../retry_exhausted,notifications/chain/step_complete,notifications/chain/completeandnotifications/framework/changedreached STDIO clients only: the emitter pushed through one server instance bound at startup, while HTTP builds a fresh one per request, so every event was dropped with aNot connectedwarning in the log. Each is now delivered on the response stream of the tool call that caused it, on both transports. An HTTP client must sendAccept: application/json, text/event-streamand read every message on the stream, not only the one whoseidmatches its request. - A prompt's
budgetandartifactsare now recorded in its version history, so a rollback restores them. Neither was part of a version snapshot, so the writer carried the current on-disk value forward instead: rolling a chain back to a version that declared a differentbudgetsilently kept today's, under a message saying version N had been restored. Both are now recorded and restored, through the writer that keeps the file's comments. One-time consequence, expected and not a regression of the entry below: widening what a snapshot records changes every prompt's recorded identity once, so the first edit of each prompt after upgrading writes one extra "Bridge" row capturing the prior state — subsequent identical edits write none. A chain'sedgesand itstoolsid list are still not recorded, and a rollback still leaves both at their current value; see the MCP tools reference for why and for what closes it. - An edit that changes nothing no longer spends a version, and the reply no longer claims one was saved. Sending
updatewith the values a resource already has recorded a new version row, answered📜 **Version 8** saved, and pushed the oldest row out of the 50-row history — so a retried or idempotent call could cost real rollback targets. Both writers,resource_managerandcpm, now compare the incoming snapshot against the newest recorded one inside the same transaction that assigns the version number and insert nothing when they match; the reply reads📜 No change to record — still at version 8. Rolling back to the version a resource is already on likewise records nothing and says so. Key ORDER is not a difference; the order of a list such aschain_stepsstill is. cpmnow ignores JSON key order when deciding whether a version changed, asresource_manageralready did. The 4.0.0 note claiming that property described one of the two writers:cpmcompared serialized text, so a snapshot the server had written with its own key order read as different data andcpmwrote a duplicate "Bridge" row holding identical content. Both writers now derive a snapshot's identity from one shared rule.- Editing a resource through
cpmkeeps its comments too.cpm's gate link/unlink andcpm rollbackrendered the whole file from its parsed value, so the same edit preserved comments throughresource_managerand stripped them through the CLI. Both surfaces now use one writer. - Editing a resource keeps the comments and layout of the parts the edit did not name. Every write rendered the whole file from its parsed value, so a one-field update stripped every comment, re-wrapped untouched block scalars to the serializer's own line width, re-quoted strings and reordered nothing but reformatted everything. Of the shipped resources, 22 of 105 carry comments, and updating one field of a bundled gate rewrote all 29 of its lines and kept none of its 6 comments. Prompt, gate, category and framework writes now edit the file in place: where every changed field is a plain value, only that field's own lines move. A change that alters the file's structure — adding or removing a key, or changing a list's length — still re-renders the document, which keeps the comments but may re-wrap a long value. One consequence worth knowing when diffing: a file newly written by this version may differ in quoting and line wrapping from the same file written by an earlier version. The values are identical; only the formatting the serializer chooses has changed.
- Deleting a prompt, gate, framework or category over
resource_managernow removes its version history too, instead of leaving it behind forever. The rows survived every delete: nothing could reach them, becausehistory,compareandrollbackall resolve the resource before the rows, and a resource created later under the same id inherited them — so a brand-new prompt could open at version 6 showing a stranger's snapshots.cpm deletepurged them the whole time, so the two ways of deleting the same thing did different things. Both now purge, and deleting a chain takes its steps' history with it. The delete reply reports how many rows were removed, and apreviewstill removes nothing. One edge remains:cpmand the server do not yet always agree on the workspace key a row is filed under, so history written by one surface may not be purged by the other's delete. - Renaming a resource onto an id that still has history no longer merges two version sequences into duplicate version numbers. A deleted resource's rollback history survives it by design, so an id can carry versions while nothing serves it; renaming another resource onto that id used to re-key its rows as they were, leaving two rows claiming to be v1 — and
history,compareandrollbackall select by version, so a rollback restored whichever of the two came back first. The incoming rows now continue after the target's newest version, in one transaction, while a rename onto an id with no history keeps its numbers untouched. Existing databases are repaired on first start: colliding rows are renumbered in time order, keeping every snapshot, and the startup log names how many moved. - A reminder-tier gate you attest to is now recorded, and counted apart from gates that were actually evaluated. The
remindersfield of agate_verdict—satisfiedids andnot_applicableids with their reasons — was rendered into the verdict text and read back by nothing. It now lands on the step's execution record beside the per-gate verdicts, shown inexecution_historywith its own≡mark, and reported byanalyticsas Reminder Attestations rather than folded into a gate's pass rate: a reminder has no evaluator, so counting your own "yes" as a gate that passed would overstate it. A reminder naming a gate the review did not advertise is dropped rather than recorded against it. system_control execution_historyandanalyticsnow report per-gate outcomes, and "Gate Validations" stops always reading 0. A reviewed step's ledger row lists each graded gate with its verdict and the reviewer's rationale, under the record line;analyticsreports how many ledger records carry a verdict and breaks it down passed/failed per gate id.gateValidationCounthad no writer anywhere in the server, so the analytics report printedGate Validations: 0andGate Adoption Rate: 0%on a machine that had run hundreds of gated steps; it is now read from the execution ledger. Records written before this carry no verdicts and render exactly as they did.- A failed gate review now tells you which gates failed and why, instead of re-showing the whole template. When you submit
gate_verdictwith aper_gatelist, the reply names each gate you marked FAIL with the rationale you gave it — over both STDIO and Streamable HTTP — and the structuredgateValidation.failedGates[].reasoncarries that rationale instead of the fixed stringGate failed (blockResponseOnFail enabled). Until now the per-gate detail you submitted was parsed and read by nothing, so a five-gate review that failed two of them got back the same five-gate template and you had to re-derive which two from your own echoed prior response. A verdict with noper_gatelist, or an all-PASS one, renders exactly as before. - Over STDIO, the server now actually sends the MCP notifications it advertises — including the gate ones, which had never been delivered either. The emitter was bound to the
McpServerwrapper through a cast, and SDK v2 movednotification()onto the innerServer, so its capability check was permanently false and every notification became a silent skip: measured by driving both transports, five refusals and zero sends. It is now bound to the object that carries the method, a dropped notification logs at warning level naming which condition failed, and a test pins the SDK shape so the next move of that method fails the build instead of going quiet. The send is also no longer fire-and-forget past its own error handling: the SDK's notification call is asynchronous, and a rejected one used to escape the surroundingtryentirely — over Streamable HTTP, where the bound server is never connected, the first chain step of the first run took the whole process down one tick after that request had already reported success. Over Streamable HTTP notifications still do not reach clients — a fresh server is built per request and the only HTTP publish channel carries list-changed events alone — but they now log a warning and are dropped, rather than ending the server. - Clients now receive chain and framework notifications.
notifications/chain/step_complete,notifications/chain/completeandnotifications/framework/changedwere declared, documented and consumed by the telemetry observer, and nothing in the server ever sent one — so a client watching for a chain to finish waited forever and polled instead. Each now fires once, after the fact it announces has persisted;notifications/chain/completecarriesstatus: "completed" | "failed" | "cancelled". The matching OpenTelemetry span eventschain.step_complete,chain.completeandchain.failedare live for the same reason. >>create_gatenow applies the enforcement mode you set, and>>create_frameworkno longer sends two fields the server derives itself. The bundledgate_builderscript forwardedenforcementModeunder that spelling whileresource_managerdeclaresenforcement_mode, so a gate authored with a non-default enforcement mode was created with whatever the loader derived from its severity — and, once an undeclared parameter became a refusal, the whole create failed.framework_builderforwardedtype(a gate parameter; a framework's type is derived from its id) andversionas a semver string against a parameter declared as a number. Both now say so in their warnings rather than dropping the value silently, andguidanceFileis no longer forwarded at all — a gate'sguidance.mdis written from the inlineguidancetext.cpm rollbackno longer writes a key the entry file does not use. A version snapshot records the spelling the server's own writer accepts, and the writer translates it on the way out; the CLI merges the snapshot straight into the YAML, so wherever the two spellings differ it added a second key beside the real one. Rolling back a framework wrotetool_descriptions:next totoolDescriptions:and reportedtoolDescriptionsas "not restored" while writing it under a name nothing reads; rolling back a prompt wrote thesystemMessageanduserMessageTemplateBODIES intoprompt.yaml, which carries only thesystemMessageFile/userMessageTemplateFilepointers the loader reads. The framework key is now renamed on the way in and genuinely restored; the two prompt bodies are reported as not restored, like a gate'sguidancealready was. A new end-to-end check runs both rollback paths over the same version and fails on any future divergence.- The
cpm.scope.sourcetelemetry attribute now reports the request's real identity source instead of the constant"default". It readExecutionContext.state.scope.source, a field that duplicatedstate.identityand whose only writer was deleted as test-only dead code at P4.52 — so every execution reported"default"regardless of whether identity was resolved from a token, a header, or a launch default. It now readsstate.identity.context?.identitySource, the fieldIdentityResolutionStageactually populates. The duplicatestate.scopefield is removed. resource_manager updateon an id that is neither loaded nor quarantined is now refused as unknown, instead of reaching patch or diagnosis logic meant for a real prompt. A bare id that only exists as a nested chain step's composite id (e.g.plan_table, addressable only asimplementation_plan/plan_table) fell through to the same path a quarantine repair uses — an empty draft defaulted tocategory: "general"— and apatchagainst it failed withanchor_not_found, naming an anchor that could never have existed rather than the id that does not resolve. The refusal now fires before any patch or diagnosis runs, and names the composite id when the bare id matches exactly one loaded nested step (or lists every match when it matches several).- A
resource_managerupdatepatchinguser_message_template(orsystem_message) on a hand-authored, inline-template prompt now changes what the prompt renders.userMessageTemplate/systemMessageare notprompt.yaml-resident keys — their text lives in a separate.mdfile for every prompt the writer itself produces — so a call supplying only one of them leftprompt.yamluntouched. That is correct when the file already points at its.mdfile, but a hand-authored prompt that declares the template inline (no*Filepointer) has no pointer for the write to leave alone: the write rewrote the.mdfile and the stale inline value kept rendering, because the loader prefers a*Filepointer over the inline key only when one is present. The write now also converts the file to pointer form — the shape every writer-produced prompt already has — whenever it finds the field declared inline; a prompt already in pointer form is untouched, unchanged from before. cpm createrefuses a single-file prompt's id instead of shadowing it.resourceExistschecked only the directory form ({cat}/{id}/prompt.yaml), socpm create prompt xnext to an existing single-file{cat}/x.yamlcreatedx/, which the loader then served in the file's place.createnow refuses by name, citing the existing path, when either form already exists.moveandrenamewere unaffected — both already check both forms throughoccupantOf.cpm renamenow finds a reference living in a.mdtemplate, including a nested chain step's own template.scanReferencesread only each resource's own entry YAML, so a>>idreference in asystemMessageFile/userMessageTemplateFiletemplate was invisible — a real rename reported 1 remaining reference where there were 4. It now reads every file the loader would read for each resource and matches on an id boundary, so scanning forfoono longer misreportsfoo_bar.- Editing one field of a gate or category through
resource_managerno longer deletes the fields it did not touch.GateFileWriterandCategoryFileWriterhad the same defect just fixed for prompts: an update rebuiltgate.yaml/category.yamlfrom the fields each writer models, so a description-only edit dropped any key outside that set — a hand-authored keyGateDefinitionSchema's.passthrough()(orCategorySchema's unrejected-unknown-key parsing) otherwise allows. Both writers now start from the file on disk and overlay only the keys the call decides, sharing the overlay logic the prompt fix introduced. Comments and YAML layout are still rewritten whenever either file is written. - Editing one field of a prompt through
resource_managerno longer deletes the fields it did not touch. Anupdatethat changed only a prompt'sdescriptionrebuiltprompt.yamlfrom the fields the writer knows about, so it deleted theartifactsdeclaration that attaches artifact-scoped gates, a chain'sedgesandbudget, and any extra key the prompt schema allows. It also reordered keys insidearguments. An update now starts from the file on disk and changes only the keys it was asked to change. Comments and YAML layout are still rewritten wheneverprompt.yamlitself is written. cpmnow lists, validates and edits nested chain steps and single-file prompts, andcpm deleteon a single-file prompt removes that file, not its category.cpmread only{category}/{id}/prompt.yaml, so the 15 chain steps shipped underresources/prompts(deep_analysis/deep_diveand the rest) and any{category}/{id}.yamlprompt were invisible tolistandvalidate, which now check 51 bundled prompts instead of 36. Every command now finds a prompt under the id the server serves it as.deleteremoves a single-file prompt's file alone and a directory prompt's directory as before.history,compareanddeleteread and remove a nested step's own version history rather than that of a top-level prompt with the same last segment.renameandmovevalidate the target before writing, so a refused target leaves the prompt untouched rather than half-renamed.renamerenames a nested step or a single-file prompt in place but refuses to change anything but the last segment of an id, andmoverefuses a nested step, which moves with its chain.- Version history is looked up by each resource's own type and id, never from its path.
skills-syncread a resource's history from its directory path, which made any gate in a workspace under a directory namedpromptsread as a prompt, so its export recorded another resource's version number.cpm renamenow moves history only after the renamed files pass validation; a rename that validation rolled back used to leave the history under the new id. A failedcpm createno longer deletes history stored under the id it tried to use, which can be a deleted prompt's history thatresource_managerkeeps. - Renaming or deleting a chain with
cpmnow moves or removes its steps' history, and a renamed chain still finds its steps. Chain steps record their history aschain/step, and only the chain's own history moved with it, so after a rename or delete the steps' history stayed under ids nothing serves. A renamed chain also keptpromptId: chain/stepin itschainSteps, which then named steps that no longer existed. Both now follow the new id. References from other prompts are still reported and left unchanged. cpm rename,move,toggleandlink-gateno longer leave a partial write in place when a mutation throws instead of returning a failure.runValidatedMutationrolled back a validation failure but not an exception raised inside the mutation it wraps, so a bug there could leave the resource tree holding whatever had been written before the throw. A throw is now rolled back the same way a validation failure is, and the original error still propagates afterward — the helper restores the tree; it does not decide how the error is reported.- Enabling or disabling the framework system now takes effect in the workspace that asked for it. Over Streamable HTTP one server process serves several workspaces, and
system_control(action:"framework", operation:"enable"|"disable")read the calling workspace's state to decide whether the toggle changed anything, then wrote the launch workspace's. The caller was answered "Framework System Enabled" while its own workspace stayed disabled, and an unrelated project's setting flipped instead.operation:"reset_metrics"on theanalyticsaction had the same split. A toggle or reset written under a workspace is also read back after a restart now — startup loaded only the launch workspace's row and answered every other workspace with defaults, which also lost aswitchperformed by one of them. - A gate FAIL in advisory or informational mode now reports the step the run actually moved to. Both modes clear the pending review and advance the step, but the handler doing that work was started and not waited for, so the response was assembled from a snapshot taken before either finished: it reported the run still holding on the step it had moved off, and a failure to clear or advance vanished with nothing logged. The blocking mode and the PASS path were already correct. Separately, a chain session blueprint whose write failed at the end of an execution was reported as persisted — the handler meant to catch that failure could never see it.
- Hooks now read the config file in your workspace, not only the one the server ships with.
hooks/lib/config_loader.pylooked only atserver/config.json, sohooks.expandedOutputset in a workspace config never reached a hook — the setting silently did nothing. Hooks now resolveMCP_WORKSPACE, then${CLAUDE_PLUGIN_DATA}, then the packagedserver/directory, tryingconfig.jsoncthenconfig.jsonin each and using the first file found; an unreadable or malformed file still falls back to defaults rather than making a hook refuse to run. npx claude-prompts --initnow writes the config file the README has long said it scaffolds. The flag created the starter prompts and stopped there;config.jsonconly appeared if you separately rancpm init. It now calls the same config generatorcpm initdoes, right after the workspace is created, and leaves an existingconfig.jsoncorconfig.jsonalone.cpmversion history now findsstate.dbwhere the server actually writes it.resolveStateDbPathwalked up from the resource directory looking for an existingruntime-state/directory and ignoredMCP_RUNTIME_ROOTentirely, so under the Claude Code plugin — which setsMCP_RUNTIME_ROOTto a directorycpmnever discovers on disk —cpm history/compare/rollbackread a different database from the one the server wrote, or found none at all. It now checksMCP_RUNTIME_ROOT, thenMCP_WORKSPACE, the same precedence and resolutionPathResolver.getRuntimeRoot()uses, before falling back to the old ancestor walk when neither is set.- A
:: verify:"…" loop:truereply now says so when the loop's state could not be armed or cleared, instead of reporting as if it had been. Both failures were caught insideVerifyActiveStateStore, logged as a warning, and discarded, so a write that failed to armverify-state.dbstill let verification proceed, and a clear that failed to run on pass or escalation still returned the normal "passed" or "attempts exhausted" reply — in both cases with the Stop hook left reading a stale or missing record. The write and the clear now throw, and the pipeline's single error boundary reports the failure to the caller instead of the reply that would otherwise have claimed success. skills_sync pullandclonenow report how many files they wrote. Both commands write real files —pullwrites edited prose and frontmatter back to canonical YAML,clonescaffolds a new resource from an externalSKILL.md— but the run report'swrittencount stayed 0 for both and the MCP reply'sFiles writtenline was left out entirely rather than print a false0, so the only way to know what either command did was to list the directory yourself.pullnow counts distinct files written and breaks the count out by client the same wayexport/syncalready do;clonecounts the distinct files it wrote across its base document, prose files, and any companion gates or chain steps.- Updating a framework no longer resets its version or rewrites files the update did not change.
resource_managerframeworkupdatewroteversion: 1.0.0over the stored value on every call, so a description edit took CAGEERF from 2.0.0 to 1.0.0. It also wroteenabled: true, so editing a disabled framework turned it back on. It also rewroteframework.yamland the phases file every time, dropping their comments and reflowing flow-style YAML, even when the update named neither. An update now keeps the storedversionandenabled, and writes only the files whose content it changes. The others stay byte-identical, comments included.rollbackfollows the same rule. skills-syncand the prompt delete lookup skip the directories the server skips.exporttreated a_-prefixed directory as a category and a category-leveltools/as a prompt, and reported each as a skipped prompt.clonecopied a skill'sresources/tools/or_drafts/step into the new prompt, wheretools/holds script tools. The prompt delete lookup also listed_-prefixed files andtool.yamlas prompts. A newvalidate:prompt-walkscheck finds every function that walks a prompts tree and fails when its module does not use the shared skip rules.cpm listandcpm validatenow see only the prompts the server serves.cpmread any directory under a category as a prompt, so it listed and validated prompts under_drafts/, under a category'stools/directory, and under top-levelbackup/ornode_modules/. It also listed aprompt.yamlplaced directly in a top-level directory, which the server treats as a category, not a prompt. A broken prompt in any of those places no longer failscpm validate.cpmnow uses the server's own rules instead of a copy.- Every part of the server now agrees on which top-level directories are prompt categories. The server never served prompts under a top-level
backup/directory, but the resource index, the startup change comparison,validate:prompts,skills-sync exportandcpmall treatedbackup/as a category, so its prompts were indexed, announced, validated and exported. A top-levelnode_modules/was the opposite case: the server served it as a category but never watched it for changes. Neither directory is a category anywhere now, and neither is a.- or_-prefixed one.validate:prompt-walksnow also fails when a module that walks a prompts tree does not use the shared category rule. - A prompt category named
toolsis now indexed and announced, and prompts under a_-prefixed directory no longer are. The resource index and the startup change comparison treatedtoolsas reserved at the top of the prompts directory too, so a whole category the server serves was missing from both. The index also walked into directories such as_drafts/, which the server never serves, and indexed what it found there.validate:promptsnow skips the same directories the server skips, so a file under a prompt'stools/or under_drafts/can no longer fail CI as a prompt. - Three architecture checks that read as enforcement now enforce something.
validate:archstated in its own header — and two source comments cited by name — that nothing may import the server's composition root, and no rule expressed it: a plantedmodules/file importingruntime/cruised with zero errors while the control that should fail did fail, so the harness was working and the rule was simply missing.no-imports-into-runtimenow forbids that direction, and the three MCP tool files that were reaching into the composition root to record resource changes read the change log through a contract inshared/instead; the log itself, and what it records, are unchanged.cli-shared-no-runtime— cited by the CLI barrel's own header and by the CLI guide since the CLI split, and never written — now exists, and checks the whole reachable closure rather than the first hop, which is what keeps the server out of the CLI bundle.no-runtime-state-direct-accesswas deleted rather than repaired: it aimed at a directory the server writes at run time, so no module could ever match it, and two of its three exemptions named files that no longer exist. - A prompt you edit in a workspace overlay, or in the bundled catalog, now reloads. The catalog is assembled from the bundled tree, your primary prompts directory and every workspace overlay, but only the primary directory was watched — so an edit to a prompt that lives in either of the others was never noticed, and the previous version kept being served until a restart. Every root the catalog is assembled from is now watched, using the same resolution the loader itself uses. An overlay directory that does not exist when the server starts is still not picked up until a restart.
- Editing a framework's files now reloads it, and deleting its folder now releases it, instead of both waiting for a restart. Every framework file change was watched and then discarded: the reload event was built without the framework id it needed, so the handler refused it with
missing frameworkId, skipping. An editedframework.yamlkept serving its previous guidance, and a framework whose folder you removed stayed selected and kept rendering. Both now take effect within about a second — an edit re-reads the definition, and a removal moves the selection to the framework named byframeworks.defaultFramework. Files a framework carries that are not YAML, such assystem-prompt.md, also reach the reload now instead of logging a warning and stopping. - Repairing or breaking a style now takes effect without restarting the server. The server kept two separate style loaders — one whose record of unloadable files decided what
resource_indexcontains, and one the style hot reload actually refreshed. The two agreed the moment the server started and diverged from the first edit afterwards, so fixing a brokenstyle.yamlleft it missing from the index until a restart, and breaking a working one left it listed there as though nothing had happened. Anything reading that index, including the Python hooks, was offered a style the server cannot load or denied one it can. There is one style loader now, and the hot reload refreshes it. A loaded style also records which root it was served from, the way gates and frameworks already did, so a style shadowed by a copy in another root can be named by path rather than by id alone. - Hot reload now watches your prompts folder itself, not the folder above it. It watched the parent of the prompts folder instead — your whole
resources/folder, or with the older<workspace>/promptslayout the entire workspace,logs/andruntime-state/included — and polled everything in it. Every other resource type has its own watcher, so edits to prompts, gates, frameworks and styles still reload as before. Editing a framework file now triggers one prompt reload instead of two. - A resources folder you create while the server runs is now picked up, and something you create then delete right away no longer lingers. A workspace overlay folder (
<workspace>/prompts,gates,frameworksorstyles) that did not exist at startup was ignored until a restart; it is now read and watched as soon as it appears. And when a create wrote the first file into a folder that did not exist yet, deleting that entry within about a second went unnoticed — a framework created, selected and deleted stayed selected, and a gate stayed listed. Once the watcher reaches a new folder, the server now checks what it holds against the disk and drops anything that is gone. system_control(action:"changes")now records edits in your workspace overlay folders, not just your primary ones. A resource present in two folders is recorded once, for the copy that is served, so editing a copy another folder overrides no longer logs a change that did nothing. A nested chain-step prompt is logged under the same id the startup comparison uses. The bundled catalog is still not tracked unless it is your primary folder.- A resource file that fails to load in the root you edit is no longer silent because a higher root answers. Gates, frameworks and styles stopped reading roots once one of them served an id, so a malformed file below the winner was never opened: no warning, no
listentry, no repair target. That used to hide a broken workspace overlay; since overlays started outranking the writable root it hid the file an operator is most likely to have just edited — a broken<workspace>/resources/gates/foobehind a legacy<workspace>/gates/foowas invisible on every surface. Every root holding the id is now read and every refusal recorded, while the id is still served by the first root that yields a valid definition, unchanged. inspecton a category declared in another root now says whether your update would actually displace it. It said an update "writes your own copy under , which then takes precedence". A write lands in your resources root, and a workspace overlay outranks that root — and an overlay is exactly where a category declaration in "another root" most often lives, because overlays are read last and win. So for the overlay case the sentence was false in both halves. It now names the root that wins: either your copy would take precedence over the bundled tree, or the overlay outranks your resources root and the category would still be declared from there, so edit it where it lives.- Repairing a prompt whose broken file is in another root no longer reports the repaired prompt as absent. A prompt write always lands in your own resources root, so repairing a malformed file that lives in the bundled tree leaves that file exactly as broken while a working copy appears in yours. The response called that "Still quarantined — the prompt remains absent from the catalog", and the second half was false: the prompt was being served, from the copy the same call had just written. It now distinguishes the three outcomes the gate and framework repairs already did — repaired, refused-file-in-another-root, still-refused — and says which root serves the id afterwards, read back from the loader rather than assumed. Two bundled-resource refusals that said your copy "takes precedence" now say what it takes precedence over.
- A repair no longer tells you your copy is serving when it is not. Repairing a quarantined gate or framework answered, verbatim, that it "wrote
<path>, which takes precedence, so<id>now serves your copy" — and a repair writes the root aresource_managerwrite lands in, which workspace overlays outrank. Both halves could be wrong at once: the write takes precedence over nothing, and the id can go on being answered by an overlay. The response now reads which root actually served the id back off the loader after the reload, and says either that your copy is what answers, naming the root, or that<other root>still outranks it and the copy you just wrote is not what answers.inspectandlistdrop the same assumption: a refused file behind a served one is "another file", not "a nearer file", and repairing it changes what serves only if its root outranks the one answering. The three places that documented their repair target as matching root precedence now say what is actually true of it — a repair goes to the root you can write to. - The startup warning for a prompt file that failed to load now names the root that is answering instead of it. It said
(another root is serving this id — your edit is not live), which leaves the operator to work out whether the bundled tree, the primary root or an overlay took over — the fact that decides whether repairing the file changes what serves. It now names that root by path, exactly asresource_manager'slistandinspectalready did, and a refused file that shadows nothing still carries no such clause. - An edit to a chain's step prompt is now reported in the change log. The startup comparison stopped descending as soon as a directory held a
prompt.yaml, on the reasoning that such a directory is a prompt rather than a folder of them — but a chain directory is both, holding its own definition alongside its steps. Fifteen step prompts ship below that line (every step ofdeep_analysis,implementation_plan,quick_decisionandscaffold_project), so editing one outside the server produced noadded, nomodifiedand noremovedat the next start. They are now reported, under the same qualified id the server serves them by (deep_analysis/deep_dive), rather than under a bare step name nothing answers. The first start after upgrading reports each of them once as an external addition — they are entering the change log for the first time — and nothing is reported as removed. createno longer overwrites a gate file the server refused to load.createchecked the gate registry, and a refused file is absent from it — so creating a gate whose id already existed on disk in a broken state rewrote that file and reported plain success, never mentioning that anything had been there. That was one of several dead ends while a quarantined gate was unreachable by every verb; onceupdatelearned to repair one,createwas the remaining door and the only one that gave no signal. It now refuses, naming the refused file, its load error, and theupdateverb that repairs it. A gate the registry does know still refuses with the message it always did, and an id nothing on disk claims still creates normally. Frameworks were affected in a narrower shape, closed by the next entry.createno longer writes over a framework whose only file is a broken one in another root. Unlike gates, framework creation did consult the filesystem — but only the directory it writes into, so a framework whose sole file is a refused one in the bundled tree or another resource root was invisible to all three of its existence checks.createthen wrote a fresh copy that took over the id, reported plain success, and never mentioned the broken file, which stayed exactly as broken. It now refuses, naming the refused file, its load error, and theupdateverb that repairs it. A framework the server already serves still refuses with the message it always did, one whose broken file is in the writable root still refuses with the directory check's message, and an id nothing on disk claims still creates normally.- Repairing a quarantined gate or framework now records a version. Both repair paths deliberately wrote no version row, on the sound reasoning that a file which never loaded has no prior state to diff against — but
version_historyis durable and nothing regenerates it, so the first recorded version of a repaired resource was its second edit. A repair now records the state it produced as version 1, described as a repair and carrying no diff, and the response says what was recorded instead of saying that nothing was. The prior broken content is never recorded: it is the content that failed validation. - A hot reload no longer re-publishes the gates, frameworks and styles the server refused to load. The index that Python hooks read withholds a file its loader refused, and did so correctly at startup — but the re-sync that runs after every hot reload was handed the prompt loader's refusal record alone, so the first reload put every refused gate, framework and style back into
resource_indexfor a hook to hand toprompt_engine, which rejects them. The refusal record for all four kinds is now assembled once, at startup, and the reload reads that same one. A file repaired between two reloads still appears at the next sync, as it did before. - The resource index and the change log no longer report resources the server cannot load. Both ran their own filesystem walk and accepted any file whose YAML parsed, which is considerably more than the loaders accept — so
resource_index, the table the Python hooks read, could name a prompt or gate thatprompt_enginethen rejects, and the startup change log recorded an externaladdedfor a file that never entered the catalog. Each now reads what the loaders refused: the index withholds such a file and deletes a stale row for one, reporting it as refused rather than as removed, because a file still sitting on disk has not been deleted; the change log records no event at all and keeps the file's key live, so a repair is later reported as theaddedormodifiedit actually is. A refused file's absence is a correct index, not a missing one, and the startup warning says so by name and path. - A prompt written as a single
{category}/{id}.yamlfile is now indexed. The index walk only ever looked at directories, so the second of the two supported prompt layouts reached no row at all — such a prompt loaded and served while being invisible to every hook that reads the index, including prompt suggestion. Nested single-file prompts are indexed under the qualified id the server answers to (chain/step, notstep), and where both spellings of one id exist the directory form wins, which is what the loader already did.category.yaml, a script tool'stool.yaml,_-prefixed files and anything at the prompts root are still correctly absent. - The startup change log no longer reports a category declaration as a new prompt. Its filesystem walk called any
.yamlfile in a prompts directory a single-file prompt, socategory.yaml— which ships underguidance/— was announced at every startup as an externally added prompt with the idcategory, and so would a script tool'stool.yamlor a stray file at the prompts root. The rule for which filenames are a prompt is now the prompt loader's own, shared rather than copied, so the three walks that ask the question agree on the answer. A genuine{category}/{id}.yamlprompt is still reported. - A script tool whose definition fails to load is no longer offered as an available tool. Its index row was deliberately kept so the removal sweep would not report it as deleted — a file that fails validation is still on disk — but
skills-syncreads the same rows, so the tool stayed published while being unusable. It is now reported as refused: no index row, absent from the tool listing, and still not counted as removed. It is no longer counted as a sync error either, because nothing failed on the indexer's side; look for it on the refusal line, which names the tool directory to repair. A valid tool under the same prompt is unaffected. - A response style the server cannot load is no longer listed in the resource index. Prompts, gates and frameworks already withheld a file their loader refused; styles were the fourth kind the index walks and the only one with no such record, so a
style.yamlthat failed validation left the catalog and stayed in the table the Python hooks read. It is now reported as refused by name and path at startup, and a stale row for one is deleted. The startup inventory line for styles also changes meaning as a result: it counts the styles that actually load, not the directories that exist, so a broken style now shows up as a smaller number rather than not at all. - An
updateon a prompt that could not load reported success and wrote the wrong file. It answered✅ Prompt Updated, created a second prompt undergeneral/, printed a move that never happened, and left the broken file untouched. The repair now lands on the file that is actually broken. - Two framework fields were written to disk and never read back.
judge_promptwas loaded from its file and dropped before the framework was reassembled, andexecution_type_enhancementswas written tophases.yamland read by nothing — so an update carrying either could not preserve it. Both now survive the round trip. The mapping they were missing from is now a declared field table rather than fifteen near-identical blocks, so a future field cannot be omitted the same silent way. - A category's metadata written into a workspace was discarded when the same category also shipped with the server. The catalog merged categories by display name and let the bundled copy win, so a personal
category.yamlwas written correctly, loaded correctly, and then dropped. Categories now follow the same rule prompts do — same id, your copy wins — and two categories that merely share a display name no longer evict each other. - The HTTP
create_categoryendpoint required a name and a description and wrote neither. It created an empty directory and let the loader invent both. It now writes the declaration it was given, through the same path the MCP tool uses, and to the resources root the server actually serves from rather than the one named in the config file. - Enabling or disabling the framework system now reports a save that failed instead of answering with success. The toggle updated memory and then wrote to the database, but the write's failure was caught and discarded inside the framework state store, so
system_control(action:"framework", operation:"enable"|"disable")answered✅ Framework System Enabledwhile the database still held the old value, and the setting reverted at the next restart. The same toggle applied fromconfig.jsonwas started and never waited for, so its failure was reported to no one. The write is now awaited and its failure surfaces: the tool answers with the error, and a toggle applied from configuration at startup stops the server with that error rather than serving state it failed to record. - A Streamable HTTP request whose server cannot be built now reports why. Every HTTP request is served by a freshly built server instance. When building one failed, the request answered
Internal server errorand nothing said what went wrong unless the failing step happened to log for itself — a failure while binding resources or prompts left no trace anywhere, so consecutive failing requests produced an opaque body and a silent log. The cause is now written to the server log, naming the stage that failed (tools, resources, or prompts). The request itself still fails: a half-built instance is never served, because a tool surface missing the tools that failed to bind reads as success to every client. Requests after it are unaffected, as they were before. - A create or update-prompt reply no longer suggests a gate that does not exist.
ContentAnalyzersuggestedbasic_validationon every analysis, and the prompt-analysis failure fallback suggestedexecution_validation— neither has ever named a gate underresources/gates/. Both analyzers have no gate registry to check a name against, so both now suggest none;GateAnalyzer's registry-backed recommendation (for examplecontent-structure), which the same reply also carries, is unaffected. - A
skills_syncreply now states what the run did, instead of ending in progress lines with no summary.export,sync,diff, andpulldiscarded the machine-readable report the run itself produced, so a caller — including on a preview — could not tell how many resources loaded or files were written without inspecting folders directly, and a preview's "nothing was written" outcome was never stated. The response now leads with resources loaded, files written (0 and labeled preview when previewing), a per-client write breakdown, directories pruned, drifted-resource counts fordiff, and a failure count, all read from the run's own report. - Switching to a framework you created now works, and the selection survives a restart and the framework's removal. The framework state kept its own list of frameworks, which held only the ones shipped with the package, so it could not resolve a framework created with
resource_manageror stored in your workspace. After switching to one, every later request over Streamable HTTP failed withInternal server erroruntil the server restarted,tools/listincluded, and over STDIO every prompt failed withActive framework ... not found. A restart replaced a workspace framework selection with the first framework available. The framework state and the tools now share one list, so a created or workspace framework can be switched to and stays selected across a restart. Deleting the active framework withresource_managernow selects the framework named byframeworks.defaultFrameworkand saves that selection, and the same default replaces a saved selection whose framework no longer exists at startup. The framework named byframeworks.defaultFrameworkcannot be deleted, and nothing is removed, until that setting names another framework. A server whose saved selection andframeworks.defaultFrameworkboth name no registered framework refuses to start, with an error naming that setting, instead of selecting another framework. A change toframeworks.defaultFrameworkwhile the server runs applies to that fallback and to the delete refusal without a restart. - A prompt hot reload now picks up a
skills-sync.yamlexport change, instead of only a restart. A prompt newly registered for export stayed inprompts/listuntil the server restarted, and one unregistered stayed hidden just as long: the filesystem-watch reload path refreshed prompt content and re-registered every prompt, but never recomputed which onesskills-sync.yamlexports — only startup, and a manual full refresh, did that. The exported set is now recomputed on that path too, so editingskills-sync.yamland then touching a prompt file (or waiting for the next unrelated one) is enough; editingskills-sync.yamlalone still needs one of those, since it is not itself watched. @idnow applies a framework created while the server runs. The command parser recognized only the framework ids that existed at startup, so afterresource_managercreated a framework,@<id> >>promptwas read as literal text and failed with a parse error until the server restarted. The parser now checks the current framework set on every command: a new framework applies at once, and after a framework is deleted@<id>is literal text again.- A server started over STDIO now writes nothing to stderr on a clean start. A startup trace of about a dozen
DEBUG:lines printed on every launch regardless of verbosity; it now prints only with--verboseor--debug-startup. Two warnings that described no real problem are also gone:Invalid framework state, falling back to defaultson a first start with no saved framework state, andChainSessionStore: no DatabasePort provided, persistence disabledfrom a chain session store that went on to persist anyway. - Warnings and errors now reach stderr when the server runs over STDIO. The logger previously printed nothing to the console under STDIO unless
CIorNODE_ENV=testwas set, so a warning or error your server raised existed only in its log file. Warnings and errors now go to stderr; info and debug messages follow them there only with--verboseor--debug-startup. Nothing is written to stdout, and CI and HTTP output are unchanged. Theconfig.jsonschema warning goes through the same logger now, so its line starts with[WARN]like every other warning. - Tools are now described by their contract descriptions in every install. The tool description loader looked for the generated descriptions in a directory that exists in neither the repository nor the published package, so every server silently fell back to built-in descriptions instead. The descriptions are now bundled with the server, so the fallback no longer triggers.
- Skills sync now reads the prompts and gates in your workspace, not only the ones the package ships. Every command read resources and
skills-sync.yamlfrom the package folder, so withMCP_WORKSPACEset, as the Claude Code plugin sets it, exporting a workspace prompt or gate found nothing, andclonewrote into the folder a plugin update replaces. Sources now resolve the way the server resolves them: the bundled resources with the workspace layered over them, a workspace entry replacing a bundled one with the same identity, andMCP_RESOURCES_PATHtaking precedence over the workspace.skills-sync.yamlis read from the workspace when it holds one, else from the package, and is no longer looked for beside anMCP_RESOURCES_PATHdirectory.clonewrites into the workspace,patchwrites underMCP_RUNTIME_ROOTwhen it is set, andpullrefuses to write an edit into a bundled resource while a workspace or resources directory is configured, naming the file. The server'sprompts/listreads the same file, so a prompt registered in the workspace'sskills-sync.yamlis no longer listed beside its skill. An export readsgates.harnessCoversfrom theconfig.jsonthe server reads, so it leaves out the same reminders the runtime does. - A server started with
--workspaceand noMCP_WORKSPACEnow finds its own workspace's skills-sync sources over MCP.system_control'sskills_syncaction re-resolved its directories from the environment on every call instead of reading the server's own path resolver, so--workspacealone reported the package'sskills-sync.yamland could not see a resource the workspace held, while settingMCP_WORKSPACEworked correctly.statusandexportnow resolve through the running server the same way every other resource type does; the standalone CLI (npm run skills:exportand friends), which has no running server to read from, is unaffected. readme_improveranddocumentation_changeget their documentation gates back. Gates now attach by the artifacts a prompt declares (B13), and neither bundled prompt declared any, soinformation-placement,product-positioning-fidelity,prose-hygiene, andsemantic-discoverabilitysilently stopped reaching them — a documentation prompt with no declared artifacts got none of the checks meant for documentation. Both now declareartifacts.produces: [docs, readme], which they always write regardless ofdoc_type, and picked upapi-documentationthe same way.code-quality's gate definition dropped itsprompt_categorieslist, which had been unreachable since gates started attaching by artifact.system_controlnow receives every parameter its actions read. Its input schema declared 11 fields and dropped the rest before the action ran, so askills_syncexport withpreview: truewrote skills for every registered client instead of previewing, andclientdid not narrow it. Aninjectionoverride answeredInvalid injection type: undefined.configrestore andmaintenancerestart refused even withconfirm: true, and thechanges,analyticshistory andexecution_historyfilters ran on their defaults. Each parameter is now declared with its type. Thechangesresource type filter isresource_type, the nameskills_syncalready uses; the documentedresourceTypenever reached the action.resource_managernow declaresfull_restart, and its promptguidereceivesgoalandinclude_legacy, which it read but was never sent.- Creating a prompt, a gate or a framework now records the created state as version 1.
resource_manager(action:"create")previously saved no version at all, so its receipt reportedcurrent_version: 0, and the firstupdatehad to invent a "prior live state" bridge row for a resource that had never been recorded — a reader saw version 0, then a bridge, then 2.createnow saves version 1 as part of the same write, so its receipt reportscurrent_version: 1,action:"history"shows one row with a create description, and the firstupdatesaves version 2 directly with no bridge. The bridge row is unchanged for a resource created before this fix, or edited out of band, where the latest recorded snapshot genuinely differs from the live state. - A framework error now names the directory the framework actually resolves from, not the server's own install directory.
reload, and acreatewhose rollback could not finish, reported a path built from the server's package root — so with a workspace or personal resources library configured, the message pointed at a directory the framework never occupied. Each now resolves through the same writable and bundled roots the framework loader itself reads. - Updating a prompt stored as a single file (
{category}/{id}.yaml) no longer leaves it defined twice. The writer only ever produces the directory layout ({id}/prompt.yamlplus message files), and its lookup for a prompt already on disk skipped single-file matches, so an update wrote a fresh{id}/directory beside the untouched file — one definition updated, one going stale, both on disk, both served. An update of a single-file prompt now converts it to directory layout in the same write: the flat file is deleted, the directory is written, and the preview names the deleted file alongside the files it creates. The lookup scans every category, not just the update's target one, so a single-file prompt updated with a newcategoryin the same call is converted and relocated together — before this, that combination left the flat file under its old category and a new directory under the new one, served under both. - A prompt update preview now shows the change the update makes, in the files it makes it in. For a prompt stored as a directory —
prompt.yamlbesideuser-message.mdandsystem-message.md— the preview rendered the prompt's fields as a single YAML file named after the prompt, with long template lines rewrapped, while the update itself changed one line ofuser-message.md. The preview, the diff the update reports and the change counts saved in version history now all come from the write itself: each file is named by its path under the resources root, lines appear as the file holds them, a created file is diffed from/dev/null, a deleted one to it, and files the update leaves untouched are left out. A prompt rollback preview does the same. - A gate or framework update, and its rollback preview, now show the change in the files it is made in. A gate is stored as
gate.yamlbesideguidance.md, and a framework asframework.yamlbesidesystem-prompt.md,phases.yamlandjudge-prompt.md, but the diff an update reported and the one a rollback preview showed rendered the saved fields as a single YAML file named after the resource — so a guidance edit appeared as a change togate.yaml, andguidance.mdnever appeared at all. Both now come from the write itself, as prompt previews already do: each file is named by its path under the resources root, lines appear as the file holds them, and files the write leaves untouched are left out. The change counts saved in version history follow the same diff. - A framework update now writes its phases and judge-prompt content to the file names
framework.yamlitself declares, instead of alwaysphases.yamlandjudge-prompt.md. A framework can name its companion files withphasesFileandjudgePromptFile, and the loader follows those names — but an update that touched phases orjudge_promptsilently rewrote the declaration to the default names, orphaning the declared file with stale content, and an update that touched neither still wrote a stray default-named file beside the declared one while the declaration (and the loader) kept pointing at the original. The update diff and rollback preview now name whichever file each write actually lands in, and a declared name that resolves outside the framework's own folder is refused rather than joined. - Updating a gate without touching its guidance no longer rewrites
guidance.mdone byte short.resource_manager(resource_type:"gate", action:"update")omittingguidancefalls back to the gate's loaded guidance text, and that text had lost its file's trailing newline on load — so an update that changed onlyactivation(ordescription, or anything else) rewroteguidance.mdwithout it, and the repo's own commit-time formatting check then blocked on a file the caller never asked to change. Loadingguidance.mdnow keeps its exact bytes, the same way the prompt and framework loaders already keepsystem-message.md,user-message.md,system-prompt.md, andjudge-prompt.mdverbatim.create,update, androllbackall writeguidance.mdthrough the same path, which now appends a trailing newline to guidance that lacks one instead of leaving it off — restoring a version recorded before this fix, whose guidance was already.trim()'d and unrecoverable byte-for-byte, now reconstructs the same one-newline ending every shipped gate has rather than reproducing the missing-newline defect a second way. - A gate update naming only
guidanceno longer rewritesgate.yaml.resource_manager(resource_type:"gate", action:"update")planned and wrotegate.yamlon every update regardless of which fields were supplied, so a guidance-only edit re-serialized it into the writer's own key order and dropped any hand-authored comment — invisible in a hand-authored gate whosegate.yamlhad never been touched, since a gate the tool itself had written re-serialized back to the same bytes.gate.yamlandguidance.mdare now two independently-scoped writes: a field that lives ingate.yamlis what putsgate.yamlin scope,guidanceis what putsguidance.mdin scope, and the reported diff names only the file that actually changed.createandrollbackstill write the whole gate, both files. - A prompt you create through the Claude Code plugin, and its version history, now survive a plugin update. Created prompts, gates and frameworks, gate and framework settings, argument history and version history were all saved inside the plugin's install folder, which Claude Code replaces on every update. The plugin now keeps them in the plugin data folder Claude Code preserves across updates, and its hooks read them from there. Nothing already saved in an install folder is moved: a prompt created before this release stays in that version's
server/resources/prompts/folder, and creating it again withresource_managersaves it in the data folder. - A server given a workspace now saves the prompts, gates and frameworks you create inside that workspace, even when the workspace starts empty. A workspace set with
MCP_WORKSPACEor--workspacethat had noresources/prompts/folder, or noresources/gates/orresources/frameworks/, sent each new resource of that type into the package's own folder, where the next reinstall or update deleted it. New resources now go to<workspace>/resources/<type>/, created by the first write; the bundled resources still load underneath, a workspace resource with the same id still wins, and an edit to a file in that folder reloads without a restart. A workspace that already keeps a type in the older<workspace>/prompts/layout keeps writing there. An explicitMCP_RESOURCES_PATHand a server with no workspace set write where they did before. - Disabling gates through
system_controlnow survives a restart. While gates are off,prompt_enginestops advertisinggates,gate_verdictandgate_action, which is how a client saves the tokens those parameters cost — but the next server start advertised all three again, because the toggle was saved for your workspace and startup never read it back. A server now reads every saved toggle when it starts, on STDIO and Streamable HTTP. A toggle saved before workspace isolation, under no workspace, is adopted into the launch workspace once, if that workspace has no toggle of its own. - Deleting a framework the package ships is now refused, and names where it lives.
resource_manager(resource_type:"framework", action:"delete")refused only four of the eight bundled frameworks;focus,liquescent,radiantandverifywere removed from the package tree in a default install, and the tool reported success. A comment claimed a second guard covered them, but that guard is reachable only when the framework directory is absent, which is never true for the ids it named. The same four also loaded as operator-created rather than built-in, so every surface readingisBuiltInmisreported them. Frameworks you created yourself are still deletable;validate:shipped-frameworksnow fails when the declared set and the shipped tree disagree in either direction. - A failed resource update no longer leaves a version row describing a file that was never written. The write and the version record describing it ran as two sequential steps in every prompt, gate and framework update and rollback, so a failure between them left one without the other — and
version_historyis durable, so a row pointing at a state no file ever held was permanent. Both halves now commit together: a failed write records nothing, and a failed record restores the files byte-for-byte.validate:mutation-atomicityfails when a version record is added outside a write transaction. - Prompt-id normalization now has a single implementation. Two copies previously disagreed on nested chain-step ids (
deep_analysis/initial_scancollapsed todeep_analysisinitial_scanin one of them);npm run validate:promptsnow enforces the id convention (snake_casefor>>/-->command ids,kebab-casefor everything else) and accepts a--rootflag to check an external prompt library. - Creating a prompt with a spaced category no longer reports failure for a write that succeeded.
My Categorystill writes tomy-category/on disk as before; the post-write check now compares both sides through the same normalization instead of the caller's raw value against the reloaded slug. - A failed prompt mutation no longer opens with a success headline. The response now chooses its headline after verification runs, so a failed write leads with the failure and the reason instead of
✅ **Prompt Created**followed by a buried error. - The claims-conformance test suite no longer writes into the packaged resource tree. A resources-path default previously outranked the test's isolated workspace, leaving real files behind on an otherwise-passing run; a teardown check now fails any run that adds a file to the packaged tree.
npm run validate:promptsnow catches prompts that fail schema validation instead of silently dropping them from the catalog at load. It also accepts--rootto check a library outside the repository, and it found (and fixed) two gates in the bundledpractice_captureprompt that had never loaded.- Editing a bundled prompt or framework no longer destroys the parts of it that aren't fields (chain steps, tool files). An update now copies the resource's whole source subtree into your own resources root before editing, and the receipt notes that the resource has forked and will no longer track bundled updates;
inspectreports where a loaded resource came from and whether editing it would fork a copy. - Deleting a bundled prompt, gate, or framework no longer reports a false "not found."
deletenow states where the resource actually lives and that updating it (not deleting) is how to change it; deleting your own copy still succeeds and now says the bundled version is being served again. - A caller-supplied
categoryor resourceidcan no longer write outside the resources root via a value like../../ESCAPED. Every join built from payload data now resolves through a containment check, andnpm run validate:contained-resource-writesenforces it on every run so a new resource type can't ship unguarded. - A chain step named
.or..can no longer scaffold outside its parent prompt's own directory. Naming it in a prompt's very firstchain_steps— before any directory existed to catch it — wrote a strayprompt.yamlinto the parent's category folder and reported success; naming it in an update to an existing chain was silently dropped instead, also reporting success. Both are now refused by name before anything is written. - The e2e test suite no longer inherits your local environment. All server spawns now route through one helper that fully controls the child's environment, so a developer's
MCP_RESOURCES_PATHcan no longer leak a personal prompt library into what a test asserts against. - Startup now checks that the SQLite resource index matches the live catalog and reports any mismatch. Previously the index could silently diverge — including reporting zero gates, frameworks, and styles — which broke every Python hook that reads it and made bundled, executable prompts answer
Unknown prompt. - A prompt id defined in two categories now reports the conflict by file path instead of silently dropping one definition. The warning distinguishes an intentional override (matching category — the overlay contract) from a real conflict (same id, different category).
- The startup inventory line now names what it excluded, e.g.
prompts: 120 (16 categories, 3 invalid)instead of a bare success count. - Two internal prompt-count structures that disagreed on catalog size now use the same key. A consumer's reported prompt count no longer depends on which internal structure it happened to read.
- A workspace resource directory now overlays the bundled resources instead of replacing them. Previously, adding even one workspace framework, prompt, or style directory could silently drop the rest of the bundled catalog (or crash startup entirely); a same-id workspace entry still wins, matching what
--helphas always described. - The startup inventory now names the bundled base it merged with your workspace, e.g.
gates: 25 — <workspace>/resources/gates. - Skills Sync now refuses to write through a symlinked skill directory instead of letting one client's export silently overwrite another's through the link. It names the real target the symlink resolves to.
resource_managerresponses now include machine-readablestructuredContentfor prompt validation drafts, inspect metadata, write receipts, and version conflicts, not just text.- Reloading a prompt no longer creates a spurious extra version from argument key reordering alone. Version comparison now ignores JSON key order.
- Claude Code plugin hooks now resolve Python 3 reliably on Windows, macOS, and Linux, including a stock python.org Windows install that lacks the
python3.exeWindowsApps alias. (#224) - A chain running in one client (Claude Code, Codex, OpenCode) can no longer surface inside another client's conversation. Hooks now key chain recovery to the chain the conversation itself recorded, instead of scanning for the newest active chain across every server sharing the same
state.db. - Hook session rows now clear once their chain run resolves, instead of surviving until a 24-hour sweep and letting compact recovery re-inject an already-resolved gate.
- Attaching a gate to a prompt call no longer breaks prompts whose arguments are all optional or defaulted.
>>reference_demo :: code-qualitypreviously failed with "Missing required field: text" while>>reference_demoalone worked; both paths now resolve default arguments the same way. - A chain step can now reach its own prompt's script tools. Script lookup already checked a prompt-local
tools/{id}/before the shared workspace location, but the chain renderer never passed the information needed to use it; it now does. - A prompt's
tools/*/tool.yamlscript-tool manifest is no longer validated against the prompt schema, which previously logged an unavoidable error on every startup. - A phase guard no longer blocks on a section header the executing prompt was never told to produce. A header the prompt doesn't declare is now advisory only; a declared-and-missing header still enforces as before.
- Gate and framework version history now records the state each edit produced, matching how prompt version history already worked — the state currently on disk is now always rollback-reachable.
- Rollback now restores the recorded snapshot exactly instead of merging in current values. A version whose snapshot can't fully rebuild the resource is refused, naming the missing fields, and the response now names anything a rollback intentionally leaves untouched (category-inherited fields, framework fields the version never recorded, script tools under
tools/{id}/). - A rollback that gets refused no longer writes a version row. Validation now happens before the file write, so a persistence failure leaves nothing on disk.
- A gate or framework edit no longer writes a spurious extra version row on every save.
cpm rollbackno longer destroys resource fields on gates. It previously wrote the recorded snapshot as the entire file, deletingpass_criteria,retry_config,activation, andguidanceFile; the snapshot is now merged over what's on disk, andcpm rollback styleis refused (styles have no version history).resource_manager framework update description:"…"now actually writes the new description, instead of accepting and reporting the value while silently discarding it.- A script tool's declared interpreter fallbacks (e.g.
python3thenpython,bashthensh) now work, instead of always trying only the first name in the list and failing with a bareENOENTon hosts that only have the second. - Script output is now capped, and exceeding the cap fails the render instead of silently truncating it. The default limit is 50,000 characters, configurable via a script tool's
maxOutputChars. - A prompt can now document
{{script:...}}syntax inside a{% raw %}block without triggering it. Previously the script-reference scan ran before Nunjucks and didn't understand raw blocks, so documenting the syntax executed it (or broke the render if the reference wasn't well-formed). - The
{{script:...}}examples in the template syntax reference documentation now actually work. They were written with a space ({{ script:word_count }}) that the resolver, which matches{{script:literally, never recognized. - On STDIO, a server whose default
config.jsoncannot be loaded no longer writes to stdout ahead of its JSON-RPC frames. It used to printUsing default configurationto stdout, which is the protocol channel, so a client parsing stdout read a line that was not a frame. That line now goes to stderr beside the load error it explains, and stdout carries only protocol frames. - Claude Code now receives the write receipt, preview notice, and other readable summaries it was silently dropping. On 2.1.272, a tool result carrying both
contenttext andstructuredContentreaches the model asstructuredContentalone, soresource_manager'svalidate,create,preview,update, andinspectresults — draft-valid notices, write receipts, "nothing written" previews, saved-version confirmations, and next-step guidance — never reached the model when they carried structured data too. Every such result now mirrors its readable text intostructuredContent.message. A result withcontenttext only is unaffected. config.json's schema now rejects an undeclared key in every section, not just at the top level. A typo one level down —gates.enabld,server.prot— previously passed schema validation, and the server silently used the default for the setting you thought you'd set. Keyscpm config setcan already write are declared in the schema, some marked deprecated, so a config the CLI wrote still validates.- A workspace script tool and a workspace style are now found. Both were resolved from the server's own package directory regardless of
MCP_WORKSPACE, so a script under<workspace>/resources/scripts/{id}/— the tierdocs/guides/script-tools.mddocuments — answered "not found", and a style under<workspace>/resources/styles/{id}/— wherecpm create stylewrites — matched nothing and rendered no guidance, silently. Both now resolve the way prompts, gates and frameworks already do; a workspace style overlays the bundled styles rather than replacing them, so#analyticaland the rest keep working alongside your own. - Editing a style's
guidance.md, a framework's guidance, or a workspace script while the server is running now reaches the next call, instead of needing a restart. Style hot reload was built but never wired in at all, so#style_idkept serving the guidance loaded at startup; framework hot reload was wired but watched only the bundled tree, so a workspace framework's edits were silently dropped once a workspace was configured; and the workspace-tier scripts folder (resources/scripts/, loadable since the previous release) was never watched, so an edited script kept running its old version until something else happened to trigger a reload. All three now watch every directory their loader reads, workspace and bundled alike, matching how gates already did. - The diff shown after a category update, and the one a category rollback preview shows, described a file that was never written. Both were rendered from the recorded fields rather than from what the write puts on disk, so they missed everything a write to
category.yamlreplaces — a hand-authored comment, the author's key order, the way a long description is wrapped — and the preview's diff could not even be applied to the file it named. Both now read the writer's own plan, as the prompt, gate and framework diffs already did, so they namecategory.yamland exactly the lines that change in it. The prompts the category holds are never named, because a category write never touches them. system_controlnow advertises which twelve valuesactionaccepts, andresource_managerdocuments the five parameters it accepted without ever listing them.actionwas published as a plain string, so a client had to mine the twelve valid values out of prose and learned an invalid one only from the error it came back with; it is now an enum the client can read and check against before it calls. Onresource_manager,chain_step_operation,chain_step_index,chain_step_data,chain_step_orderandsubjectwere live parameters missing from the tool's own documentation,preview_actionwas documented as a free string when only three values are accepted, andactionwas documented withoutpreview. Every parameter of all three tools is now described with the type it actually takes, and a test holds them to it.- The tool descriptions a client reads now list every action, whichever framework is active. A framework's tool text used to replace the tool's own description, so under CAGEERF, the default framework,
resource_managernamed 8 of the 15 actions it accepts and said nothing about validating a draft before creating it, and under FOCUS, LIQUESCENT or VERIFY it named none. The same replacement droppedsystem_control's action list andprompt_engine's syntax reference, and several bundled frameworks still taught the deprecated@Frameworkform. Each tool's description now comes first, with the active framework's guidance appended under anACTIVE FRAMEWORK [TYPE]:heading. A custom framework'stoolDescriptionstext is appended the same way, so write only what the framework adds. - Chain runs and execution history are now recorded under the launch workspace on an ordinary launch, instead of only when
--identity-modewas passed. The pipeline read identity settings only after that flag set an explicitidentity.mode; without it, a run's continuity scope fell back to the shareddefaultbucket even though the server had already derived a workspace id from the project directory. Framework and gate state already scoped to that derived id — chain runs and execution history now match them. - A verification loop's state now lives in your runtime directory, so a plugin update no longer erases it and the Stop hook can find it.
:: verify:"…" loop:truerecords its progress inverify-state.db, which the server wrote into its own package directory whilestate.dbwent toMCP_RUNTIME_ROOT. Under the Claude Code plugin that meant the install directory, which every plugin update replaces, and the Stop hook looked for the file in a place that disagreed with the server as soon as a workspaceconfig.jsonexisted. The server now writes it besidestate.db—${CLAUDE_PLUGIN_DATA}/runtime-state/for plugin users,$MCP_RUNTIME_ROOT/runtime-state/otherwise — and the hook reads it from there. - A change that lands while a hot reload is already running is now served, instead of waiting for the next unrelated change. The server dropped any hot-reload event that arrived during a running prompt reload, so a prompt edit, or a folder created after startup, that landed inside that window was logged and discarded, and the previous content kept being served until something else triggered a reload. Reloads still run one at a time; every event that arrives during one now produces exactly one more reload when it finishes, which reads the disk as it then is.
- Five allowlisted telemetry attributes now report real values instead of nothing.
cpm.prompt.id,cpm.operator.types,cpm.chain.total_steps,cpm.gates.applied_countandcpm.gates.temporary_countwere declared on the root span's attribute type and documented, but no code path ever assigned them, so every trace reported them absent. Each now reads a value already computed elsewhere in the same request (the parsed command's prompt id and detected operator types, the chain session's total step count, and the planner's applied-gate list and temporary-gate count). A new check fails the build if a future allowlisted attribute is added without an emitter. cpm historyandcpm rollbacknow find the version history a running server wrote, when run with-w <workspace>from a different directory.cpmguesses the tenantversion_historyrows are scoped under fromCLAUDE_PROJECT_DIR, or its own working directory — a guess that only agrees with the server's own resolution by accident once the two processes stop sharing a launch directory or environment, which is the common shape for a background daemon serving several per-project workspaces.cpm historyanswered with an empty history andcpm rollbackrefused withVersion 1 not found, both against history that exists. The guess is now corrected against whatstate.dbactually holds whenever it is wrong and unambiguous:cpm history,cpm compare,cpm rollback, andcpm delete(the operations that only ever act on rows that already exist) read the tenant that really wrote them instead of the one this process happened to guess.
- adaptive-chain-runtime residuals sweep (ba03839)
- chains: a chain prompt runs its steps in every command source (#401) (927c209)
- chains: a chain whose first step is delegated starts (#365) (5f122f3)
- chains: a re-opened unknown can rewrite its run again and cpm rollback can skip validation (#422) (6b03ea0)
- chains: a re-opened unknown gets a new remainder and a pull keeps prompt.yaml intact (#423) (1fb4df6)
- chains: a resume addresses the node ids its run declared and a claim keeps its gates (#408) (8d5b0a5)
- chains: a resume that carries no answer never moves the chain (#393) (fdbbeaa)
- chains: a run keeps its framework decision, its saved state and a true re-run line (#413) (379ca04)
- chains: a run's clears, gate ids, framework and completion reply hold what the run declared (#414) (388bf9d)
- chains: a run's leftovers, its interrupt and its JSON form match what the run did (#419) (58ec70a)
- chains: a run's records, contributed gates and claims name what the run declared (#410) (0900e73)
- chains: a run's step results, written gates, claims and interrupts hold what the run declared (#418) (6d442cb)
- chains: a workflow is validated and remaindered as the chain-prompt steps it runs (#402) (9f83336)
- chains: an evicted run, a re-opened unknown and a droppable gate leave what they should (#420) (46b4568)
- chains: every review read names its node and a failing last-step verification holds the run (#386) (6289aae)
- chains: one gate review per node and a run completes whichever stage ends the call (#384) (b88a371)
- chains: sync cpm rollback onto go-forward version numbering (2e15f0c)
- chains: two servers on one database number their runs in one chain-id space (#409) (6dad55f)
- ci: a plan footer asserts progress, not same-PR retirement (#263) (12d96ed)
- ci: run integration at pre-push, and make git-backed checks hook-safe (639fe26)
- cli: cpm finds the version history the server wrote for that workspace (#341) (81b88d7)
- cli: every write and import holds a prompt to the rule the server loads it by (#421) (48c9099)
- close both conformance-gate blockers from the test roadmap (ee6c5e3)
- config: architecture checks enforce the boundaries they name (#316) (bbd5f69)
- config: the server reports its package version, not 1.0.0 (#349) (8faee8f)
- contracts: array element types match what each schema accepts (#328) (79b9fa3)
- contracts: every tool's input schema matches its contract (#318) (37fafdd)
- contracts: give both suggestion matchers one scoring contract and a word floor (b5b1904)
- deps: declare @jest/globals so the knip ratchet measures real debt (3d2572a)
- docs: correct four dead commands in CONTRIBUTING and gate the class (cd7d18b)
- docs: correct four dead commands in CONTRIBUTING and gate the class (#233) (91f5058)
- docs: file the plan pair under the frontmatter status vocabulary (ecb0c2d)
- execution: a step a run adds mid-flight behaves as a planned step at every surface (#412) (ce5666f)
- execution: advisory names the real delegated step, not the synthetic review step (S10) (19a537b)
- execution: commit the declared-sections closure efe9a605 depends on (4fa54e5)
- execution: honor execution.confirm for inline script references (d6cd6b7)
- frameworks: a framework created or kept in the workspace can be the active framework (#308) (1534763)
- frameworks: a framework update keeps its version and unchanged files (#330) (67adc5d)
- frameworks: a framework's system prompt has one source (#360) (d20280c)
- frameworks: framework companion files keep the names framework.yaml declares (#295) (e19dcba)
- frameworks: framework tool text adds to each tool's description (#323) (9a7bfb1)
- frameworks: re-register on update and reload instead of claiming it (G2, G4) (d5eaa6a)
- frameworks: the @id override recognizes frameworks created after startup (#305) (6fc40cb)
- gates: a chain step's own declarations bind that step (#354) (deb51bb)
- gates: a failing resume can gate the step it retries (#425) (c813fe1)
- gates: a gate review's record and events tell the truth about order (#355) (e18346d)
- gates: a gate review's record and reply say what happened (#369) (c3c49ea)
- gates: a gate sent with a FAIL joins the open review (#427) (88f3fbb)
- gates: a gate with no activation block is opt-in; export hooks are opt-in per prompt (#286) (b3e8df3)
- gates: a gate's declared enforcement mode decides what its fail does (#372) (8910a63)
- gates: a guidance-only gate edit leaves gate.yaml alone (#294) (9e87964)
- gates: a judge-routed gate review hands the client its judge prompt (#364) (0753f1c)
- gates: a run claimed by another server keeps its temporary gates (#407) (9f9ea9b)
- gates: a run resolves a gate id it declared to the gate it registered (#404) (327dcbc)
- gates: a run's added steps, chain-defined gates and saves behave as planned steps do (#411) (a4880c9)
- gates: a run's temporary gates live exactly as long as the run (#403) (936611f)
- gates: a sent gate keeps its shape and a built-in gate id is refused from every surface (#416) (76f9dad)
- gates: a structural failure joins the gate's open review (#371) (962203f)
- gates: add Stage 1.5 existence gate to GateSetResolver (P6-F14) (084b662)
- gates: an excluded framework gate stays excluded (#350) (3c7a858)
- gates: every step of a chain runs its inline verification (#398) (e9a1763)
- gates: exhausted reviews and delegated briefs say what the server accepts (#399) (7b066e5)
- gates: give ReACT's compliance guidance the heading other frameworks get (#428) (4cf5072)
- gates: guidance.md stays byte-identical when a gate change leaves guidance alone (#284) (340b341)
- gates: join a gate sent without a verdict to its step's open review (#435) (ebe8936)
- gates: keep a gate a step's own prompt supplies on that step (#444) (8e94735)
- gates: keep a gate written on a chain step on that step (#442) (f6946c4)
- gates: let a chain advance while the gate system is switched off (#437) (00dedaf)
- gates: preserve gate.yaml fields the caller did not supply on update (dc1c5f7)
- gates: refuse a gate action by name when nothing is pending (#429) (23e11f9)
- gates: resolve script_tool_id to a tool, and let raw blocks hold (c6f111f)
- gates: review a chain prompt's own gates on its final step (#445) (016e16a)
- gates: stop the default framework gate from undoing every resolver veto (794f460)
- gates: the documentation gates reach the documentation prompts (#313) (9cdc3ad)
- gates: the review path does what its replies promise (#395) (04b1139)
- hooks: a worker's hooks never act on its parent's chain (#397) (5c6027f)
- hooks: accept every contract resolution verb while a gate is pending (60e161e)
- hooks: carry the >> resolution line on the channel clients actually read (488e5bb)
- hooks: clear session rows when their run resolves (6c1f923)
- hooks: delete the blocking subagent-gate-enforce hook (S3) (fcdfe7d)
- hooks: resolve Python across platforms (#241) (dd70238)
- hooks: route >> resolution through the channel clients read (9d8c92d)
- hooks: scope chain recovery to the owning conversation and read the server's real state.db (e53be94)
- hooks: withhold systemMessage on SDK hosts and echo it as assistant text (0bc86c1)
- mcp-tools: a chain step cannot scaffold outside its parent prompt's folder (#289) (53bed1f)
- mcp-tools: a framework error names the folder the framework resolves from (#293) (a14491c)
- mcp-tools: a gate or framework diff names the files its write changes (#290) (fed35e2)
- mcp-tools: a misspelled key, a stray parameter and a stale schema are refused by name (#367) (6f6c628)
- mcp-tools: a new prompt, gate or framework starts at version 1 (#296) (61b43c3)
- mcp-tools: a parameter the resource type ignores is refused (#337) (b98822e)
- mcp-tools: a prompt preview shows the diff that lands on disk (#285) (975093b)
- mcp-tools: a refused resume names its unmoved run (#424) (6b6fff7)
- mcp-tools: a resource_manager key that nothing reads is refused (#342) (bdca52a)
- mcp-tools: a tool reply reports only what the server actually did (#310) (b6ac608)
- mcp-tools: apply the database port to the framework tool built after it (G5) (63ee8ed)
- mcp-tools: declare every parameter a resource or system handler reads (#440) (4311d68)
- mcp-tools: deny destructive resource actions once and preview them (d57a9c5)
- mcp-tools: every field a tool handler reads reaches it over MCP (#299) (4d323d6)
- mcp-tools: every prompt resource type now loads from the workspace (#298) (5a5c50e)
- mcp-tools: every tool refuses an argument key it does not declare (#344) (b87ddbb)
- mcp-tools: fix resource_manager prompt write-path settability defects (5c3198b)
- mcp-tools: register a created gate so the process that made it can use it (b7102dd)
- mcp-tools: register skills_sync in the action inventory and stamp plan tags (860aeca)
- mcp-tools: the model in Claude Code sees every tool result's readable text (#283) (868f36e)
- mcp-tools: updating a single-file prompt leaves exactly one definition of it (#292) (648e88e)
- parsers: a chain prompt named with an operator runs its declared steps (#400) (427899f)
- parsers: attribute inline gate tokens to their chain segment (S9) (e693164)
- parsers: resolve empty symbolic args and prompt-scoped chain tools (2f84df6)
- pipeline: a phase-guard review names the step it graded (#272) (6a1ee22)
- pipeline: a resume's framework prompt follows the step it renders (#426) (ea62d39)
- pipeline: a shell verification holds its step and the hooks see every held run (#396) (f4e3db2)
- pipeline: every command source refuses a gate target it does not declare (#405) (ff936b0)
- pipeline: gates-off prompts declare no sections and a subagent passes the delegation hook (#394) (07567a6)
- pipeline: grade an answer before the run advances (#430) (f6ffe92)
- pipeline: grade the next step's answer sent with a structural-review pass (#390) (59d857b)
- pipeline: re-evaluate pending gate review after same-request advance (P5-F6) (d5e14d7)
- pipeline: stage 19 grades a step against that step's own review (#388) (716a5eb)
- prompts: every README install path ends in a command a fresh install can run (#276) (c68205b)
- prompts: review implementation_plan against plan-quality only (#446) (0263d00)
- prompts: strategic_worker export excludes its auto-attaching gates (#282) (d1f3f71)
- resources: close the false-claim shape in the paths the first pass missed (b9c1b6e)
- resources: compile the else-branch for export and say when a placeholder survives (ecc8537)
- resources: editing a resource changes only what the edit names (#339) (02392db)
- resources: emit skill artifacts that state only what clients can honour (9b0fcfc)
- resources: every prompts-folder walk follows the server's rules (#324) (01cbb36)
- resources: make the framework draft validator check the shape it requires (G1, G3) (295271a)
- resources: refuse skill export through a symlink that leaves the client's base dir (#256) (df2e9f1)
- resources: resolve export gate activation through the engine, not a local copy (519ec10)
- resources: skills_sync pull and clone report the files they wrote (#332) (637fbb5)
- resources: skills-sync diff works without a manifest and export names what it removes (#309) (05b6115)
- resources: tell a tool that failed to load from a tool that was deleted (def97ce)
- resources: the resource surface keeps one copy of each rule it enforces (#319) (83196fc)
- resources: the server reports which copy of a resource actually serves (#315) (f2e07a4)
- runtime: a change that lands during a hot reload is served (#331) (0a06800)
- runtime: a failure that was silently dropped now reaches the caller (#311) (6bc3622)
- runtime: a prompt created through the Claude Code plugin survives a plugin update (#281) (aed06d0)
- runtime: a prompt hot reload refreshes which prompts are exported as skills (#304) (5163dff)
- runtime: a prompt or framework edit reaches the running server (#314) (24977b9)
- runtime: a server runs only on the settings it was given, including after a restart (#279) (d2af7a8)
- runtime: a STDIO server writes only its real warnings to stderr (#307) (e50095a)
- runtime: a verify loop that cannot save its state says so (#333) (8b99db8)
- runtime: a workspace header is served as its own tenant (#368) (eb0f460)
- runtime: a workspace header scopes each streamable http request (#359) (2aa2699)
- runtime: align prompt execution contracts (#432) (497c5ba)
- runtime: every run, server or check, uses only the configuration it was given (#278) (c141a04)
- runtime: give five two-answer questions in chains, loaders and scopes one answer (#443) (564970d)
- runtime: hot reload serves folders created after startup (#329) (53cbcbd)
- runtime: hot reload watches every folder a style, framework or script loads from (#302) (af4ff5d)
- runtime: load the same roots on reload that startup loads (#258) (5ba103c)
- runtime: read skills-sync deregistrations from the registrations key (d5948b4)
- runtime: record what each edit produced and refuse incomplete snapshots (39ae9dd)
- runtime: runtime state is written under the runtime root (#327) (a844497)
- runtime: stop cpm rollback from deleting the fields a snapshot does not carry (1fc8f42)
- scripts: add contracts self-test to generate-contracts.ts (29c76e9)
- scripts: regenerating a ratchet baseline cannot raise a ceiling (#321) (998c12b)
- scripts: removing lint debt requires lowering the ratchet ceiling (#326) (dd105ea)
- server: a reply reports the state the server actually reached (#336) (ed73dcd)
- server: a toggle says whether it persists and a bundled gate runs from any directory (#357) (f579625)
- server: execute the security review — bound every path from authored content to code (#249) (7826298)
- server: harden the HTTP transport to the MCP spec (#245) (15d27a5)
- server: one implementation answers each question two used to answer differently (#358) (378dcba)
- server: skills sync over MCP follows the workspace a server was started with (#303) (d59bffa)
- server: skills sync reads resources from the workspace a server was given (#301) (267bc61)
- tests: a test run leaves no trace outside its temp directories (#320) (7a24676)
- tests: cover the two new tool parameters and align gates with merged reality (d156038)
- tests: make the path-resolver test hermetic against the operator's environment (#251) (3186883)
- tests: the flaky e2e suite waits for its server before deleting its workspace (#260) (d9726fd)
Security
script_toolgate criteria no longer execute through a shell. The declared tool id is now resolved through the same script-tool registry the inline{{script:}}path uses, instead of being passed to the process helper as a raw command string. A check that can't run — unknown id, no registry wired, or a tool requiring confirmation a gate has no way to grant — now fails closed instead of reporting a false pass.{{script:id}}inline references now honor a tool'sconfirm: truesetting. Previously an inline reference ran the tool unconditionally even when it required approval; approve one run by naming the tool astool:<id>in the invocation arguments. Prompt authors: a template referencing aconfirm: truetool now aborts the render instead of running it — setconfirm: falseon tools that need no approval.- Script output is no longer evaluated as template syntax. Output is now inserted as literal text, closing a path where a script emitting
{{,{%, or{#could read other arguments, inject structure into the prompt, or hang the render.
Documentation
docs/architecture/overview.mdreads as three views — building blocks, runtime, crosscutting concepts — and no longer restates the module graph by hand. Two hand-drawn diagrams that said the pipeline had 23 stages (it has 21) andsystem_controlhad 11 handlers (it has 12) are gone; each is replaced by a sentence pointing at the generated catalog. The Pipeline Behavior table stops claiming a failed stage is recovered (it is rethrown) and that stages never skip (a stage that sets the response ends the run).- PR bodies and squash merges now show the actual change instead of every commit message. The PR template gains a required
## Demonstrationsection forfeat/fix/perf/refactorchanges,npm run pr:bodypre-fills it from the branch, andcommitlintwarns on titles that name session activity instead of the merged outcome. - The PR body is now a two-register document and its gate blocks merges. Reader voice above the fold; a collapsed
<details>appendix carries the session archive (deviations, captured transcripts) intomain's history via the squash body.PR Conventionsis a required check -- bot PRs exempt -- and aPlan:footer refuses to merge until the named plan is finalized. - One command now runs every PR gate before you open a PR:
npm run pr:check. It runs all four gating steps thePR Conventionsworkflow runs -- the two positive controls, the body check, andcommitlinton the title -- so a green local run predicts a green check. Previously the documented local route covered only the body, and the body checker does not read the title beyond its type, so a title that CI would reject passed every instruction a contributor was given. A parity test reads the workflow file and fails when the two sets of steps diverge.npm run pr:bodymoves fromserver/to the repo root, wherepr:checkandcommitlintalready live, so the whole flow runs in one directory. MCP_RUNTIME_ROOTis now documented in--helpand the handbook. It independently pinsruntime-state/and relativelogs/regardless ofMCP_WORKSPACE, so you can pointMCP_WORKSPACEat a personal resource library while keepingstate.dband logs elsewhere;--helpalso now notes thatMCP_RESOURCES_PATHalone does not enable resource overlays.
- docs: add frontmatter to the F17 implementation notes (8adf66d)
- docs: archive the script-tools notes its plan retirement left behind (e01b403)
- docs: close out adaptive-chain-runtime residuals sweep plan (dbaa450)
- docs: close the prompt-surface plan's stale markers and growth rows (42f30fc)
- docs: delegation plan records the live drive of a detached chain (#374) (1161853)
- docs: document uniform version semantics and the tool that stops a run (03aa4e7)
- docs: draft the cross-client chain handoff plan (interview 2A) with T0 measurements (5af7e88)
- docs: finish the plan sweep and repoint every technical-debt reference (2aa7592)
- docs: flip both re-measurement falsifiers in the test roadmap (3d47a8d)
- docs: four P4 rows rejoin the table their gates read (#274) (3b39cae)
- docs: make PR and commit boundaries explicit, and enforce the template (#250) (9bf1d09)
- docs: open the adoption-conversion plan with a weekly traffic ledger (6d99b7c)
- docs: open the contract-layer ledger and the chain-integrity plan (fe0e21f)
- docs: phase 4 carries a dispatch table and two corrected premises (#269) (ac398cb)
- docs: plan the framework resource lifecycle fixes with the operator's rulings (e97d82e)
- docs: plan the primitive rework of reviews, gate loading and contract binding (#375) (fb36959)
- docs: primitive-rework plan records row 3.14 and cuts row 3.15 (#389) (6bec85d)
- docs: primitive-rework plan records row 3.15 and closes Tier 3 (#391) (a277c21)
- docs: primitive-rework plan records the module evolution analysis and its rows (#376) (0daafa6)
- docs: primitive-rework plan records Tier 3 slice 2 and its eleven findings (#383) (c67708e)
- docs: primitive-rework plan records Tier 3 slice 3 and cuts its six rows (#385) (d8e8577)
- docs: primitive-rework plan records Tier 3 slice 4 and cuts its five rows (#387) (beeb9f6)
- docs: primitive-rework plan records tiers 1 to 3 and retires the delegation plan (#381) (d208362)
- docs: queue residuals plan for retirement as reference (2ba8771)
- docs: record F15 — skills-sync.yaml was tracked and self-contradictory (dba185b)
- docs: record settability-parity row status and six owner decisions (b1d5d23)
- docs: record the F17 root cause and answer two of its open questions (0130fa6)
- docs: record the owner rulings that scope the remaining skills-sync rows (85d2ff3)
- docs: record the schema-bump exclusion zone in the framework lifecycle plan (7500eb8)
- docs: record the skills-sync export fidelity plan and its rulings (f999b04)
- docs: resource-surface plan closes the 2026-09-21 dispatch (#363) (acaf76c)
- docs: resource-surface plan closes the evening dispatch (#370) (1cf0dc7)
- docs: resource-surface plan closes the night dispatch (#373) (153e99d)
- docs: restore subagent-delegation-contract plan to active (30b941d)
- docs: retire delegation-contract plan and test roadmap as reference (1e485dc)
- docs: retire fifteen completed plans to reference and archive (d17dfe1)
- docs: retire the acquisition-recovery plan with its traffic scorecard (87f566f)
- docs: retire the resource-versioning plan to reference, promote F17 out (9051c45)
- docs: retire the semantic module governance plan to reference (#243) (697148f)
- docs: retire the skills-sync export-fidelity plan to reference (fb0c70b)
- docs: stamp OQ-F1 resolved so a retired plan stops reading as open (f8a6bfe)
- docs: stamp the 2A plan's resolved sections with the linter's marker (6a6372d)
- docs: stamp the rulings table so no open question reads as unresolved (7cc94b5)
- docs: stamp two implementation notes done so retirement can proceed (b78ec47)
- docs: tell every session to work in a worktree (#247) (0b5ab85)
- docs: the adoption implementation notes carry plan frontmatter (#267) (c951834)
- docs: the contract-layer ledger records the chain-integrity landing and the findings handoff (f76f621)
- docs: the environment-gaps plan holds 5.0.0 with every downstream fix live (#280) (e52e8eb)
- docs: the unreleased changelog speaks to the consumer (#261) (f455c8b)
- frameworks: retire the Stage 09b name and close the phase-guard plan (655642d)
- prompts: a slice's plan writeback lands in that slice's PR (#392) (d1b9c6e)
- queue adaptive-chain-runtime residuals plan for retirement (e7fb1f4)
- resource-surface plan records the PR batch that landed (#352) (29b9dd6)
- resource-surface plan states what is on main (#348) (7669aa4)
- resources: apply the do-or-kill row lifecycle to the delegation plan (dc97413)
- resources: convert filed-not-fixed findings into owned rows (D.1/D.2) (32bd77c)
- resources: correct the framework lifecycle plan's G2 before dispatch (07c6364)
- resources: flip the integration claim from inferred to observed (913f00b)
- resources: record the framework lifecycle live drive and close every tier row (088d01d)
- resources: record the honesty audit that found this fix incomplete (0fd0f21)
- resources: record the run cancelled by its own documentation (6a476c8)
- resources: record what today's green run did and did not observe (ca7839c)
- resources: retire a deviation row that outlived what it described (0cad516)
- resources: retire the framework lifecycle plan and re-home what outlives it (bf3c9ac)
- resources: retire the sub-agent delegation contract plan (#240) (f2e685f)
- restore subagent-delegation-contract plan to active (8dc4d8a)
- retire delegation-contract plan and test roadmap as reference (f2c0f12)
- scripts: commit the script-tools verification deviation log (5f6a0f1)
- scripts: rule Q1-Q3 and record the template-injection finding (acfa935)
⚠ BREAKING CHANGES
resource_managerno longer acceptsexecution_hinton promptcreate/validate, orpersiston frameworkswitch. Neither was read by anything: a prompt's execution type comes from its shape (chain steps or not), and a framework switch is already saved in the server's state store whether or notpersistwas sent. Both are now refused by name like any undeclared key; drop them from the call.system_controlno longer acceptsinclude_history. It was declared forstatusandanalytics:statusnever read it, andanalyticsused it to show a "Performance Trends" list that only a hot reload of tool descriptions ever filled — one startup memory reading, or nothing. Sending it is now refused by name, the way any key the contract does not declare is. Drop it from the call; the rest of the report is unchanged.- A version row written by
cpmnow sayscpm, notresource_manager.cpm createandcpm togglereused the server's row descriptions, so every row they wrote readCreated via resource_manager/Update via resource_manager— the wrong surface, in the only prosecpm historyorresource_manager historyshows about what produced a row, and the two surfaces restore differently. Rows written bycpmfrom now on readCreated via cpm/Update via cpm; rows the server writes are unchanged, and rows already inversion_historyare left exactly as they are. A script matching on the old text for acpm-written row needs the new wording. resource_manager's promptvalidateandcreatereplies no longer carry awarningsarray. Chain-step integrity was its only producer, and that is now a refusal — the offending steps arrive inerrorsand the write does not happen — so the key had been an empty array on every reply, of every outcome, whether the call succeeded or failed. A client branching on it was branching on a constant. Readerrorsinstead; apreviewreply still carrieswarnings, which reports pre-existing template defects and is not always empty.resource_managerrefuses a key its contract does not declare, instead of accepting it and reporting success. The tool's input schema passes unknown keys through, so a misspelled or invented parameter —chain_stepforchain_steps, say — arrived intact, was read by nobody, and the call answered "updated successfully" having changed nothing. It is now refused before dispatch, naming the key:'chain_step' is not a parameter of resource_manager. This is the undeclared half of the class the per-resource-type refusal closed for declared parameters; both run ahead of any write or version snapshot. Scripts sending a key this tool does not document now fail instead of silently doing nothing — remove it, or correct the spelling.prompt_engineandsystem_controlgained the same refusal; see the next entry.prompt_engineandsystem_controlrefuse a key their contract does not declare, instead of dropping it and reporting success. Both schemas stripped an unknown key before the handler ran, soprompt_engine(command:">>x", force_restrt:true)returned a normal prompt list andsystem_control(action:"status", previw:true)a normal status overview —isError:falsein each case, with the key read by nobody. That is a security defect, not untidiness: a caller, or a model following a prompt-injected instruction, that sends a safety flag under a slightly wrong name got a success reply while the server did the unguarded thing — adry_run/preview/confirmtypo performed the real write. All three tools now share one refusal, which reads the declared key set from the tool's published contract, names every undeclared key in one message, and suggests the nearest declared spelling (enforcementMode→enforcement_mode). A gate parameter sent toprompt_enginewhile the gate system is disabled gets its own message — it is declared, just not advertised in that state — rather than "not a parameter". A script tool'sauto_execute.paramsare checked the same way before the mutation runs, and a refusal there becomesprompt_engine's error naming the script and the key, where it used to be swallowed and reported as success. Correct the spelling, or drop the key. The publishedinputSchemastill allows additional properties, deliberately, so the server can name the key rather than a client rejecting it locally with a message naming neither the tool nor the fix.resource_managerrefuses a parameter the resource type does not read, instead of ignoring it and reporting success.updatewithresource_type:"framework"(or"gate", or"category") andunset:["description"]answered "updated successfully", saved a new version, and left the file byte-identical —unsetis a prompt parameter and nothing else reads it. Removing a field from a gate, framework or category is still not implemented; the call now says so by name and names the type that does implement it, before any write or version snapshot. The same holds for every other type-specific parameter the tool publishes — around sixty of them were accepted and dropped for the three types that do not read them. A call that sent one of these parameters used to succeed while ignoring it; it now fails, naming the parameter and the resource types that read it. Scripts must drop the parameter for that resource type.- A key a NESTED object does not declare is now refused instead of dropped. The top-level refusal above closed one level; zod's default dropped an unknown key one level down, so a misspelling inside
gate_verdict,observations[], a prompt'sarguments[], an inline gate, a framework's phase/step objects, orsystem_control'sconfigwas accepted, read by nobody, and answered success. The sharpest case isgate_verdict: it is the safety-review submission, and{pased: true}lost the key AND leftpassedabsent — an absent boolean is the FAIL reading, so a misspelling could invert a gate review silently. Every object reachable from a tool's parameters now refuses an unknown key, naming the path it sits at, and the publishedinputSchemamarks those objectsadditionalProperties: falseso a client can catch the typo before sending. Two objects stay open on purpose and say why: each tool's own top-level parameters (so the key can arrive and be named with a suggestion) andchain_steps/chain_step_data(a chain step is an opaque object). Callers sending an extra key inside any of these objects must drop it or correct the spelling. - A script tool must write JSON to stdout, and output that does not parse is refused by name instead of being wrapped as success. Unparseable stdout arrived as
{"output": "<the text>"}and the run reported success, so a script that printed a traceback, or a debug line before its payload, looked like a tool that had returned a value while every{{script:id.field}}rendered empty with nothing anywhere saying why. Empty stdout did the same with an empty string. Both now fail the run, naming the tool and showing what it printed — send diagnostics to stderr instead. A tool may also declare the shape of what it returns, in anoutput-schema.jsonbeside itsschema.json(or the pathoutputSchemaFile:names); output missing a required field or carrying a declared field of the wrong type fails the same way, and is checked by the same predicate an input is checked by. Declaring a shape stays optional; returning JSON does not. A script that printed plain text and was treated as succeeding now fails, which is the point — it was never producing a value a template or anauto_executeblock could read. - A script tool's
auto_executethatresource_managerrefuses now FAILS theprompt_enginerun, instead of being reported as success. The tool's error reply was written into the run's script results and logged, and nothing downstream read it, so a refused or failed mutation rendered the prompt normally and answeredisError: false. Measured by drive: a parameter the resource type does not own, a delete of a target that does not exist, and a schema-invalid value each reported success. The run now fails, naming the script tool and carryingresource_manager's own message. Relatedly, a script tool's emitted parameters are now validated against the sameresource_managerschema an MCP client's call is validated against — the in-process path previously ran none, so a script could send a shape no client could. A script emitting parameters the tool rejects must be corrected; it can no longer fail quietly. cpm config setandcpm config resetno longer leave aconfig.json[c].backup.<timestamp>file, and--jsonno longer reportsbackupPath. Nothing ever read those files back:cpmnever had aconfig restore, andsystem_control config restorewas retired in #312 — so a workspace accumulated one unreadable copy per invocation, with no equality check and no retention bound. Two identicalcpm config set gates.enabled falsecalls left two files. Every config write is now a version carrying the file's exact bytes, comments included, andcpm config history/cpm config rollback <version> [--preview]are what read them back. The same applies tocpm enable/cpm disable, which write config through the same path. Backup files already on your disk are left untouched — move or delete them yourself. Scripts readingbackupPathshould readrecordedandversioninstead.cpm rename --jsonandcpm move --jsonreportoldPath/newPathinstead ofoldDir/newDir. A prompt can now be a single file, so the moved thing is a path, not always a directory. Scripts reading the old keys need the new names.- The
strategicImplementprompt is nowstrategic_implement. Type>>strategic_implementwherever you typed>>strategicImplement, in commands, chains and templates. The old spelling is no longer recognised and there is no alias: it answers "Unknown prompt" and suggests the new name. Every other prompt id in the >> command language is already snake_case, and this was the last bundled prompt that was not. If you export skills, the exported skill folder is renamed too, tostrategic_implement/. resource_manager's gate parametergate_typeis renamed totype, andgate_typenow writes the gate.yaml key of the same name. The parameter published asgate_typenever wrotegate_type— it wrote the gate.yaml keytype(validation|guidance), which left the realgate_typekey (framework|category|custom, the classification the loader filters framework gates on) with no parameter at all, because its name was taken. Every gate parameter is now named for the key it writes: send the validation/guidance value astype, and the classification asgate_type. A client still sendinggate_type: "validation"is rejected by the schema rather than silently reinterpreted. No alias was added — one would ship a parameter already scheduled for deletion.resource_manager'sformatparameter is removed. It was declared in the schema and the contract and forwarded by the router, and read by nothing, soformat: "json"returned the same markdown asformat: "text". Measured with a positive control: the identical probe againstdetailfinds two real read sites. Callers passing it were already getting the default output and now get a schema rejection instead of a silent no-op.prompt_engine'sformatis a different parameter and is unchanged.resource_manager'sresource_typegainscategory. Adding a member widens the published union of the tool surface, which this repository's Public API Contract prices as breaking.session cancelmoved fromsystem_controltoprompt_engine. Callprompt_engine(chain_id:"chain-x#1", cancel:true)instead ofsystem_control(action:"session", operation:"cancel"), which now refuses and names the replacement;list,inspect, andclearremain onsystem_control.resource_managergains asource_workspaceparameter for cross-workspace history. It is honored only byhistoryandcompare; every other action, includingrollback, refuses it, because a snapshot from another workspace may not exist locally and version numbers are per-workspace. The refusal is deliberate: silently scoping back to local would leave the caller believing they had restored the other workspace's version.- A gate criterion that cannot be enforced now fails to load instead of auto-passing.
shell_verifywithout ashell_command, andscript_toolwithout ascript_tool_id, now fail gate loading with a message naming the missing field. - A script tool with an unrecognized file extension is now refused instead of silently run as a shell script. Auto-detection covers
.py,.js,.mjs,.cjs,.ts,.sh, and.bash; declareruntime:explicitly (includingruntime: shell) to use any other extension. - A script tool's
envcan no longer set keys that control which interpreter it resolves to, and itsworkingDirmust stay inside the tool's own directory. Refused keys:PATH,LD_*,DYLD_*,NODE_OPTIONS,PYTHONPATH,PYTHONHOME,PYTHONSTARTUP,BASH_ENV,ENV,IFS,SHELLOPTS,BASHOPTS,PERL5LIB,PERL5OPT,RUBYLIB,RUBYOPT; ordinary variables are unaffected. The same restriction applies to a gate'sshell_env, and a newMCP_SHELL_VERIFY_ALLOWED_DIRSenvironment variable bounds a gate'sshell_working_dir. - A gate's
shell_commandmust now be an argv array; a bare string fails to load. Replaceshell_command: "npm test"withshell_command: ["npm", "test"](the shippedtest-suitegate is already migrated); the inline:: verify:"..."operator is unchanged and still takes a string. category_ship_statusis removed from the prompt mutation receipt. It always reportedtruebecause the.gitignorefile it checked no longer exists; the receipt still reportsresource_root, the path the write actually landed in.dry_runis removed fromresource_manager; previewing is nowaction: "preview"with a requiredpreview_action. Replaceaction:"update", dry_run:truewithaction:"preview", preview_action:"update", and likewise for"delete"and"rollback". A preview no longer needsconfirm: true— as a flag ondeleteit did, because the confirmation guard reads the action, so seeing what a deletion would cost meant confirming the deletion first.preview_actionsent withoutaction:"preview"is refused rather than ignored, and the response'sstructuredContentnow reportsaction:"preview"withpreview_actioninstead ofdry_run: true. Removing a parameter narrows the reachable-shape union of the MCP tool surface, which this repository's Public API Contract prices as breaking.preview_action:"update"is accepted for prompts only. Gates and frameworks support"delete"and"rollback"; asking to preview a gate or framework update is refused by name. This closes a defect rather than adding a restriction —dry_runwas forwarded to those two handlers and nothing there read it, so a preview of a gate or framework update performed the update and returned a success receipt.skills_syncmergesdry_runandpreviewintopreviewpluspreview_detail. Replacedry_run: truewithpreview: true(all four commands), and the oldpreview: trueonpullwithpreview: true, preview_detail: "diff". The two were never synonyms: one planned the run, the other rendered unified diffs and worked onpullalone. The--dry-runCLI flag becomes--preview,--preview-detail diffreplaces the old--preview, and the--jsonrun summary reportspreviewwhere it reporteddryRun.- The prompt authority HTTP route
POST /api/v1/authority/prompts/{id}/dry-runis now/preview. Request and response bodies are unchanged; only the path moved, so the removed vocabulary does not survive on the surface an operator reads most often. chain_step_operation: "replace"is removed; use"update"instead. Thereplacemember returned the step array untouched and let a suppliedchain_stepsarray overlay, which is exactly what omitting the parameter already does — so omit it to replace the whole array. In its place,"update"edits one step atchain_step_indexby overlayingchain_step_dataonto it, leaving unnamed fields alone. Removing an enum member narrows the reachable-shape union of the MCP tool surface, which this repository's Public API Contract prices as breaking.system_control configno longer servesget,set,reset, orrestore. A request naming any of them — or naming no operation at all — is now refused by name instead of silently answered with a dump of the whole configuration, which is what a malformed or unrecognized request used to fall back to. What remains:list(the whole loaded configuration),keys(the keys the packaged schema declares), andvalidate(the load-time schema result, or a per-key candidate check via the nestedconfig: { key, value, operation: "validate" }). This is not "configuration cannot be written over MCP":system_control(action:"gates"|"framework", operation:"enable"|"disable", persist:true)still records that one setting inconfig.json— what is gone is a caller naming an arbitrary key or value to write, or restoring a backup. Set a key withcpm config set <key> <value>, reset withcpm config reset --force, and read one key withcpm config get <key>for now — the MCPgetoperation is held rather than dropped, and returns once a generated config shape can resolve most of the schema's declared keys.- A path setting the server cannot use now stops it instead of starting it on something you did not ask for. This covers a config path set with
MCP_CONFIG_PATHor--configthat is missing, a directory, unreadable, or malformed JSON; a workspace set withMCP_WORKSPACEor--workspace, or a resources directory set withMCP_RESOURCES_PATH, that does not exist or is not a directory; and aconfig.jsoninside your workspace that is unreadable, a directory, malformed JSON, or not a JSON object. Each used to produce a server that started and served anyway: a named config file was ignored, a missing workspace was silently created, a missing resources directory quietly served the bundled prompts instead of yours, and a broken workspaceconfig.jsonwas replaced by built-in defaults. The server now exits non-zero before serving anything, on STDIO and Streamable HTTP, and prints to stderr which variable or flag was set, its value, the path it resolved to, what is wrong with it, and what removing it would fall back to. Fix the path, or unset the variable (or drop the flag). A workspace with noconfig.jsonstill uses the packaged one, an empty value still counts as unset, andMCP_RUNTIME_ROOTis still created when missing. Theskills-syncCLI refuses a missingMCP_RESOURCES_PATHthe same way. config.jsonnow requires"version": 5, and several settings moved or left the schema.frameworks.systemPromptFrequency,frameworks.systemPromptTarget,frameworks.gateGuidanceFrequency,frameworks.gateGuidanceTarget,frameworks.styleGuidance,frameworks.styleGuidanceFrequency, andframeworks.styleGuidanceTargetare replaced by nestedframeworks.injection.{systemPrompt,gateGuidance,styleGuidance}.{enabled,frequency,target};advanced.sessionsmoves to a rootchainSessions; andversioning.max_versions/versioning.auto_versionare renamedversioning.maxVersions/versioning.autoVersion.server.version,gates.enforcePendingVerdict, andresources.prompts.defaultRegistrationare gone outright, and are refused as unrecognized keys rather than silently ignored. Theanalysissection is not exempt from this reshape: it is removed from the 5.0 schema entirely, not merely deprecated — see the dedicatedanalysisentry below. Add"version": 5and move each setting to its new home; a file with no overrides needs only{"$schema": "./config.schema.json", "version": 5}.server.transportno longer selects the transport;--transport=streamable-httpdoes. Aconfig.jsonstill settingserver.transportto anything but"stdio"now refuses to start the server, naming the value it found and the--transportflag to pass instead; a value left at"stdio", or the key omitted entirely, draws only the ordinary schema warning. Removeserver.transportfrom your file and launch with--transport=streamable-http(or--transport=both).cpm config setandsystem_control(action:"config", operation:"keys")now work from the 5.0 key set. Every key this reshape moved or removed —server.transport,gates.enforcePendingVerdict,resources.prompts.defaultRegistration, the snake_caseversioningkeys, andadvanced.sessions— is no longer settable or listed; both surfaces now read the same generated tableconfig.schema.jsondoes, so a key that left the schema leaves both at once instead of drifting out of sync.- A framework's
toolDescriptionstext is now appended after each tool's own description instead of replacing it. Every tool now serves its own description first — its actions, parameters and syntax — followed by the active framework'stoolDescriptions.<tool>.descriptionunder anACTIVE FRAMEWORK [<type>]:heading;parameterstext forprompt_engineandsystem_controlis appended to each parameter's own description the same way. If your framework'sdescriptionwas written as a full replacement for the tool's text, it now appears after the tool's own text, so the model reads both. Rewrite it as the framework's own guidance only: say what the framework adds, and don't restate the tool's name, actions, resource types, parameters or syntax. A copy of those goes stale when the tool changes. No YAML key changed, so a framework that loaded before still loads. - The
analysisconfig section is removed fromconfig.json's 5.0 shape, not just deprecated. It is gone from theConfigtype and the generated schema. Aconfig.jsonwith noversionkey that still carriesanalysis.semanticAnalysis.llmIntegration.*is read as a 4.x file: the section is dropped on load and named in the one-time translation notice, which points to the%judgemodifier orgates.evaluation.defaultModefor model-graded gate evaluation instead. A file already declaring"version": 5that still carriesanalysisis flagged by the ordinary schema check, the same as any other key the schema no longer declares. gates.directoryis removed fromconfig.json. It was never read — the gates directory always comes from the resources path, not this key. Aconfig.jsonwith noversionkey that still sets it is read as a 4.x file: the key is dropped on load and named in the one-time translation notice. A file already declaring"version": 5that still sets it is flagged by the ordinary schema check instead.- A workspace holding both
config.jsoncandconfig.jsonnow refuses to start, and refusescpm config, instead of silently preferring one. Starting the server against a workspace set withMCP_WORKSPACE/--workspacenow checks for this ambiguity before doing anything else, and refuses naming both paths;cpm config list/get/set/validate/resetandcpm initrefuse the same way:Two config files in one directory: <path> and <path>. Keep one — config.jsonc is the 5.0 name, config.json is still read.An explicitMCP_CONFIG_PATH/--configpath is unaffected — it names the file it means, whatever else sits beside it. Delete whichever file you are not using. - A delegated chain step (
==>) now refuses a resume that does not carry the worker's handoff token, where it used to accept any prose and advance the chain. The execution brief renders aHANDOFF RESULTtrailer contract naming a per-node token, and the worker's reply must echo that line back with its work. A resume without it is refused by name — the step, the missing line, and how to supply it — and nothing is captured, so a chain cannot advance on a reply that never came from the brief it was handed. The opt-out is one config key: setexecution.delegation.evidencetoadvisory(defaultrequired) to record the reason and accept the resume as before;cpm config set execution.delegation.evidence advisorywrites it, and the write is recorded as a config version like any other. Every delegated step's execution record carrieshandoff_evidence, the reason its resume was or was not acceptable, replacing thedelegation_skippedboolean. Claude Code handoffs additionally pinrun_in_background: false, and the delegation hook refuses aTask/Agentcall for a pending delegated step that does not carry it. - A workspace header now scopes a Streamable HTTP request, as
docs/guides/identity-scope.mddocuments. Since 4.0.0,x-workspace-id,x-project-id,x-organization-idand the other identity headers, and any OAuth token claims, never reached per-request state over HTTP: every request used the server's launch workspace, so asystem_control framework switchsent for one workspace changed it for every client of that server. Each request now reads and writes its own workspace's state, a request with no identity header keeps using the launch workspace, andsystem_control statusreports the requesting workspace's framework rather than the launch workspace's. STDIO is unchanged. Two limits remain and are documented:prompt_enginestill renders with the launch workspace's framework, and resource version history is still recorded under the launch workspace.
- mcp-tools: every system_control parameter is one its handler reads (#366)
- execution: a delegated step's resume must carry the worker's handoff token (#271)
- cli: config changes are versions cpm can restore (#347)
- mcp-tools: every tool refuses an argument key it does not declare (#344)
- mcp-tools: a resource_manager key that nothing reads is refused (#342)
- mcp-tools: a parameter the resource type ignores is refused (#337)
- resources: editing a resource changes only what the edit names (#339)
- config: a workspace config can be config.jsonc, every setting a commented example (#338)
- config: every config key resolves the same from a 4.x file, a 5.0 file, or over MCP (#325)
- frameworks: framework tool text adds to each tool's description (#323)
- config: one generated authority describes the 5.0 config.json (#322)
- config: configuration is read-only over MCP (#312)
- runtime: a workspace, resources directory or workspace config.json the server cannot use now stops it from starting instead of being silently created, replaced or ignored.
- runtime: a config path set with
MCP_CONFIG_PATHor--configthat cannot be used now stops the server instead of starting it on defaults. - mcp-tools: every mutation intent gets a verb the tool can refuse (#262)
- server: three gate/resource YAML contracts change, each with the migration in the error message.
- server: the HTTP transport now binds 127.0.0.1 by default and the four /api/v1/tools/ routes require MCP_TOOLS_WRITE_TOKEN. Deployments reaching the server across a network must set MCP_HTTP_HOST, and any client calling the tool routes must present the new token.
- gates: a gate criterion that cannot be enforced is refused at load.
shell_verifywithoutshell_commandandscript_toolwithoutscript_tool_idfail gate loading, naming the missing field. Both previously auto-passed at review time, so a gate declaring a check it could not perform reported as verified -- the strongest available evidence for the one outcome that carries none. Gate YAML is contract surface, so this is priced as breaking even though nothing in this repository or the three downstream consumers relies on the old permissiveness. - hooks: delete the blocking subagent-gate-enforce hook (S3)
- execution: the rendered ==> handoff text — a client-observable prompt surface — changes shape entirely (brief-at-resume replaces CTA-at-preview).
- mcp-tools:
system_control(action:"session", operation:"cancel")is removed; callprompt_engine(chain_id:"...", cancel:true)instead. The removed operation now refuses with a message naming the replacement rather than reporting an unknown operation.resource_manageradditionally acceptssource_workspace. Both alter the reachable-shape union of the MCP tool surface, which this repository's Public API Contract prices as breaking.
4.0.1 (2026-08-16)
Fixed
- ci: verify the registry entry flagged latest, not the first one returned (5cef21e)
- make gate abort terminal, and stop prose punctuation parsing as arguments (f4452ce)
- prompts: bind prompts per serving unit, resolve content at call time (#227) (e2297de)
Documentation
- docs: correct the origin SHA in the phase-guard plan (#221) (06e88da)
- docs: plan the phase-guard declaration contract (4125d25)
- docs: retire the agent-plugins and plan-retirement plans (2b784e9)
- docs: retire the agent-plugins migration as reference (00d1811)
- docs: retire the plan-retirement federation as reference (ea74eee)
4.0.0 (2026-08-15)
Added
- GitHub-native planning pilot. Repository intake now distinguishes unresolved Discussion ideas from accepted Issues, repairs Issue-form links, and provides the local pilot for a dry-run-first Discussion → Issue → Project → plan → PR lifecycle.
- Patch-mode prompt editing.
resource_manageraction:"update"acceptspatch— anchoredold_string/new_stringoperations overuser_message_template,system_message, anddescription— so a one-section edit no longer retransmits the whole prompt. Anchors are exact-match and uniqueness-checked with typed rejections (empty_old_string,target_absent,anchor_not_found,anchor_ambiguous); a rejected patch writes nothing and consumes no version.dry_run: truepreviews the produced text and diff without writing. Both are update-only —createrejects them explicitly. - All five preserved prompt fields are now authorable through the tool.
injection,register_with_mcp,mcp_prompt_mode,subagent_model,agent_typejoinresource_managercreate/update as optional parameters (additive, non-breaking). The two resolution-hierarchy fields carry an explicit freeze-hazard warning in their descriptions: setting them overrides category/global defaults permanently until unset. - Category ship-signal. Creating or updating a prompt in a category excluded by
server/resources/prompts/.gitignorenow appends a warning naming the allowlist file and the exact lines to add — previously the tool reported success identically whether the prompt would ship or stay workspace-local. Theplanning/category is now allowlisted, so the implementation-planning chain ships with the repo. - Per-step visibility policy. A chain step's YAML may declare
visibility: { withhold: [...], expose: [...] }over three server-sourced context items —previous_step_output,chain_history,unknowns_ledger. A withheld item stays out of every later step's render (replaced by a named[CONTEXT WITHHELD]instruction) until a step explicitlyexposes it, for that step only; a==>delegated step's handoff envelope excludes withheld items and carries a names-only manifest line instead. Withholding covers only what the server sources — the server cannot unsee the client's window, and true isolation remains the==>delegation operator; the docs now state that ceiling explicitly. prompt_engineaccepts aworkflowparameter — a structured, node-addressed workflow IR, mutually exclusive withcommandandchain_id. It is validated server-side against schema and structural budget caps (maxNodes32,maxFanOut8,maxInsertions3, enforced and narrow-only) and then compiled into an ordinary chain run — the resultingchain_runs/chain_run_nodesrows are structurally identical to an equivalent>>chain's, and there is no IR-specific execution path.edgesare dependencies, not branches: they linearize into one run order (Kahn's algorithm, ties broken by declaration order); there is no branching runtime. An invalid workflow is rejected with one addressed line per problem naming the offending node/edge and rule, and writes nothing — no run, no session, no version. IR nodes can carry every field the runtime consumes that the string command grammar cannot express — stable node ids, per-stepvisibility, gate bindings (gates,inlineGateIds), delegation hints (subagentModel,agentType), andinputMapping/outputMapping. A declareddeclaredCostCeilingis recorded on the run's terminal telemetry and never enforced. See Workflow IR Reference.- Chain steps accept a
frameworkfield, selecting the framework for that step alone. It outranks the run-wide default and yields to an explicit^Frameworkoperator on the command; an id the registry does not know falls back to the run-wide framework rather than failing the load. - Documentation governance now routes public-doc changes through a versioned
documentation_changechain, with focused gates for product positioning, information placement, semantic discoverability, and prose hygiene. prompt_engineaccepts an optionalobservationsparameter — chain steps declare typed unknowns (discovered/resolved) that accumulate in a per-run ledger and surface in subsequent step context.execution_recordsnow records per-run telemetry (steps planned/executed, gate verdict submissions, the FAIL subset of those submissions, unknowns opened/closed) as record-only facts on terminal rows, surfaced as one plain-text line per session bysystem_control execution_history. No scoring, weighting, or routing decision is derived from these fields. Schema v21 —execution_recordsisephemeral, so existing rows do not survive the bump.- Adaptive chain mutation v1 — a blocking unknown inserts one investigation step; an irrelevant-resolved unknown skips its declared target step; both audited on the terminal execution record (
nodes_inserted/nodes_skipped) and capped per run (1 insertion per unknown id, 3 per run). The model only ever declares typed observations; the server owns every graph edit, in reaction to a declared observation only. Schema v23 —chain_run_nodesgainsorigin/origin_unknown_idprovenance columns.
- agent-plugins migration, codex fleet integration, and brand system for 4.0.0 (3d402fe)
- chains: add per-run unknowns ledger via prompt_engine observations param (0bdbfbe)
- chains: add record-only per-run telemetry to execution_records (2e84bb3)
- chains: per-step visibility policy + step-scoped gate review (P5) (c07a80c)
- chains: planner-submitted workflow IR compiled to ordinary chain runs (56d8e26)
- chains: stable node identity, per-row run storage, and adaptive mutation v1 (951db98)
- ci: publish server.json to the official MCP registry on every release (5b9ca4b)
- ci: ship the new mark as the desktop extension icon (b165014)
- ci: ship the new mark as the desktop extension icon (d2ff78a)
- ci: sync codex-prompts with the fleet on every release (dcfc124)
- docs: pilot GitHub-native planning lifecycle (237b0ca)
- docs: promote the approved mascot into a canonical brand system (de19f56)
- execution: publish the resolved shell-verify budget and correct force_restart (d76f741)
- gates: add documentation governance gate suite (0beda2d)
- hooks: add hook-harness traced verification and registration dead-wiring detection (b216289)
- main: integrate unreleased 3.3.0 work (c5a7813)
- main: integrate unreleased 3.3.0 work (#211) (c5a7813)
- mcp-tools: enforce confirm on prompt delete; remove the checkpoint resource type (2c3b872)
- mcp-tools: patch-mode prompt update, argument contract repair, ship-signal (P7 T1/T3/T4) (1210977)
- planning: federate plan retirement (621991a)
- planning: federate plan retirement (9bd68db)
- planning: federate plan retirement (#209) (621991a)
- prompts: per-step framework declaration; reject unknown chain-step keys (0e8b787)
- resources: add documentation governance prompt workflow (5f2a1ae)
- resources: ship the planning/ prompt category (P7 OQ-P7-7, T5) (15462ed)
- scripts: commit row 1.5's version registration and its manifest parity (ce8b3d8)
- scripts: declare what each gate reads, and test the rewriter against the corpus (e582adc)
- scripts: fail when a validate/verify script is wired to nothing (a295645)
- scripts: gate framework coverage and the README's shipped-prompt count (f90b024)
- scripts: render the published manifests from the canonical tree (c93034c)
- scripts: require a gate declaring closedBy to audit its exceptions (ee1478e)
- scripts: ship no skills by default; gitignore the export output (16b1a09)
- server: commit row 1.4's agent-plugins skills-sync registration (5f20ff9)
- server: commit Tier 1's deliverables, which were never committed (31e0638)
Changed
- BREAKING —
confirm: trueis now required to delete a prompt.resource_manager(resource_type: "prompt", action: "delete")refuses without it and names the chains that would break. The parameter's schema text has always described it as delete's safety gate, but onlyrollbackread it — the guard sat on the recoverable verb and was absent from the unrecoverable one. Deletion has no undo through the tool surface: a deleted prompt'sversion_historyrows survive, butrollbackreports "Prompt not found" once the prompt is gone, so those snapshots are unreachable by any action. Scripts calling delete must addconfirm: true. - BREAKING — chain steps reject unknown keys.
chainStepsentries in prompt YAML are validated strictly; a key with no schema field now fails the prompt's load, naming the key and its step index, instead of being silently discarded. A typo likeframwork: ReACTpreviously parsed to a normal-looking step and ran the chain under the wrong framework with no signal — the same silence left sixinlineGateIdsdeclarations dead across three shipped chains. Every key with a real consumer was declared first, includingdelegation, which no code in the prompt module reads but the skills-sync exporter reads straight off the YAML. - Renovate hosted reruns now use a dry-run-first request script that verifies local/remote config parity, strict protected checks, full-SHA Actions enforcement, successful Renovate and Release Please checks on
main, and the exact dashboard request marker before changing GitHub state. Automated lock maintenance retains Renovate's three-day npm resolution cutoff while treating the timestamp-less synthetic maintenance update as timestamp-optional, preventing a permanentrenovate/stability-daysblock.
- frameworks: retire the pre-rename methodology back-compat folds (05b6efc)
- scripts: port no-crosslayer-relative to dependency-cruiser and retire the guard (3837b91)
- scripts: scope shipped-content gates by git, not filesystem walk (8468c4a)
Removed
- BREAKING — the
checkpointresource type is gone.resource_manager(resource_type: "checkpoint", ...)and theclearaction are removed from the tool surface, along with theGitCheckpointmodule behind them. Every checkpoint action returned "Checkpoint manager is not available" under every configuration: the handler was defined and exported but never constructed, so a quarter of the publishedresource_typeenum could not succeed. Reinstating it was rejected rather than deferred — the actions weregit stash push/pop/dropwrappers, and exposing stash manipulation to a client silently discards uncommitted work belonging to anyone else sharing the working tree. Nothing depended on it, since no call could ever have returned success. - BREAKING — the pre-rename
methodology*back-compat folds are retired. The methodology→framework rename shipped in v3.0.0 with fold-forward support for the old spellings; those folds are now deleted. Six input spellings stop being accepted, and in every case but one they fail by silently falling back to the default rather than erroring:- framework YAML
methodologyGates:→ useframeworkGates: - gate YAML
pass_criteria.methodology:→ useframework:; andtype: methodology_compliance→framework_compliance(this one throws at parse, being a closed enum) config.jsongates.methodologyGates→gates.frameworkGates; the top-levelmethodologies:section andresources.methodologiesare no longer adopted into theirframeworksequivalentsresource_managerauthoring payloadmethodology_gates/methodology_elements→framework_gates/framework_elements- prompt-template placeholder
{METHODOLOGY}→{FRAMEWORK_TYPE}Marked breaking because the framework/gate YAML schemas andconfig.jsonare declared API surface. Shipped in a minor by explicit maintainer decision — the pre-rename spellings have been unsupported since v3.0.0 (2026-07-31), and the only spelling measurably outside the declared contract was theresource_managerauthoring payload. If you hold a workspace resource written before v3.0.0, rename the keys above before upgrading.
- framework YAML
Fixed
- Version history now records what an edit produced, not what it replaced. Version N holds the state edit N produced (go-forward numbering), so the newest version always equals what
inspectshows; unrecorded prior state is bridged in automatically, rollback validates its target before writing anything, records the restored state as a newRollback to vNrow, and restores snapshots exactly — the oldsnapshot ?? livehybrid merge and the "Pre-rollback snapshot" rows are gone. A failed version save now aborts the update instead of logging and proceeding. - Tool schema stopped silently discarding argument fields. The
argumentsitems on prompt create/update now acceptrequired,defaultValue, andvalidation, andtypeis the loader's 5-value enum — previously Zod stripped them at the first boundary while 45 shipped prompts declaredrequired: true. - Chain step labels retired their stale phase vocabulary. The implementation-planning chain's five
stepNamelabels (and two prose references) now carry the sub-prompts' own(Step N)names instead of(Phase N)numbering that no longer exists. - Step-targeted gates now review only their target step. A gate bound to a node id (or step number) previously entered the run-wide review accumulator, so a gate "on step 3" reviewed every step — targeting scoped guidance injection only. Review participation is now carried per-step (
reviewGateIds), with untargeted gates keeping their run-wide inheritance and single-prompt runs byte-identical. The two residual fallback paths are also closed: a mutation-inserted node inherits its triggering unknown's target gates, and a skipped step triggers no review at all. - A step-level
subagentModelnow marks its step delegated on any chain invocation, not only after a==>operator.OperatorValidationStagereturned early on an empty operator set before delegation normalization ran; the direct (non-symbolic)>>chainpath always has an empty operator set, so a YAML-declaredsubagentModelparsed but produced no delegation CTA or handoff envelope unless the command also spelled==>. Delegation normalization now runs before that exit.agentTypealone still does not mark a step delegated — it only selects which agent a==>-delegated orsubagentModel-marked step uses. - Handoff visibility (and the delegation CTA/envelope it feeds) is now resolved by node identity instead of array position.
resolveHandoffVisibilityindexed the parsed step blueprint bynextStepIndex, so after an adaptive mutation inserted or skipped a node, the visibility declarations resolved belonged to the wrong step. The run's live next-node id is now asked for directly and matched back to the step it names; awithholddeclared by a step the mutation policy retired (skipped) no longer applies to a step that does run. Runs with no mutation and chains with no declared node ids are unaffected. - Named outputs (
outputMapping) now publish under a reservedoutputs.<name>namespace ({{outputs.findings}}) instead of a flat template key ({{findings}}), and are removed together with the rest of chain history when a step withholdschain_history— previously a withheld alias still leaked through becausestripChainHistorycould only delete regex-identifiable positional keys.{{findings}}is no longer published; existing authors must migrate to{{outputs.findings}}. No shipped chain declaredoutputMappingat the time of this change. Each key ofoutputMappingstill receives the step's whole output — the declared value is not read (documented, not newly broken).
- ci: assert the codex-prompts marketplace entry, which nothing checked (769b023)
- ci: consume repository-standards v1.3.0 so retirement stops restyling docs (4c6cde7)
- ci: consume repository-standards v1.3.0 so retirement stops restyling docs (23af8b4)
- ci: make registry publish idempotent and fix the verify jq path (b845930)
- ci: rebuild the marketplace source guard where it runs (852830b)
- ci: ship package.json in the claude-code-plugin staging too (edb037c)
- contracts: anchor the gate operator's examples and gate registry self-consistency (18d2f7a)
- deps: consume repository-standards v1.2.1 so retirement ignores sidecars (93ebbe9)
- deps: patch OTel advisories via 0.221.0 bump; retire stale protobufjs override (b3f76b8)
- docs: repoint dead cross-references in the retired plan set (18fa3fe)
- execution: resolve handoff visibility by node id, not array position (P6 T2, P4-F2/P6-F1) (db966f8)
- gates: close the last two fallback-to-run-wide review paths (P5 4.4/4.5) (b6cceb5)
- gates: unref temporary gate expiry timers (528647f)
- hooks: delegation enforcement deadlock (mention-vs-use arming, gate sentinel, Agent tool clear) (3073dfd)
- parsers: commit D3's remaining
^sites, which never landed (572789d) - parsers: commit the providers 8875ab42 left behind (f54ab79)
- parsers: commit the reserved-operator rejection its tests already assert (08dd126)
- parsers: repair operator registry SSOT, unbreaking
^in chains (f2336ac) - pipeline: make YAML delegation reachable on the direct chain path (P6 T1, P5-F5) (a5097a5)
- release: accept Release Please manifest formatting (a1c6333)
- release: accept Release Please manifest formatting (0a8cb30)
- release: synchronize Agent Plugins manifest (9c4c088)
- release: synchronize Agent Plugins manifest (4daf326)
- resources: avoid methodology-vocab false positive in readme_improver (3bf8c9a)
- resources: retire methodology vocabulary from planning prompts (d624481)
- runtime: apply preset timeouts, clear the loader cache on reload, reject reserved + in chains (8875ab4)
- runtime: claim the SqliteEngine singleton at the composition root (741afe5)
- scripts: allowlist tracked-scope.js in the vocabulary guard (d1b03ea)
- scripts: commit the two validate scripts a2956450 wired to nothing (727f802)
- scripts: re-resolve the downstream lock with npm update, not npm install (a6bfef8)
- scripts: resolve build outputs in the agent-plugins consumes check (b85f3fd)
- scripts: scan the tracked set so the vocab guard can see dot-paths (5af52f5)
- scripts: stop the plan-row self-test depending on where a real plan lives (2e143f8)
- scripts: stop the tests-typecheck ratchet reading a parse failure as a clean backlog (8428e3e)
- scripts: widen eslint ratchet target to scripts/ and eslint-rules/ (1a96887)
- server: repair injection continuation, nested-prompt ids, doubled banner literal (2286bcb)
- server: version history records what an edit produced, not what it replaced (P7 T2) (08b001f)
Documentation
- docs: adaptive-chain-runtime master plan + P1 unknowns-ledger plan + staged P0 template (1c87f4a)
- docs: add a square 512px icon derived from the logo for directory listings (c874040)
- docs: add Tier 6 + Agent Plugins migration plan (single source tree, rendered distributions) (713f520)
- docs: bring Tier 2b current — registry+Smithery live, Glama claimed, PulseMCP baseline (a6ca08b)
- docs: close 0.5b Workstreams A/B/C, file the checkpoint finding as 0.5.26 (ec73f85)
- docs: close 4.0.1, unblock 4.4, row the unmeasured install path (13e2ba2)
- docs: close 4.10 by deletion, row the unvalidated marketplace PR path (0328f21)
- docs: close 4.4, ship 4.12's capability, record the 4.0.0 verification (2a6e980)
- docs: close 4.5, record shape A as implemented, row the last unobserved link (584f6db)
- docs: close 4.8 from a live marketplace install, row same-name shadowing (30f2207)
- docs: close all release gates — v3.2.1 finalized across every surface (009d952)
- docs: close E9/E10, file E11 — a ✓ did not mean committed (37b012e)
- docs: close the push sequence, row the SIGPIPE flake (6c1d213)
- docs: close the Renovate remediation plan — the canary merged itself (3490711)
- docs: close Tiers 2-3, mark the falsified premises (c5405be)
- docs: close Tiers 5-6, apply the owner rulings (0e9c3ef)
- docs: close Workstream D and E6, correct the D1/D3 site claims (47636b0)
- docs: confirm 4.5 — a git-sourced Codex install has no runtime (84aa445)
- docs: correct the downstream consumption model (a4e51ca)
- docs: defer Tier 5 — Codex integration + rendered-distribution decision (9f2922e)
- docs: documentation governance policy and client-install CTA correction (4bb8955)
- docs: file E8 for the two foreign validate:all reds (7fcdc4e)
- docs: file E9/E10 — the committed state was six gates behind the tree (6a3bab3)
- docs: finalize Tier 4 — close the shipping lane, name every open blocker (d8c806f)
- docs: flush the extension-icon findings to the deviation log (7ff6fab)
- docs: keep each retiring plan with its own deviation log (b91a260)
- docs: link Workflow IR reference from docs index (06d9630)
- docs: mark the brand-asset plan finished (b11624a)
- docs: p1 live drive complete — success signal observed; p0 template applied (0013955)
- docs: patch mode, version semantics, visibility fields, ship-signal (P5/P7) (7897f7e)
- docs: queue adaptive-chain-runtime plans for retirement at release (c88ebb2)
- docs: queue claims-conformance suite design (token-free functional validation) (7dc83ae)
- docs: record accepted-work lifecycle (9232acd)
- docs: record Codex skill repair (a9b4df1)
- docs: record commit-preparation verification trail on P6 companions (1f2e8fb)
- docs: record E7 — the gate that watched the wrong tree (717e792)
- docs: record final commit-preparation verification trail (f6da764)
- docs: record further commit-preparation verification trail (ce07dc4)
- docs: record GitHub Project rollout (0481033)
- docs: record HEAD verification runs in the implementation ledger (ac07fd1)
- docs: record orphan cleanup + nested-delete fix in p1 notes (5e89d28)
- docs: record p1 unknowns-ledger implementation notes + status writeback (c809943)
- docs: record P5 compile-closure verification runs (5ce70a7)
- docs: record p5/p6/p7 plan writebacks and implementation notes (a2af74d)
- docs: record P6 plan writebacks + master-plan closure (860af13)
- docs: record PR linkage fallback (89d6cb2)
- docs: record sub-agent delegation contract plan (ab8ded5)
- docs: record the auto-merge-on-major finding (59f3c3d)
- docs: record the breaking bundle and close its plan rows (82cf4cd)
- docs: record the migration-exposed retirement defect and release validation (fdc7ea0)
- docs: record tiers 0.8-4.3, and what the probes got wrong (4bc3b6b)
- docs: retire codex-prompts-port and downstream-standards-federation plans to reference (f011130)
- docs: retire E8, flush the retirement deviations (e8f84ef)
- docs: retire the Renovate notes with their plan (a61479f)
- docs: retire the Renovate notes with their plan, not after it (274a0de)
- docs: retire Tier 5 into Tier 6; draft step-10 awesome-claude-code form for owner (2add00d)
- docs: row the codex distribution shapes, flag the same risk in gemini (fec8a60)
- docs: row the npm ruling and the codex staleness before Tier 4 (27cb02a)
- docs: row the release-time sync, correct what joining the fleet costs (f850cd7)
- docs: shipped prompt count 27 -> 28 (5862564)
- docs: stamp the open planning rows, log the release-readiness sweep (26352a9)
- docs: state the resource:// opt-in, correct the prompt count, close Tier 0.5 (1044afd)
- docs: track the brand-asset plan and close its acquisition-recovery row (faf69ae)
- docs: workflow IR reference + chain docs lockstep + CHANGELOG (af4a8c5)
- planning: bind retirement plan to issue 210 (b2fb2de)
- planning: record GitHub lifecycle linkage (af83915)
- planning: record isolated review handoff (5d42f97)
- planning: record mainline delivery linkage (384c76c)
⚠ BREAKING CHANGES
- prompts: chain steps are validated strictly. A key with no schema field now fails the prompt's load, naming the key and its step index, instead of being silently discarded.
- mcp-tools:
resource_manager(resource_type: "prompt", action: "delete")now requiresconfirm: true, and thecheckpointresource type andclearaction are removed from the tool surface.
Maintenance
3.2.1 (2026-08-07)
Fixed
- ci: ship package.json in the plugin dist layout; patch protobufjs to 8.7.1 (655f324)
3.2.0 (2026-08-07)
Added
- Codex CLI support seams: generic
PLUGIN_ROOTworkspace resolution, acodex execspawn strategy (SpawnConfig.client+CodexModelStrategy), and a codex skills-sync registration — consumed by the new codex-prompts downstream plugin.
- chains: let a prompt choose which subagent its delegated steps spawn (8a4c066)
- ci: retire finished plans at release and gate the misclassification (15a71d4)
- hooks: add the codex client seams to the shared hook library (98e7f4e)
- hooks: require every shipped hook to be registered, and retire session-skills (e6c19eb)
- mcp-tools: build the prompt_engine surface from state and take gate verdicts structurally (d27bafa)
- runtime: reject the removed sse transport instead of substituting one (21d715f)
- runtime: serve both protocol eras over STDIO and route list-change events (96957d2)
- runtime: serve MCP protocol revision 2026-07-28 over stateless HTTP (d1400b9)
- scripts: detect state fields that have readers and no writers (9b3d1fc)
- scripts: give the one-time durable exclusion a retirement condition (36ff257)
- scripts: sort finished plans out of the working set by status (8504088)
Changed
- The
analysisconfig section is deprecated, and no longer settable from either tool surface. Aconfig.jsoncarrying it still loads with its values intact — the section is parsed and ignored for one deprecation cycle, and the server now warns once at startup naming the replacement. What is withdrawn is the ability to set it:cpm enable analysisreportsUnknown subsystem, and bothcpm config setandsystem_control configreject the fiveanalysis.semanticAnalysis.…keys. Scripts that set them were previously writing values no runtime path read; they now receive a non-zero exit instead of a silent no-op. Delete theanalysissection to silence the warning. The section itself is removed in the next major. llm_self_checkgate criteria no longer consult configuration. The reserved type is unchanged in the gate YAML schema and still auto-passes, so gates declaring it are unaffected. Its skip message previously told the reader to enable a config key that no longer exists; it now names%judgeandshell_verify.- Prompt analysis feedback is no longer suppressed by a disabled flag.
resource_managerprompt create/update responses previously printed⚠️ API Analysis Disabledand withheld gate suggestions whenever the LLM flag was off — which was every installation, since it defaults off. The underlying analysis is rule-based and needs no model, and a sibling code path already ran it ungated, so the suggestions now always appear.
- chains: give the chain run-identifier format one owner (f4ad2a1)
- chains: rename tenant_id to run_owner_pid and repair the view that never read it (a0dc36f)
- execution: drop six ConvertedPrompt fields no producer ever set (d66356a)
- execution: give auto-approve partitioning to the filter that already partitions (15270fd)
- gates: delete the gate retry re-entry API, which had no callers (7a43f99)
- gates: delete the unreachable enforcement-decision cache (335a8b6)
- gates: move the shell-verify clearing decision to its owner module (20b795e)
- mcp-tools: delete the dead ContextBuilder rather than consolidate it (05ab145)
- pipeline: delete the two judge-selection channels that never had a producer (76630b7)
- pipeline: extract execution metric payload derivation (b840472)
- pipeline: move framework-requirement predicates out of the stage (a25aaf2)
- pipeline: stop deriving the framework requirement twice (6479ab1)
- runtime: express the version_history cleanup as a schema bump, not resident code (92b1dcd)
- runtime: extract list-change routing and cover both transports (0f5437f)
- scripts: retire four guards, re-home them, and share one definition of a live exception (c4b920b)
- server: delete 11 unreachable types and the analyzer's unread config (558e4dc)
- server: delete the last nine unreachable types in shared/types (675095f)
- server: remove the 76 dead symbols the unused-locals sweep reported (31dd755)
- server: retire the semantic LLM side client, which never ran here (d219f8b)
Removed
- The semantic LLM side client is gone — roughly 2,200 lines across an outbound provider client (
LLMClient+ factory), the gate service that consumed it, the dual-mode analyzer's model branch, and an 863-line framework-integration module that was never constructed. None of it ever ran: it was reachable only whenanalysis.semanticAnalysis.llmIntegration.enabledwas true, that flag defaults tofalse, and the only spelling the CLI ever wrote (…llmIntegration.mode) reached no reader. Model-graded gate review is served by judge mode — the%judgemodifier andgates.evaluation.defaultMode— which delegates to the client's own sub-agent and returns throughgate_verdict, so no API key is configured, stored, or sent anywhere. - The undocumented
MCP_LLM_*environment variables (MCP_LLM_ENABLED,_API_KEY,_ENDPOINT,_MODEL,_MAX_TOKENS,_TEMPERATURE). They were never listed in the declared environment surface.
Fixed
- Codex prompt resource activation: the downstream launcher now reads a persistent user-level resource path while keeping mutable MCP state under the OS temporary runtime root; the curated 26-prompt package catalog remains the default.
- Structured gate verdicts are now hook-enforced:
hooks/gate-enforce.pycrashed on the object form ofgate_verdict(the schema-preferred shape); because hook failures are fail-open, object verdicts were never gate-enforced on any client. Surfaced by the codex-prompts port's live E2E. - The state database is now closed on shutdown, and its write-ahead log checkpointed. Nothing ever called
SqliteEngine.shutdown(), so the WAL was never checkpointed and grew across every restart — measured at 4.2 MB against a 598 KB database, truncated to 0 by a clean shutdown. The close runs last, after every subsystem that may still write on its way down. - The execution ledger grew without bound.
execution_recordshad noDELETEanywhere and declared its retention as "unbounded-justified" with a rationale readingPLACEHOLDER, whilestate.dbis shared across every project on the machine. Row caps are now declared per table and enforced at startup, and the resource-change log trims through the same shared implementation instead of its own inlinedDELETEagainst a config value that could disagree with the declared cap. - The
tenantstable is gone (schema v19). It held one seeded row, had no reader outside tests that inserted into it to prove it existed, and nothing referenced it —tenant_idelsewhere is a plain column, never a foreign key. - The
cpmCLI could leave the MCP server unable to start. Version-history commands reachedstate.dbthrough an embedded Python sqlite3 helper carrying its ownCREATE TABLE, whose shape predated the workspace-isolation columns. Running the CLI before the server's first start createdversion_historywithout those columns, and the server then failed to boot withno such column: workspace_id. The CLI now usesnode:sqlitedirectly, resolves the same workspace scope the server does — they previously wrote history under different tenants and could not see each other's versions — and no longer creates any schema. - Database initialization failures now fail startup instead of degrading silently. A configured database that failed to open was logged at
warnand swallowed at three startup wiring sites, so the server reported a clean start while running with no audit trail, no argument history, and an inert version-rollback feature. A failed resource-index resync during hot-reload likewise no longer reports "completed successfully". - Codex plugin runtime: plugin launch no longer depends on Claude root interpolation, and mutable SQLite/log output can be routed to a sandbox-writable runtime root without moving bundled resources.
- ci: make plan retirement self-contained and record why it runs on the PR (cbdfeae)
- ci: point downstream sync PR bodies at the renamed upstream repo (410acb7)
- ci: re-add formatted staged files with -f in pre-commit (59dc266)
- config: stop gitignoring the project rules the comment says to track (7dc9b15)
- deps: close the Dependabot set — override tmp to 0.2.x, plan Tier 4 triage (8381c0a)
- gates: correct the workflow-preflight source, not its compiled output (163a58a)
- hooks: enforce the structured gate_verdict shape gate-enforce claims to check (a5d1b60)
- prompts: drop the undocumented gates alias on prompt update (0e7f857)
- runtime: scope every state.db writer, then delete the backfill migration (378b0bf)
- scripts: close the root package.json version drift channel (b753038)
- scripts: read script flags as ground truth, not just the two parser tables (6ebc998)
- server: bound the execution ledger and drop the dead tenants table (821e2af)
- server: close the database on shutdown and fail loudly on init failure (fae1f55)
- server: stop the CLI creating state.db schema, which blocked server startup (2d83827)
Documentation
- docs: add plan-board frontmatter to the pipeline follow-up plan (453dfe1)
- docs: add the G4 CI run log to the implementation notes (2e2c9da)
- docs: add the implementation notes both plans should have had (bdadea3)
- docs: bring Tier 2b statuses current — pushes done, main merge is the sole gate (9fc2a2f)
- docs: close out the sidecar retirement plan and carry its open findings (a59d58c)
- docs: close the implementation_plan prompt defect, and correct my own report of it (ba975e5)
- docs: commit the plan-frontmatter rollout and apply the done/reference test (ba366cd)
- docs: correct F4's delivery model — it targeted repos with no plans (a3cdc88)
- docs: drop legacy smithery.yaml — Smithery now takes the MCPB release artifact (e223088)
- docs: file Tier F5 — the MCP surface verifier runs in no workflow (06ed8c3)
- docs: file Tiers 15-16 for the work Tiers 10 and 12 deferred (251aa67)
- docs: finalize the SQLite remediation plan against measured outcomes (8bb5555)
- docs: format the plan my frontmatter rollout left unformatted (5dbf017)
- docs: log G2 execution deviations in the implementation notes (9059d07)
- docs: log the tool-schema snapshot recapture in the implementation notes (a2db54c)
- docs: make the retirement script publishable and file the federation tier (f6de2ec)
- docs: mark F9 and F11 resolved where a reader actually looks (50954b1)
- docs: mark tier completion where it is scannable, and correct F14 twice (a8f1ba3)
- docs: rebrand to Wolfflow — rename repo references, README identity, plan records (9d9eeb4)
- docs: reclaim the -mcp repo suffix; prepare registry integration files (8506b76)
- docs: reconcile the SSE references left behind by the transport removal (ffcdc95)
- docs: record the codex port -- alignment rule, changelog, plan + notes (75960b3)
- docs: record the Tier 8 stage domain-ownership review and scope Tiers 11-14 (bb9fdb6)
- docs: record Tier F5 execution — three plan premises corrected (1df409a)
- docs: record where the validation-mechanism work landed, and why it is three commits (1bbc304)
- docs: revert Wolfflow branding to Claude Prompts; align README with MCP-ecosystem conventions (848f7db)
- docs: scope the Python hook contract to the durable surface (774252f)
- docs: strip trailing whitespace that fails the PR-range hygiene check (e0d3255)
- mcp-tools: use the framework vocabulary in the gate-veto comment (9133654)
- scripts: cite the plan convention at a path every consumer can reach (06ef228)
⚠ BREAKING CHANGES
- chains: schema v20 renames chain_sessions.tenant_id and chain_run_registry.tenant_id to run_owner_pid, and drops
lifecyclefrom v_execution_status. Both tables are derived/ephemeral and are dropped by the bump, so no data migrates. - runtime:
--transport=sseandtransport: "sse"in config.json are rejected at startup rather than silently resolved to another transport. Usestreamable-http. - runtime: The deprecated HTTP+SSE transport is removed; use
--transport=streamable-http. The Streamable HTTP transport no longer issues or requires anMcp-Session-Idheader.
Maintenance
- server: pin the next release to 3.2.0 (e709a1c)
3.1.1 (2026-08-03)
Fixed
- deps: defer CLI TypeScript 6 migration (#193) (aef8e5e)
- deps: defer TypeScript 7 migration (#190) (04a9931)
- deps: update server runtime dependencies to ^0.221.0 (#184) (b848ae6)
3.1.0 (2026-08-03)
Added
- frameworks: make the default framework config-declarable per project (8b948fb)
- frameworks: scope framework state per project (905c926)
Changed
- Dependency delivery contract: Renovate now preserves semantic release types, validates extraction, uses the committed MCPB resolution path, and limits future automerge to observed low-risk development updates.
- Supply-chain controls: GitHub Actions use immutable full commit SHAs with readable release comments, backed by a repository validator.
- Runtime support: The server and desktop extension require Node.js >=22.13.0; server CI covers 22.13.0 and 24, the standalone CPM CLI remains >=18.18.0, and local/publish tooling uses Node 24.
- MCP SDK v2 and protocol revision 2026-07-28: Migrated to the scoped
@modelcontextprotocol/*v2 packages. One build serves both protocol eras — a 2026-07-28 client (noinitializehandshake, per-request_metaenvelope) and a 2025-era client are answered over both STDIO and Streamable HTTP. - Stateless HTTP: Protocol sessions are gone. A fresh server instance is built per HTTP request; no
Mcp-Session-Idis minted and requests carrying a stale one are served normally. Chain state is unaffected — it was always keyed by this project's own run handles (chain_id), not by protocol sessions. prompt_engineparameter surface is derived from runtime state:gates,gate_verdict, andgate_actionare advertised only while the gate system is enabled, since the runtime ignores gate input from every source when it is off. The declared contract is the union of all reachable shapes, so narrowing within it is not a breaking change.- Structured gate verdicts:
gate_verdictnow accepts{overall, rationale, per_gate[]}in addition to the"GATE_REVIEW: PASS - reason"string. The object is schema-validated, so it cannot be submitted malformed; the string form is read back by regex and can fail to parse. Rationales are single-line. The string branch is retained for existing clients and is scheduled for removal.
Removed
- The deprecated HTTP+SSE transport. SDK v2 no longer ships
SSEServerTransport, so keeping it would have meant hand-maintaining a transport upstream had deleted.--transport=ssenow exits with an error namingstreamable-httprather than falling back to the configured default — the previous fallback started the server on a transport nobody asked for and reported success. The same check applies totransportinconfig.json. Streamable HTTP still usestext/event-streamframing for responses; only the separate transport is gone. - The protocol session registry, together with the session-attachment failure path it required.
- ci: route validation by change impact (#183) (c7509c2)
- frameworks: route fallback defaults through DEFAULT_FRAMEWORK_ID (b41adc4)
- mcp-tools: drop dead wiring from the tool constructor seams (6642c04)
- pipeline: construct the pipeline from an ordered stage array (094baec)
- pipeline: delete DependencyInjectionStage and the metadata bag (af8401a)
- pipeline: drain the execution context metadata bag to one key (b29dff2)
- pipeline: renumber the 22 stage files to execution order (467ee5a)
- server: land finalized pipeline and framework remediation (67e905e)
- server: type McpToolRouter's mcpServer as McpServer (2ddd763)
- server: type TransportRouter's mcpServer as McpServer (4feaab2)
Fixed
- ci: allow Renovate validator to inspect updates (#179) (927a816)
- deps: align Renovate with delivery contracts (#177) (6ac5e87)
- deps: enforce Renovate release-age gate (#187) (c091bbf)
- gates: declare the gate surface on the hook and notification ports (d4a5360)
- pipeline: derive skipped stages instead of accumulating them (cd7abc0)
Documentation
- deps: close Phase 6 Renovate rollout (#180) (a785167)
- deps: scope the SDK v2 package swap and record Tier B's blockers (59dfdee)
- docs: record acquisition recovery baseline (#182) (8bfd41b)
- pipeline: add coordinator decomposition tier (#181) (41cbbb9)
- pipeline: drop a doubled stage name left by the renumber sweep (bd8095a)
3.0.2 (2026-08-02)
Changed
- deps: the published MCP
inputSchemachanged in 39 places, all of them permissive.@modelcontextprotocol/sdkselects its JSON Schema converter from the installed zod major (zod-json-schema-compat.js:19-28), so upgrading to zod 4 swapped the engine that produces the tool surface. The principal difference is thatadditionalProperties: falseis now emitted asadditionalProperties: {}. No consumer action is required: every request that validated under zod 3 still validates under zod 4 — the surface changed shape without narrowing what it accepts, which is why this ships as a patch rather than the major Release Please computed. The surface is now pinned byserver/tests/snapshots/mcp-input-schemas.jsonand checked in CI, so a future converter change cannot land unnoticed.
Fixed
Maintenance
- deps: dependency modernization — Tiers A–D (zod 4, js-yaml 5, TypeScript 6, ESLint 10) (#172) (a28119c)
- release 3.0.2 rather than the computed 4.0.0 (#173) (550b2e4)
3.0.1 (2026-08-01)
Fixed
Changed
- server: migrate cross-layer imports to package.json subpath imports (901081a)
- server: subpath imports + declared public API contract (b05a181)
Documentation
- declare the public API contract that major versions protect (02f937f)
3.0.0 (2026-08-01)
Added
cpmCLI now ships with the npm package: the workspace CLI is published as a second bin ofclaude-prompts, so it runs from any directory without cloning the repo or installing an MCP server —npx -p claude-prompts cpm validate --all -w ./my-workspace. Useful for scripting, CI, and for agents working outside a configured MCP client. The bundle is self-contained (no runtime dependencies) and is built fromcli/srcbyserver/esbuild.config.mjs, which importscli/esbuild.config.mjsrather than duplicating it so the standalone and published bundles cannot drift. See CLI Guide.- CLI validated in CI: a
CLIjob now runs the CLI's typecheck, build, and integration suite on every run.jest/ts-jestwere missing fromcli's devDependencies, so its 75 integration tests had never executed; six were failing against fixtures left stale by thecontent_check→inline_guidanceand methodology → framework retirements. Fixtures corrected, suite green. - Warnings for dropped inline gate definitions:
normalizeInlineGateDefinitionspreviously discarded malformed definitions in silence. Each drop now logs the prompt, the gate (by ID, else name, else position), and every field that disqualified it — all of them, not just the first, so one load cycle reports every mistake. Applies to YAML and markdown prompts. Malformed definitions are still dropped rather than failing the load, so a bad block costs one gate instead of the whole prompt. This is release N of the warn-then-arm migration described under Deprecated. - Prompt-level injection control: a prompt may declare an
injectionblock in itsprompt.yaml(system-prompt,gate-guidance,style-guidance, each acceptingenabled,frequency,target). It resolves between step and chain config, taking the hierarchy to eight levels. Settingsystem-prompt.enabled: falsealso withholds gates that score methodology adherence, since scoring a methodology that was never injected is incoherent. See Injection Control. session:cancelaction onsystem_control(Tier 3): MCP clients can now cancel an active chain session viasystem_control(action:"session", operation:"cancel", session_id:"chain-X#1"). TransitionsrunStatustocancelled; idempotent on already-cancelled sessions; refuses sessions in terminalcompleted/failedstate. Useoperation:"cancel"for soft-stop (preserves session for audit),operation:"clear"for hard removal (deletes session and chain history).ExecutionRecordpersistence (Tier 5): Pipeline stages 9 and 10 now emit append-only ledger rows to theexecution_recordstable on every chain-step transition. Stage 9 emits aworkingrecord per render (withsubstate.renderedAt); stage 10 emits acompletedrecord on chain terminal. Records carry SEP-1686-alignedStepLifecycle+StepSubstate+GateVerdictSummaryshape. ULIDs (monotonic) preserve insertion order across rapid emissions. Emission is best-effort — failures log a warning and never break pipeline execution.command-tokenizer.ts: Pure functiontokenizeCommand()with quote-aware detection for all 8 operator types (chain, delegation, gate, parallel, repetition, conditional, framework, style). Includes delimiter overlap filtering to prevent==>from false-matching as gate operatorcommand-tokenizer.test.ts: 56 tests covering all operator types, quoted argument regression suite, mixed operators, prompt ID extraction, cleaned command generation, and edge cases
- execution: execution ledger Tiers 1-5 + Phase 4 SQLite cleanup (#131) (9fd4520)
- gates: add script_tool verification criteria and fix schema normalization (d12c278)
- gates: clarify gate vocabulary, add path-verification gate, documentation pass (#132) (7d46db0)
- gates: gate resolution precedence (ADR 0001), injection hierarchy, launcher envelope (a06287d)
- scripts: add verify:mcp to check a build without restarting Claude Code (dda4cd6)
- scripts: assert content in verify:mcp and gate its own falsifiability (8c79454)
- server: ship the cpm CLI as a second bin (f9d9ec2)
- server: unify contract-surface vocabulary on framework, add recurrence guard (6b5b27a)
Changed
- Downstream marketplace sync no longer hardcodes the license: the
sync-downstreamjob stamped.license = "AGPL-3.0-only"ontominipuft-plugins'marketplace.jsonas a literal, so it would have re-applied the pre-2.1.0 license on every release and silently reverted the MIT change above. The license is now read from.claude-plugin/plugin.json(one source of truth) and asserted in the job's validate step. - Repository housekeeping: removed
.actrc(no workflow, script, or doc referenced it) and.nvmrc(no machine consumer;.node-versionis the pinned file and is read bysetup-nodeand the extension build). Publish workflows now resolve Node vianode-version-file: '.node-version'instead of five hardcoded24literals. Theremotion/product-demo tree moved out of the repository into a standalone local project, ending its dependency-bot churn; its commitlint scope was retired with it. - Single active Renovate configuration: the repo carried both a bare root
renovate.jsonand a tuned.github/renovate.json5. Renovate resolves the root file first, so the tuned config — including thezod <4andexpress <5holds — had never taken effect. Confirmed by a--dry-run=fullagainstmain, which reported it would ensure a dashboard titledDependency Dashboard(the default) rather than the📦 Dependency Updates Dashboardthe json5 specifies. The root file is removed. Because the surviving config had never executed, it was audited before activation rather than trusted:- Dropped
baseBranches: ["main", "develop"]→["main"]; nodevelopbranch exists on the remote. - Dropped
postUpgradeTasks; it requires self-hosted Renovate withallowedPostUpgradeCommandsand is inert on the hosted app. - Dropped the
enabledManagers: ["npm"]allowlist entirely. A dry-run comparison showed activating it as written would have silently stopped updates for thedockerfilemanager (server/src/Dockerfilebase images) andnodenv(.node-version— the file every workflow now resolves Node from). Auto-detection is what had actually been running. - Set
vulnerabilityAlerts.minimumReleaseAge: null, overriding the global 3-day soak. With Dependabot security updates disabled, Renovate is the sole remediation path and CVE fixes must not wait three days. Dependabot alerts remain enabled — Renovate reads that feed to detect vulnerabilities and would go blind without it. - Automerge ships off pending one review cycle now that the config is live for the first time.
- Dropped
- Re-licensed from AGPL-3.0-only to MIT to match the MCP ecosystem default and unblock corporate adoption. This reverses the 2.0.0 change that moved the project from MIT to AGPL-3.0-only. Network use of modified versions no longer triggers the source-disclosure obligation of AGPL Section 13; the MIT terms impose only attribution.
LICENSE,server/package.json,manifest.json, and.claude-plugin/plugin.jsonnow all declare MIT, as does the defaultlicensefield stamped on skills exported byskills:export— regenerate exported skills to pick up the new value. Production dependencies were audited before the change: 109 MIT, 33 Apache-2.0, 14 BSD-3-Clause, 12 ISC, 2 BSD-2-Clause, 1 Python-2.0, and zero copyleft. %leanno longer keeps methodology-scoring gates:%leansuppresses the methodology system prompt, so the gates that score adherence to it are now withheld too — today that isframework-compliancealone. Non-framework gates continue to run under%leanexactly as documented; the correction is narrower than the previous wording suggested. Same applies to%cleanand to a prompt-levelsystem-promptopt-out.%judgeforces the methodology in and therefore keeps the gates.framework_gates: falsenow takes effect: the prompt-level opt-out was read from a field that had five readers and no writer, so it did nothing. It now withholds methodology gates across every tier, including registry-activated ones. A prompt already setting it will see those gates disappear — which is what the option has always promised.gateConfiguration.excludenow applies to registry-activated gates: excludes were honoured during planning and then silently undone when the same gates were re-added by category activation. One veto set now covers every tier.db_reader.pymigrated tov_execution_statusSSOT view (Tier 4): Python hook now reads chain state from the cross-language SSOT view introduced in Tier 1, using the canonicalrun_statuscolumn (Tier 2) for boundary detection. Falls back tochain_sessionsper-row table, thenchain_run_registryblob, for backward compatibility during rollout (Tier 10 will retire the blob fallback). Hook output shape unchanged — existing chain-stop integration is preserved.- Consolidated four KV-blob tables into shared
kv_state(SCHEMA_VERSION15 → 16):framework_state,gate_system_state,argument_history, andresource_hash_cacheare now rows in a singlekv_statetable keyed on(tenant_id, key).SqliteStateStoreConfiggains an optional discriminatorkeyfor shared tables.state.dbis ephemeral, so the schema bump auto-recreates on next server start with no migration burden. Drops 4 tables and 5 indexes. - Atomic dual-write to chain registry + hook view:
persistSessions()now wraps thechain_run_registryblob write and the derivedchain_sessionsprojection inside a single transaction so the two can never diverge. RenamedsyncToSessionTable→projectToHookViewto reflect thatchain_sessionsis a read-only projection of the registry blob. SqliteChainRunRegistryclass removed: dead code with zero consumers. SingleDirectChainRunRegistryimplementation remains.- Command tokenizer refactor: Replaced duplicated operator detection across 3 parsing strategies with a single-pass, quote-aware
tokenizeCommand()functioncommand-parser.ts: 771→710 lines; symboliccanHandlereduced from 20 lines to 1; gate/framework/style stripping regex (~25 lines) replaced bytokens.promptId/tokens.rawArgsparser-utils.ts: 198→156 lines; removedhasOperatorOutsideQuotesandstripFrameworkOperatorOutsideQuotes(zero consumers — tokenizer subsumes)- Strategies now consume
TokenizedCommandinstead of re-detecting operators:canHandle(command, tokens)readstokens.formatandtokens.hasSymbolicOperators - Eliminates the class of bugs where special characters inside quoted arguments (e.g.,
"R3F + Visx","modes: (1)") triggered false operator detection
- chains: rename ChainSessionManager identifiers to ChainSessionStore (6f9428a)
- chains: retire StepState enum for StepLifecycle + StepMilestone (d617330)
- config: separate authored framework settings from the resolved view (0bc61f8)
- execution: extract shared process utility with POSIX signal interpretation (465bf53)
- frameworks: dedup and disambiguate colliding framework types (0b9d8c2)
- frameworks: delete the enableArgumentSuggestions flag (4738763)
- frameworks: move methodology-named files and directories (12d2470)
- frameworks: remove the deprecated methodology field from definitions (bb1f590)
- frameworks: rename internal methodology identifiers to framework (4c49340)
- frameworks: rename methodology to framework in comment prose (ffd1033)
- frameworks: rename the 16 exported Methodology* symbols (7d16376)
- frameworks: retire FrameworkDefinition.methodology (a5ef404)
- frameworks: unify methodology vocabulary on framework (0393797)
- gates: rename gate source methodology to framework-guide (4c83c66)
- mcp-tools: delete the system_control tool-description sink (0ad5769)
- mcp-tools: rename resource_type value methodology to framework (916b61c)
- parsers: centralize operator detection in single-pass command tokenizer (1dab41b)
- prompts: rename create_methodology to create_framework (7d1c32e)
- remotion: rename methodology to framework in the tutorial video (5808785)
- resources: consolidate write paths, remove dead JSON format (4e8bdf6)
- resources: rename methodologies resource dir to frameworks (98b1fd9)
- server: delete dead barrels and compat aliases (837d847)
- server: name the script-tool filter for triggers, not the retired mode field (3ef5411)
Deprecated
inline_gate_definitionswill begin executing in the next release — behavior change, action may be required. A prompt'sgateConfiguration.inline_gate_definitionsblock has never executed: every consumer to date was display or analysis. Per ADR 0001 (d), the next release registers these definitions and schedules them as real gates, controlled bygates.executeInlineGateDefinitions(defaultfalsein this release,truein the next).- Why you may care: a prompt in your workspace that declares inline definitions — possibly written before they were inert, or copied from the
create_promptscaffold — will start enforcing them. The bundled corpus is unaffected (no bundled prompt configures gates this way), but workspaces overlaid viaMCP_WORKSPACEcannot be inventoried from here. - What to do this release: watch the server log for
Dropped inline gate definitionwarnings (new below) and check any prompt named there. Setgates.executeInlineGateDefinitions: trueto opt in early and verify behavior before the default flips. - When armed, definitions resolve at rank 60 (
prompt-config) — below a caller-supplied gate, removable by a prompt-levelexclude— and a definition whose ID matches a registered gate overrides that gate's body field by field, with arrays and objects replacing wholesale rather than merging.
- Why you may care: a prompt in your workspace that declares inline definitions — possibly written before they were inert, or copied from the
Fixed
- ci: keep the Docker build working now that server/ builds the cpm bin (1bb6f6e)
- ci: make CI enforce what the repo already checks, and stop sync-downstream shipping broken lockfiles (708c81b)
- ci: make sync-downstream regenerate lockfiles and assert npm ci (dbba335)
- ci: make the new CI jobs work in a clean runner environment (809089b)
- ci: stop prettier reformatting the generated gate index (499c04e)
- ci: stop validate:format failing on tool-generated files (#159) (ef3d39c)
- config: sync config.json and its schema to the framework rename (39db875)
- contracts: correct two tool descriptions that named an invalid value (436e2d5)
- docs: repair nested code fences in the first-prompt tutorial (ffef5d7)
- frameworks: reconnect frameworkGates — the YAML rename orphaned it (280603e)
- frameworks: repair five methodology-vocabulary defects; finish the rename (8a547d9)
- frameworks: unstick @deprecated from version, lay out tier 4.3 (9d8cbe2)
- hooks: filter deleted files from pre-push prettier check (5ee1fb2)
- hooks: make every local gate a strict subset of CI (3924713)
- hooks: remove stale hooks-state.db fallback from ralph-stop (2be57ab)
- hooks: use --diff-filter=ACMR instead of shell workaround (edb9526)
- mcp-tools: rename the methodology creation param to framework (e2b632c)
- parsers: quote-aware operator detection prevents special chars in args from breaking prompt resolution (0beb3ff)
- repoint the cpm CLI at resources/frameworks — Stage 3a broke it (19f9d71)
- scripts: make the action-metadata guard able to fail again (95fa1cb)
- server: fail fast instead of hanging when an SSE session cannot attach (dc83489)
- server: preserve resource ids containing a slash when detecting removals (126a037)
- server: set rootDir so the published types entry resolves (741a384)
- server: survive EPIPE when a shell_verify child ignores stdin (5bb0d05)
Documentation
- add F5b — hooks diverge from the repo's own hook standard (bf93ca7)
- add the CI enforcement and import-alias plan (6022bb0)
- add Tier 5 (contract surface + guard) and Tier 6 (dead options) (5d74253)
- audit the 62 compat sites and classify each by verdict (c0dc894)
- close out sweep stage 3 and record its deliberate exclusions (2693bfc)
- close plan row 3.9 — the guard now proves it complete (a16d021)
- close the shim-debt sweep with Tier V, E2E and ORD outcomes (0503463)
- correct the doc instructions that no longer work (62897a0)
- correct the plan's own counts after tiers 1-3 (4f9bc66)
- correct version-storage reference, record shim-debt sweep (94fa37d)
- drop dead CLI flags and env vars from server README; fix barrel rule (d25b323)
- finish removing the path-override surface that does not exist (2a3084b)
- make the docs-vs-parser gap a CI failure instead of a recurring bug (0e76c03)
- narrow F1b — path filters apply to the PR diff, not the push (db5eb40)
- open Tier E2E and Tier V for the two findings this sweep created (90bf97c)
- prompts: correct the allowedValues deprecation notices (ae6da7f)
- reconcile stale tier status prose against the row marks (3315b05)
- reconcile Tier 3 and Tier 5 plan rows against measurement (1497b4f)
- reconcile two stale plan rows against what actually happened (f452463)
- record Stage 4 outcome — three live regressions, guard blocked (b8a8b17)
- record sweep tiers 3.1-3.9 and the StepState migration (8f3731e)
- record that Tier 1 is stacked on the shim branch, not cut from main (2471d28)
- record the bb1f590a follow-up outcome (ef4aa75)
- record the tier 4.3 outcome and the fourth falsified verdict (41c84e0)
⚠ BREAKING CHANGES
- server: the project is re-licensed from AGPL-3.0-only to MIT. Network use of modified versions no longer triggers the source-disclosure obligation of AGPL section 13; MIT imposes attribution only. Skills already exported by
skills:exportcarry the old AGPL-3.0-only license field and must be regenerated to pick up MIT. - mcp-tools: resource_manager parameter
methodologyis nowframework; FrameworkCreationData.methodology removed andtypeis now required. - frameworks: FrameworkDefinition.methodology removed; use
type. - resource://methodology/ is now resource://framework/.
- prompts:
>>create_methodologyis now>>create_framework. - config: the config.json section
methodologiesis nowframeworks. Existing files are migrated in place on load; the legacy key is ignored after that and can be deleted. - frameworks: a framework.yaml with only
methodology:and notype:no longer loads. All bundled definitions already declared both, so nothing shipped requires an edit; a hand-authored workspace definition may. - mcp-tools: resource_manager(resource_type: "methodology") is now resource_type: "framework". Existing version history for frameworks is discarded by the schema recreate rather than migrated; this is accepted for a pre-release project. The 'switch' action remains valid only for this type, now under its new name.
- resources: workspace overlays under MCP_WORKSPACE/resources/methodologies/ will no longer resolve and must be renamed to resources/frameworks/ with their methodology.yaml renamed to framework.yaml. This fails silently - the resource simply stops being found - rather than raising an error. No overlay was present in this environment at the time of the rename.
- chains: SCHEMA_VERSION 15 -> 16. Persisted step state values
renderedandresponse_capturedno longer exist and substate_json changed shape, so the first server start after this drops and recreates state.db. Any in-flight chain session is lost; run it between chains rather than mid-run.
Maintenance
- server: re-license from AGPL-3.0-only to MIT (07f2f0a)
2.1.0 (2026-03-19)
Added
- Multi-source resource overlay: Custom workspace resources now load alongside bundled ones for all resource types (prompts, gates, methodologies, styles). Set
MCP_WORKSPACEto a directory withresources/subdirs — custom resources with the same ID as bundled ones take priority- Methodologies:
additionalMethodologiesDirsinRuntimeMethodologyLoader(mirrors gates pattern) - Styles:
additionalStylesDirsinStyleDefinitionLoader(mirrors gates pattern) - Prompts: Overlay merge in
data-loader.ts— loads primary then overlays per workspace dir - Gates: Already supported (unchanged)
- Methodologies:
- Wide-event root span enrichment: Pipeline root span (
prompt_engine.request) now contains 22 business-context attributes at completion, following the wide-event pattern- Performance:
stages.slowest,slowest_ms,executed_count,duration.total_ms,had_early_exit - Gates:
gates.names,passed_count,failed_count,blocked,retry_exhausted,enforcement_mode - Chain:
chain.is_chain,chain.step_index,chain.id - Framework/scope:
framework.id,framework.enabled,scope.source - Error:
error.typefor groupable incident triage - Enables incident queries like "show blocked requests by gate name" or "which stage is the bottleneck"
- Performance:
- Response Format Overlays: Methodologies and styles can define
responseFormatin YAML to guide LLM response structure at the tool description level- Methodology
responseFormatwoven into tool descriptions at synchronization time (global) - Style
responseFormatavailable for per-execution system prompt injection
- Methodology
- ci: auto-merge manual changelog entries into Release Please releases (d2f2f52)
- runtime: multi-source resource overlay and path consolidation (2f5d751)
- server: add OpenTelemetry instrumentation and observability infrastructure (48e720f)
Changed
- BREAKING: Path resolution consolidated to
MCP_WORKSPACEas single source of truth. Individual per-resource env vars (MCP_PROMPTS_PATH,MCP_METHODOLOGIES_PATH,MCP_GATES_PATH,MCP_STYLES_PATH,MCP_SCRIPTS_PATH) and CLI flags (--prompts,--methodologies,--gates,--scripts,--styles) removed- Migration: Use
MCP_WORKSPACEwith standardresources/subdirectory structure, orMCP_RESOURCES_PATHfor custom resources base - PathResolver
get*Path()methods unified via sharedresolveResourceSubdir()helper - Per-loader
resolve*Dir()methods simplified to package.json + __dirname fallback only (env var handling removed) module-initializer.tsunconditionally initializes singletons with PathResolver-resolved dirs
- Migration: Use
- Prompt management consolidation: Migrated all prompt lifecycle operations from standalone
prompt_managertool to unifiedresource_managerwithresource_type:"prompt". This completes the tool consolidation started in v1.2.0.- All 12 prompt actions preserved:
create,update,delete,list,inspect,reload,analyze_type,analyze_gates,guide,history,rollback,compare - Internal architecture improved with service decomposition:
PromptLifecycleService,PromptDiscoveryService,PromptVersioningService - No API changes required—use
resource_manager(resource_type:"prompt", action:"...")as before
- All 12 prompt actions preserved:
- Client launch preset expansion: Extended
--clientstartup presets to includegemini,opencode, andcursor(in addition toclaude-code,codex,unknown) and wired delegation strategy routing for each profile. - Delegation strategy hardening: Centralized delegation profile metadata for CTA/footer rendering, added Codex fallback guidance when
spawn_agentis unavailable, and marked Cursor delegation messaging as experimental/testing. - Tier 5 File Size Decomposition: Three oversized files decomposed to meet 500-line service advisory
loader.ts(896→544): Extracted markdown parsing tomarkdown-prompt-parser.ts; consolidated ~30 verbose info-level logstool-description-loader.ts(741→489): Extracted methodology/style overlay resolution totool-description-overlays.tsfile-operations.ts(517→345): Removed 3 dead diagnostic methods with zero consumers
- MCP Tool Schemas: Hand-written Zod schema factories replace codegen
mcp-schemas.ts, enabling methodology-aware description overlays without generated code - Operator Patterns: Loaded from JSON registry at import time via esbuild inlining, eliminating the
generate-operatorscodegen step and Python hook codegen - Style Guidance: Served exclusively from YAML definitions via StyleManager, removing hardcoded legacy fallback
- hooks: improve Python hook type safety and reduce pyrefly baseline (69cc281)
- runtime: replace ServerRootDetector with resolvePackageRoot() (3c2bd7f)
- server: decompose Tier 5 oversized files to meet size advisories (adbc670)
- server: enforce architecture boundaries via DatabasePort injection (5b39be0)
- server: replace codegen with hand-written schemas and resource-driven overlays (84b74cf)
Removed
- Legacy
prompt_managerMCP tool: Prompt lifecycle now exclusively viaresource_manager. The standaloneprompt_managertool registration has been removed.
Fixed
- Prompt update field clearing: Sending empty strings (e.g.,
system_message:"") now correctly clears the field instead of silently preserving the old value. Update handler migrated from||fallback to!== undefinedpattern matching the methodology handler
- ci: add checkout step before changelog merge in Release Please workflow (d68d0f1)
- ci: centralize downstream version sync in extension-publish (e8c25e0)
- ci: fix changelog merge target and set release-as 2.1.0 (cc2ed76)
- deps: update dependency chokidar to v5 (b10476f)
- hooks: register delegation-enforce.py in PreToolUse hooks (0c7c3a4)
- hooks: resolve generated operators Ruff typing issue (a956fb2)
- hooks: use SSOT registry for operator detection in prompt-suggest hook (2fe7a4f)
- hot-reload: support chokidar 5 upgrade (1db39c8)
- mcp-tools: fix prompt update field clearing and simplify update workflow (5a2800e)
- mcp-tools: remove section/section_content from router pass-through (61072df)
- parsers: strip leading delegation operators before argument extraction (07ed2ee)
- scripts: check dependency range instead of package version for opencode (3f3fa9e)
- scripts: generate Ruff-compatible Python operator types (aa201d7)
- scripts: update extension deps list and lint ratchet baseline (bffac0d)
Improved
- Prompt update workflow for LLMs: Update fields directly —
update(id, description:"new")— only provided fields change, omitted fields are preserved. Tool description now includes a compact UPDATE hint for discoverability - Update handler maintainability: Replaced 8 individual if-checks with
UPDATE_FIELDSmap loop for field-level overrides. Adding new updatable fields is now a single map entry
Documentation
- README install sections: OpenCode and Gemini CLI sections restructured with Option A/B (plugin vs manual config), correct config formats (OpenCode uses
mcpkey withcommandarray), and Gemini hooks prerequisite added. Fixed> [!NOTE]callouts not rendering inside<details>blocks - Custom Resources section: Documented
MCP_WORKSPACEoverlay behavior, removed false~/.local/share/claude-prompts/persistence claim, added per-client config examples withMCP_RESOURCES_PATH, added--initworkspace creation workflow - Telemetry observability guide: Restructured attribute reference into Initial/Wide-Event/Other sections with incident query examples per attribute. Fixed chain events incorrectly documented as active (now marked Planned). Updated architecture diagram to show wide-event enrichment flow.
- CONTRIBUTING.md modernization: Restructured contributor guide with quick-start path, contribution type routing (code/prompts/gates/methodologies/docs), commit scope reference, testing decision matrix, and progressive disclosure via collapsible sections
- GitHub issue and PR templates: Added YAML-based issue forms (bug report, feature request) with project-specific dropdowns (transport, MCP tool, area), preflight checkboxes, and structured fields following Next.js/Vite/Claude Code conventions. Minimal PR template complements existing CI
pr-summarybot
- cleanup: record chokidar post-upgrade rationale (b6740e2)
- modernize CONTRIBUTING.md, add GitHub templates, align project config (3afbe39)
- record open PR validation wave (81d383a)
- record package wave results (0e7de90)
- record remaining package wave (d398fe5)
- remove orphaned Unreleased section from pre-v2.0.0 changelog (73ad697)
- standardize inline doc links with TIP callouts across README (ebd6241)
- update changelog for unreleased changes (25c659e)
- update demo video plan to WebP format and re-recording schedule (b87aef7)
⚠ BREAKING CHANGES
- runtime: Individual per-resource env vars and CLI flags removed. Use MCP_WORKSPACE with resources/ subdirectory structure instead.
2.0.0 (2026-03-11)
⚠ BREAKING CHANGES
- server: License changed from MIT to AGPL-3.0-only. Network use of modified versions now requires source disclosure under Section 13 of the GNU Affero General Public License v3.
- server: All runtime-state paths require explicit PathResolver configuration. Users running via npx must provide --workspace or set MCP_WORKSPACE. Storage backend migrated from JSON files to SQLite — downstream readers of state files must use SQLite.
- paths: All path-dependent modules now require explicit path configuration. Callers must provide paths via PathResolver or CLI flags.
Added
- ci: add commitlint, changelog-sections, and downstream sync workflow (802575d)
- eslint: add claude-plugin custom ESLint rule (876b431)
- gates: add response blocking and gate event emission in pipeline (914a074)
- hooks: add server-side hook registry and MCP notification system (86ba115)
- parsers: add framework-aware quote parsing for @ operator (9555122)
- scripts: add skills-sync CLI for cross-client skill distribution (351291c)
- server: add identity resolution, delegation operator, and methodology assertions (#76) (913c2d9)
Fixed
- ci: align extension-publish tags with Release Please config (19a0024)
- hooks: allow generated file deletions for feature removal (a8fcb24)
- hooks: update Python hooks for new gate server format (1b0ddf5)
- parsers: simplify argument assignment for unstructured text (061cd0f)
- pipeline: use provider function for prompt cache synchronization (f092837)
- runtime: bridge PathResolver to jsonUtils via PROMPTS_PATH env var (3d4505b)
- tests: set PROMPTS_PATH in test setup for template rendering (34769d7)
Changed
- gates: consolidate gate verdict validation to single source of truth (0ae1ae9)
- gates: extract gate-activation utility and cleanup dead code (85bd265)
- mcp-tools: consolidate prompt_manager into resource_manager (6a41a52)
- parsers: simplify argument matching and remove dead code (3befb9f)
- paths: enforce explicit path resolution, remove process.cwd() fallbacks (b93ca78)
- prompt-guidance: remove unused resource selection code (a2da026)
- remotion: replace demo compositions with Liquescent design system (b06706b)
- runtime: migrate CLI argument parsing to node:util parseArgs (71dbe00)
- server: complete modular monolith migration to 5-layer architecture (31d3884)
- server: relocate tooling/ submodules and consolidate pipeline imports (5204a7a)
- types: consolidate context types and add gate response contract (aa51202)
Documentation
- add LIQUESCENT methodology and update changelog (26a639b)
- changelog: document breaking path resolution changes (5918e16)
- ci: update downstream sync comment to reflect Dependabot approach (82e15cd)
- consolidate documentation and remove completed plans (1e52295)
- update path configuration and release process documentation (abf3457)
Maintenance
- ci: downgrade to minor bump — path resolution change is internal only (c550b74)
- ci: release as 2.0.0 — AGPL license change is breaking (7e5d024)
- server: migrate license from MIT to AGPL-3.0-only (36961fa)
1.7.0 (2026-01-23)
Features
- ci: migrate to OIDC trusted publishing for npm (e71d272)
- config: add registerWithMcp toggle for MCP resources (471ed14)
- config: expand resources config with granular per-type controls (ddcdba2)
- docs: add Remotion animation system for documentation videos (0a40c4d)
- extension dep sync, repetition operator, hook fuzzy matching (efbdc30)
- hooks: add chain step visibility with IDs for workflow preview (ff25d5e)
- hooks: improve prompt_engine directive clarity and token efficiency (b62f8d3)
- hooks: validate operator values against registered server resources (30cba3a)
- resources: add MCP logs resources for runtime observability (5f0025f)
- resources: implement MCP Resources protocol for token-efficient access (80d56d2)
Bug Fixes
- parsers: preserve arguments after * N repetition operator (649bae3)
1.6.0 (2026-01-22)
Features
- ci: modernize Release Please and npm-publish workflows (7bb1303)
Bug Fixes
- ci: handle Release Please PR output as JSON (8559c74)
- tests: update E2E plugin validation for current structure (053b0be)
1.5.0 - 2026-01-21
Added
- Hook-level fuzzy matching: Unknown prompt interception before tool call for token efficiency
>>unknwon_prompt→ Hook returns suggestions immediately (no server round-trip)- Same multi-factor scoring algorithm as server-side (prefix: +100, word overlap: +30/word, Levenshtein: +50-distance*10)
- Saves ~50-100 tokens per failed prompt attempt
- Resource change tracking: Audit log for prompts and gates with source attribution
- Tracks filesystem hot-reloads, MCP tool operations, and external changes
- Content hashing detects actual modifications (skips no-op saves)
- Baseline comparison at startup surfaces changes made while server was down
system_control(action:"changes"): Query the audit log with filters (source,resourceType,since,limit)
Changed
- Error messages: Condensed parsing errors for token efficiency
- Before: Multi-line verbose messages with format examples and hints
- After: Single-line messages with fuzzy suggestions only when relevant
- File watching: Migrate from
fs.watchto Chokidar with automatic polling for WSL2/network filesystems- Auto-detects WSL2 environments and enables polling mode
- Configurable via
usePolling('auto' | true | false) andpollingInterval(default: 300ms) - Fixes hot-reload not working in WSL2 due to virtualized filesystem limitations
Fixed
- Resource ID extraction: Baseline comparison now correctly extracts prompt/gate IDs from directory names (was incorrectly extracting "prompt" from nested file paths)
- Command parsing: Comprehensive improvements to command parsing robustness:
- Bare prompt names: Accept
strategicImplementwithout requiring>>prefix - Double-encoded JSON: Handle nested JSON strings from MCP clients that double-escape payloads
- JSON-wrapped chains: JSON strategy now properly delegates to symbolic parser for chain commands (e.g.,
{"command": ">>analyze --> summarize"}) - Argument syntax: Gate operators (
::and=) no longer conflict with argument assignment (input="value")
- Bare prompt names: Accept
- Fuzzy prompt suggestions: Multi-factor scoring replaces simple Levenshtein-only matching:
- Prefix matching (score +100):
ana→ suggestsanalyze_code,analyze_data - Word overlap (score +30/word):
code→ suggestscode_review,analyze_code - Typo correction: Dynamic threshold based on query length (longer queries allow more edits)
- Limited to 3 suggestions: Reduces noise while maintaining relevance
- No arbitrary examples: Completely unrelated queries show no suggestions instead of random prompts
- Prefix matching (score +100):
1.4.5 - 2026-01-20
Added
- Chain workflow preview: Hooks show all chain steps before execution begins (e.g., "1/4 Initial Scan → 2/4 Deep Dive → ...")
- scaffold_project chain: Interactive project scaffolding for TypeScript, Python, or hybrid projects with modern tooling
- Nested chain discovery: Chain steps can reference prompts in subdirectories (e.g.,
scaffold_analyzeunderscaffold_project/) - System-only prompts: Prompts with only system message (no user template required)
- Operator patterns generated from single source (TypeScript + Python via generate-operators.ts)
Changed
- Symbolic parser with unified pattern matching across all operators
- Hook configuration now loaded from
config.jsonvia config_loader.py - Move detect-skills.py to global ~/.claude/hooks (not plugin-bundled)
1.4.4 - 2026-01-19
Added
- Include hooks directory in npm package distribution
1.4.2 - 2026-01-18
Changed
- Migrate Gemini-specific hooks to gemini-prompts repository
1.4.1 - 2026-01-17
Fixed
- Remove submodule path checks from distribution validation
1.4.0 - 2026-01-16
Added
- Global version integrity check across ecosystem (npm, marketplace, extensions)
Changed
- Migrate documentation guides to Diátaxis framework
1.3.2 (2026-01-14)
Bug Fixes
1.3.1 (2026-01-14)
Bug Fixes
1.3.0 (2026-01-14)
⚠ BREAKING CHANGES
- Complete MCP server restructure with new consolidated API
Features
- add Claude Code plugin for /install-plugin support (c3b5654)
- add dev:claude script for --plugin-dir workflow (2a7d6f8)
- add marketplace.json for plugin distribution (bf79fcb)
- add streamable http transport and release automation (0717f31)
- Add symbolic command language and operator executors (639f86a)
- dist: separate public and private prompts for distribution (fee7c12)
- enhance README with interactive prompt management features (13afaa9)
- enhance server startup options and help documentation (f315f33)
- gates: implement intelligent gate selection with 5-level precedence system (b8e1c11)
- gemini: add Gemini CLI extension support (50587c6)
- gemini: align hooks with Claude plugin infrastructure (c35e4f0)
- hooks: auto-regenerate contracts on source change (9b21afa)
- implement enterprise-grade CI/CD pipeline with comprehensive testing framework (#10) (25e7f59)
- implement Phase 1 - Enhanced Category Parsing System (1506755)
- multi-platform extension support with enhanced hooks and gate system (27b94ec)
- parser: enhance case-insensitive prompt matching and add strategic implementation (628b09c)
- plugin: add server/resources directory for plugin deployment (2d21a86)
- plugin: add SessionStart hook for dependency installation (3896fb9)
- plugin: persist user data outside cache directory (e59c64b)
- plugin: unify plugin with YAML resources, version history, and script tools (0963d4a)
- ralph: context isolation for long-running verification loops (f1c1014)
- re-introduce hot-reloading for prompots (720f96b)
- release 1.2.0 with CI validation and gate refactoring (75ec3e8)
- shell verification presets and checkpoint resource type (ca24027)
- styles: add style operator (#) for response formatting (d2c3173)
- update documentation for version 1.1.0 - "Intelligent Execution" (7e1fb3f)
Bug Fixes
- add missing nunjucks dependency for template processing (4f78114)
- add required metadata for plugin menu navigation (72cd845)
- ci: consolidate workflows and fix Docker paths (42efb12)
- ci: skip action inventory verification for bundled builds (dd299e5)
- ci: update extension-publish workflow for bundled distribution (8d977e9)
- clear lint regressions (77db53a)
- contracts: add prettier formatting to contract generator (6096c67)
- contracts: format all generated TypeScript files (0e4c9af)
- correct marketplace.json schema (source not path) (6d08e63)
- correct source path (relative to marketplace.json) (e281b48)
- deps: add missing 'diff' dependency for text-diff-service (12b70db)
- docker: update Dockerfile for renamed docs and styles directory (35144c8)
- docs: remove invalid color property from mermaid linkStyle (d086ca1)
- gemini: align extension with Claude plugin v1.1.1 (fb3413c)
- gemini: resolve symlinks before path calculation (77da3bc)
- hooks: add quick-check mode to prevent SessionStart blocking (c4b3f94)
- hooks: make dev-sync portable across environments (4aa1190)
- npm: Include methodologies folder in published package (9bad683)
- plugin: correct .mcp.json schema and improve dev workflow (9927529)
- plugin: include server/dist for plugin installation (5f1edd3)
- plugin: remove duplicate hooks reference causing load failure (c6c3ca6)
- prune useless prompts (daec104)
- regenerate contract artifacts and update lint baseline (0a0e67d)
- release: correct release-please config paths (20662ec)
- Remove accidentally committed node_modules, update .gitignore (ed452ac)
- return simple text responses for MCP tool visibility in Claude Code (7980567)
- source must start with ./ (b2482f1)
- source path relative to repo root (f2e3020)
Miscellaneous Chores
- prepare 1.3.0 release (225ebe6)
1.2.0 - 2025-01-12
Added
- CI validation and gate refactoring improvements
- Release automation in CONTRIBUTING.md
1.1.0 - 2025-01-10
Added
- Initial public release with MCP server, prompts, gates, and frameworks