agnidia/corpflow
Staged worktask pipeline with a durable state ledger, cross-plugin delegation, and dependency-DAG orchestration.
Changelog
All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[4.1.3] — 2026-10-19
Changed
- Medium and Moderate worktasks include finalization and stakeholder verification (#489).
- The sibling plugin template documents test-run evidence, security escalation, and class re-emission rules (#491).
Fixed
-
Bind parallel issue hooks to the assigned workspace and derive audit stages from dispatch evidence (#496, #500).
-
Size stage briefs to turn limits, resume partial returns, and prevent vacuous verification greps (#498).
-
Resolve the declared PR base, correct finalization preflight checks, and settle issues after non-default-base merges (#482, #497).
-
Keep unattended decisions, child-agent completion, CI settings, and portable command handling consistent (#499).
-
Preserve spilled escalation classes, normalize decision inputs and previous-stage IDs, and settle stale tasks from their touched files (#484).
-
Serialize DV streams in pinned sessions while retaining unpinned worktree isolation (#489).
-
Report measured self-test counts and corroborate runner summaries, including here-document command classification (#487, #500).
-
The DC option gate no longer checks released history or vendored docs (#475, #492).
doc-option-check.shscans aCHANGELOG.mdonly inside its## [Unreleased]block. It skips integrity hashes such assha512-<base64>and backticked tokens whose first segment exists nowhere in the tree. The new repeatable--exclude <path>skips a vendored doc. The git-ignored runtime hint now prints a path that works as--allow-pathfrom the tree root.
[4.1.2] — 2026-10-18
The suite runs green again (#483, #488), the cli-fallback render no longer needs a font registry,
and the marketplace id moves from igrsoft to agnidia. The repository also stops tracking
.claude/settings.local.json, and the adhoc self-test keeps a caller's EXIT trap.
Changed
- The marketplace id is now
agnidia(wasigrsoft), so the install key iscorpflow@agnidia. An existing install keeps the oldigrsoftmarketplace and thecorpflow@igrsoftkey. To migrate in Claude Code, run/plugin marketplace remove igrsoft, then/plugin marketplace add Agnidia/corpflowand/plugin install corpflow@agnidia. Then rename thecorpflow@igrsoftkey tocorpflow@agnidiain everyenabledPluginsmap that names it. In Codex (steps verified on Codex CLI 0.163.0), runcodex plugin remove corpflow@igrsoftandcodex plugin marketplace remove igrsoft, thencodex plugin marketplace add https://github.com/Agnidia/corpflow.gitandcodex plugin add corpflow@agnidia. .claude/settings.local.jsonis no longer tracked. It is per-operator Claude Code state, so.gitignorenow lists it. A pull of this change deletes an unmodified tracked copy; save your copy first to keep itsenabledPluginsand permissions.
Fixed
- The cli-fallback ImageMagick render works without a font registry.
cli-fallback.shpassed-font Courier, which only resolves through fontconfig, so a stock Homebrew ImageMagick 7 failed every render withrender_failed. It now asks for Courier by name only whenmagick -list fontlists it, else uses a monospace font file (Menlo, Courier, DejaVu Sans Mono), else no-font. adhoc-visual-evidence.sh --self-testkeeps a caller's EXIT trap. The self-test sets its own EXIT, INT and TERM traps to remove its scratch directory. If the shell exits during the test, the caller's EXIT handler now runs after that cleanup with the real exit status. When the test ends, the caller's traps come back exactly.
Tests
web-capture.batsT3 and T4 hideplaywrightfrom PATH, so a host Playwright no longer takes the npx-rung capture or fetches the test URL (#483).- The adhoc-visual-evidence self-test owns its PATH: it filters the host's
silicon,magickandconvertout and uses a PNG-writing stub, so a broken host tool no longer picks or fails an arm (#483). - The
size-budget.batsoversize cases hidepngquantthrough the--hideallowlist farm instead of cutting PATH down to the system bin directories, so a distropngquantthere no longer turns the "no pngquant" case into "pngquant present". - The
handoff-harness.batsfrontmatter-budget case reads the git-trackedtests/fixtures/worktaskartifacts, not the gitignored.context/run folder of the checkout (#488).
[4.1.1] — 2026-10-09
One CORPFLOW.md can now configure every project. This release also records contract changes
that shipped on develop without a changelog entry; sibling-plugin CORPFLOW.md files that target
an earlier corpflow should resync against them.
Fixes the prompt-audit findings and comment gaps of #458, a reader-side root-resolution flaw, the
FN/ST predecessor edges of #467, the writer's share of the reader guard (#468), the planner-owned
screenshot flag (#470), and the lessons adopted from mattpocock/skills 959a8e9..4588b32.
Added
-
Fewer full-artifact rewrites and a bounded DC path loop.
hooks/anchor-preflight.shno longer denies a whole-file Write. Frontmatter over budget, a digitlesssummary_line, a status-note sweep stub and an off-list H2 now come back at PostToolUse, one line per finding, with "fix it with a small Edit, do not re-Write the file". An Edit that adds an off-list H2 is still denied, and the stage-boundary harness still gates. Every stage agent's "Artifact anchors" block states the 200-token budget and the 2–4 option sweep rule.doc-option-check.shgains a repeatable--allow-path <path>for paths the code creates at run time, and a stderr hint for a git-ignored missing path. technical-writer DC2 gets a stop rule: one failed fix, then evidence and--allow-path, never a deleted correct claim, at most 3 runs. -
writing-styleskill andskills/shared/writing-style.mdcanon. Prose rules derived from ASD-STE100 at about 80%: single-topic sentences (procedural ≤20 words, descriptive ≤25), active voice, imperative steps, articles kept, one term per meaning, and tables or diagrams over prose for structure. technical-writer and the context-compression handoff rules cite it. -
## Merge dangerPR section. The PR format (skills/shared/git-conventions.md) and the FN template and checklist (conductor-attachments.md) carry a reversal class (one-wayortwo-way, with its reason) and a blast radius.## Changesleads with the smallest visual, and## Test planshows the failing-then-passing check or the changed output: "tests pass" alone is a claim.pr-body-lint.shP4 warns on a missing section (exit 0 unless--strict). -
Enforcement form for self-improvement proposals. Each ST proposal is
mechanical(a check: lint rule, bats test, hook, or the wiring of an unwired one) orjudgement(prose; a standards rule targets the reviewer first). No new rule targets an always-loaded file, and each proposal cites its diff hunk. Approvedmechanicalproposals go to workflow-engineer,judgementones to prompt-engineer. -
Two
skill-refs.batspredicates. One names an unquoted top-level frontmatter value that contains:; the other names aSkill(call into a skill markeddisable-model-invocation: true. -
User-scope
CORPFLOW.md. corpflow reads${CLAUDE_CONFIG_DIR:-$HOME/.claude}/CORPFLOW.mdas a fallback for## Routingand## Models. Precedence is per heading: the project-root file wins for any heading it carries, the user-scope file supplies the headings it lacks, and the built-in matrix covers the rest. A garbled project## Modelsstill owns its heading: it is auditedmodel_override_unparsedand never falls through to the user file; a bare project heading with nothing under it does not claim the heading. An explicitstate-patch.sh --resolve-models --corpflow <path>still beats both. -
model-matrix-lib.shgainscorpflow_md_locate,corpflow_md_user_pathandcorpflow_md_source.state-patch.sh --resolve-modelsandmodel-matrix.sh's bare--resolveshare them, in place of two copies of the project-root path logic. -
New source value
user-overrideforstate.models_source, per-agentstate.models.*.sourceandstate.routing_source. Themodel_override,model_override_unknownandmodel_override_unparsedaudit rows carrymetadata.sourceandmetadata.path. -
consultant-return.v1(backfill). DR and SR sibling-plugin consults return one shape, checked byskills/cross-plugin-handoff/scripts/validate-consultant-return.sh: severities are lowercase, a missingversionis stamped, a mismatched one is rejected, andneeds_changesmaps tofail. -
Reserved
## Modelsheading (backfill).skills/shared/stage-codes.md § Agent Model Matrixis the single agent-keyed model/effort source. ACORPFLOW.md § Modelstable overrides it row by row, fail-open, resolved once bystate-patch.sh --resolve-models(validation check 13) intostate.models. A plugin-rootCORPFLOW.mdmust not carry## Modelsor## Routing.
Changed
-
Requires Claude Code 2.1.294 (was 2.1.284). 2.1.293 resolves
haikuto Haiku 5.5, the model the cost table now prices, andheadless-dispatch.shfloors at 2.1.294. 2.1.292 adds theAgenttool'seffortparameter, which every in-process stage dispatch now passes. A cross-session message held at the recipient for approval was reported as delivered before 2.1.288; it now reads as not delivered and names the holding session. Background commands in unattended sessions stop at theirtimeoutsince 2.1.285, which the explicit 2 htimeoutbelow depends on. -
Stage effort is carried in-process. Step 6 passes the stamped tier on
Agent({ effort })(neweffort_transportvalueagent-param), which outranks the agent'seffort:frontmatter;effort_resolvedcomes from the subagent's hook row.CORPFLOW_HEADLESS_ROUTE=onsends every non-haiku stage headless instead; haiku stages pass noeffort(none). Breaking:effort-route.shdrops--agents-dir,--role-baselineand thebaseline/baseline_sourcefields, andheadless-dispatch.shdrops--baselineand theopted_outfallback (CORPFLOW_HEADLESS_ROUTE=off). The headless child gets the route's tier from--effortalone, notCLAUDE_CODE_EFFORT_LEVEL, so a resolver bump reaches it; awarnfallback runs in-process at the same tier (agent-param,effort_resolved: null,inproc_fallback). A refused route escalates instead of dispatching without the tier, and the PL0 and self-improvement dispatches pass their resolvedefforttoo. -
Nested delegates get the stage tier. A nested
Agentcall withouteffortruns at the target's own frontmatter tier. DV, QA, SR and RE now pass their brief'seffort:line, whichbrief-compose.shemits besidemodel:, to the agent doing the stage's primary work; consults keep the target's own tier. -
Audit hooks read effort from the payload only.
audit-tooluse,audit-subagentandagent-stopdrop the$CLAUDE_EFFORTfallback: on haiku the payload has noeffortbut the env var keeps the requested tier, so rows recorded a tier that never ran. They now write"unknown", which the resolvers treat as no row. -
Task→Agent. Grants, hook matchers, docs and pseudo-code name onlyAgent. Hook payloads name the spawn toolAgenteven when aTaskmatcher fires, sotest-execution-gate.sh'sTask)branch never matched and wrote notest_delegation_observedrow. The gate now branches onAgent), and the Codex adapter emitsAgent. -
Prompt-audit sweep (
/prompt-audit --all, 17 critical, 49 warnings). Grant gaps closed: project-manager runsfn-preflight.sh, team-lead runsjq, developer may dispatchgo-developer, tech-code-review may write its review artifact. Wrong references fixed (ST-SI,TL3,release.md, stream-blinddevelopment-<N>.mdclose-outs, lowercase self-improvement commit subject)./prompt-auditAgent rule 1 now requireseffort:equal to the matrix tier, and/create-agentscaffolds it plus the matrix row. Branch-only blocks move behind references:worktask/references/{headless-arm,return-arms,step-7a-arms,autonomy-preflight,auto-decision, megatask-per-issue,dr-reference}.md,agent-coordination/references/cross-session-messaging.mdandshared/technical-consult.md. Commands follow Options → Examples → Output Format with a Values column and defaults; inflatedBINDING/MUSTemphasis and default-behaviour sections are removed; designer writes.context/designs/ux-assessment-N.mdfor PM to merge. -
CC 2.1.285→2.1.292 band integrated. The reattach result table and
stale-check.shhandleheld(stay parked, never re-send, the operator approves at the named session). Headless-dispatchMonitor(Bash(...))calls and QA's direct-run fallback pass an explicittimeout, so an orchestrator running under-pno longer cuts a stage off at 30 min; a stop at the time limit is an infrastructure stop, not a test failure. Docs pick up fail-closedPreToolUsematching, teammateagent_id/teammate_id, worktree access fixes, the 1M default on gateways and cloud providers, new Bash and Read permission hardening, one-shot-pwaiting for background commands and wakeups, escaped<system-reminder>tags in hook output, UNC-read andallowed-toolsfixes,claude plugin testfailing on a failedexpect, and stdio MCP protocol 2026-07-28. -
CC 2.1.293→2.1.294 band integrated: Haiku 5.5.
haikunow prices at Haiku 5.5 ($0.10 / $0.50 / $0.01 cache read, 1/20 of sonnet). A prompt over 100K tokens bills 5x that. The Sonnet 5.5 cache-read rate is corrected to $0.10.estimate-calc.pyfollows the table. The benchmarkDCpin moves toclaude-haiku-5-5, which is a new era boundary. The README no longer says Haiku sees theTaskCreatetools: Haiku 5.5 does not. The docs add these changes:- instruction-style
prompt/agenthooks now block (2.1.294) subagentStatusLineagentType- rules loaded on a Bash
cat/head/tail/sed -n/grepview - no
SendMessageprompt once the tool is removed - narrowed child tool lists
claude agentsbypass consent- the compaction self-action fix
- instruction-style
-
The planner now owns
requires_screenshots.detect-ui-change.shis advisory input: the planning model judges whether the diff alters rendered output, S1 (ui_visual_check) is a hard floor, afalseover an S2 hit needs a written reason, uncertain meanstrue, and the planner never raises a screenshot question. The user-directive override is removed from the PL procedure, product-manager runbook, plan template and ledger schema. -
Detector S3 matches framework terms only.
view,component,screen,layout,theme,styling,animation,HTMLand bareComposeno longer fire it. New--ui-platformsoption shares the admitted platform set; bare.ktleaves the shared UI path vocabulary. -
DV upward-only safety net. New
skills/dv-screenshot-capture/scripts/escalate-flag.shraises afalseflag totrue(task then ledger, onescreenshot_flag_escalatedaudit row) when the change set, including uncommitted and untracked paths, matches the UI path classes on apple/web/android. It never writesfalse;agents/developer.mdruns it before capture, andhooks/dv-screenshot-gate.shruns it (--invoker gate, 10 s watchdog, fail open) on every live DV SubagentStop whose flag readsfalse, so the net also covers DV agents that never read developer.md. An escalated block opens withgate raised the planner's requires_screenshots=false: <n> UI path(s) matched in <ws>. -
Constraints blocks, all 16 agents. Each
## Constraints (DO NOT)bullet now takes the form its failure needs (agents/prompt-engineer.md § Form to failure): aDO NOTwith a short because only where an agent knows the rule and skips it under pressure, otherwise a positive recipe or a conditional keyed to something observable. Bullets the model already obeys are gone, and unearnedMUST/NEVER-style emphasis is downgraded. The heading text is unchanged. -
Completion criteria name their evidence. Stakeholder, QA, project-manager, architect, team-lead, ethics-reviewer, developer, incident-responder and release-engineer checkboxes now name the artifact file and anchor that proves them. incident-responder gains a
## Completion Verificationsection and a done-when column for IR0–IR3. -
Sibling dispatches forward
[4b]. The QA, SR, AR, RE and DV dispatch sections follow the plugin-root line with the model discipline block (skills/cross-plugin-handoff/SKILL.md § Model discipline block). -
FN multi-stream arm moved from
agents/project-manager.mdintoskills/worktask/references/fn-multi-stream.md; the agent keeps a one-line pointer. -
Audit commands require
effort:.prompt-audit,optimize-agentandcreate-agentflag only amodel:frontmatter key;effort:is required and equals the agent's matrix row. -
state-patch.shnow shares the reader guard (corpflow_inferred_ctx_ok, #468); its exit-4 refusal no longer names the plugin root path. -
team-lead stream rules. A shared registry file (changelog, string catalogue, config map) belongs to one stream, or the streams are serialised; every new name two streams share is fixed in the interface contract.
-
DR re-review is scoped. After a rework round, technical-lead checks the fixed findings and the rework diff only; a new P1 or P2 outside it becomes a follow-up, and only a new P0 reopens review.
-
A skipped test is not a pass.
skills/shared/testing-strategy.md § Skipped tests: runs report their skip count, a skip caused by material a worktree lacks is recorded with its cause, and.worktreeincludeis the remedy. -
Self-improvement proposals reach the agent their form names.
/worktaskPhase 3, the FN-gate Post-ST step and/improve-yourself --applysendjudgementitems to prompt-engineer andmechanicalitems to workflow-engineer, one dispatch per form, in sequence. workflow-engineer gains a Self-Improvement Patch Application section. An item with no validEnforcement:value (missing, or other thanmechanicalorjudgement) is not applied; it is counted as skipped and named to the user. -
One version-bump rule for applied proposals.
skills/self-improvement/SKILL.md § Hand-off to the applying agentstates it once: a target with frontmatter takes oneversion:bump, a script, hook or bats file takes none. The same section says that amechanicalproposal passes the Step 4 owning-file filter first and then targets a check file./improve-yourself,/worktaskPhase 3, the FN-gate Post-ST step and workflow-engineer point to it. -
stakeholder names both applying agents for approved proposals.
-
One plugin-paths sentence in all 16 agents. Each
## Plugin pathssection now reads "Everyskills/,commands/andhooks/path here", byte-identical across the agents.skills/worktask/references/pl0-procedure.mdnames the same three prefixes for its paths. -
The five scripts this release changes pass shellcheck at default severity with no output:
attach-visual-evidence.sh,adhoc-visual-evidence.sh,pr-body-lint.sh,pr-body-lint-selftest.shandhooks/dv-screenshot-gate.sh. Their behaviour is unchanged. -
Per-task screenshot manifests (backfill). Each DV task writes its own
.context/images/<worktask_id>/screenshots-<TASK_ID>.md.dv-screenshot-gate.sh --check <TASK_ID>andattach-visual-evidence.sh --validate-manifest --task-id <TASK_ID>validate one task's rows against the images on disk. -
DV fan-out writes per-row artifacts (backfill). Each DV ledger row owns the artifact its
metadata.artifactnames,development-<N>-<stream>.mdwhen there is more than one row, and completes withstate-patch.sh --task-id <ID> --artifact <path>. A baredevelopment-N.mdremains the single-DV case. -
Install-root dispatch line (backfill). A plugin-root
CORPFLOW.mdnames the plugin's loaded install root in every dispatch prompt, so the specialist resolves paths under it rather than searching for them. -
AR and SR consult gate at High (≥31) (backfill). The AR architect consult moves from ≥21 to the High floor, and SR consults the platform security auditor only for a sensitive surface or a High score, recording a skipped consult otherwise.
-
Docs:
routing-matrix.md § Resolutionand its three-kindCORPFLOW.mdtable,PROJECT-CORPFLOW.md(valid at user scope; reads both## Routingand## Models, not one heading),skills/cross-plugin-handoff/templates/CORPFLOW.md,plugin-contract.md, validation checks 12/13 and the README describe the user-scope fallback.
Fixed
- A count-less summary line no longer proves a test count for a counting runner.
handoff-harness.shonly warned when a corroboratedsummary_linedid not carry its entry'scount, soBUILD SUCCESSFUL in 4scould stand in for "58 tests ran". It now fails the entry for the strict runners:bats,pytest,jest,vitest,swift-testing, and everygradle*orjunit*name, matched case-insensitively. A failing pytest line, which prints no total, passes when its executed outcome counts sum tocount.bash-*live scripts,xcodebuildand other runners keep the warn and thecount_corroborationaudit row. The newskills/worktask/scripts/junit-tally.sh <results-dir>prints one count-bearing line from a JUnit XML directory (JUnit XML tally: N tests executed, F failures, E errors, S skipped, N = tests minus skipped), and the fail message names it for Gradle and JUnit.stage-contracts.md(#tpl-dv,#tpl-qa), developer and qa-engineer say the same. - Every worktask self-test now prints how many cases it ran. The pass lines
(
self-test: ALL PASS,desc-lint self-test: ALL PASS,self-test OK, …) had no digit, so the harness digit rule refused them as asummary_line. Each of the 21*-selftest.shand inline--self-testprinters inskills/worktask/scripts/andhooks/state-merge.shnow appends(N passed, 0 failed). N is a counter that goes up once per case that passed, never a constant. Counting self-tests print(P passed, F failed)on failure, and exit codes do not change.state-merge.shrepeats the count of thestate-patch.shrun it delegates to, and fails if that line has no count. The old text stays as a prefix, so existing matches still hold. - A sweep item's
classcan no longer be lowered fromescalate.state-patch.sh --factsjoinedclasslast-writer-wins, so a stage re-run could writedecisionover an item another labeller raised toescalate. The ledger then agreed with the artifact, and the harness passed. Nowclassis raise-only ondecision < escalate, and a stub with no class keeps the incumbent's. A refused downgrade exits 2. It prints one line per id on stdout and stderr and writes onefacts_items_rejectedaudit row. The rest of the payload still lands. Ledgers that agree today keep passing. After a refusal, an artifact stub that still saysdecisionfails--validate-frontmatterat the next boundary. That failure is a real defect: raise the stub toescalate.#tpl-srand#tpl-renow make a question that offers to accept a known vulnerability, CVE or security findingclass: escalate.#tpl-ststates that ST never resolves anescalateitem. apple-swift-matchchecks a minimum, not equality. It passes on anyswifton PATH at or aboveCORPFLOW_SWIFT_MIN(default 6.3.0). The compare is numeric per field, and the detail names both versions whenxcrun swiftdiffers. Aswiftthat cannot run fails with the probe's first line (home path shown as~). The fix line isswiftly use --global-default --assume-yes xcodeor a PATH change; without--assume-yes, swiftly waits on a prompt. The check id is unchanged.- The Step 2a-pre snippet exits 0 on a clean pass. It grepped for
result=, which the script prints only on a skip, so a pass ended the tool call with exit 1. It now printsresult=<pass|skipped>fromresult_json, keepsreason=andaccepted_absent=, and exits 0. - PL0 gets the full task. Step 4 calls
descriptiona capped label, never the planner's input. The new § Step 6 — the planning prompt names the minimum: the task word for word (the issue body under/megatask), the resolved flags and carriers, and theWORKSPACE_ROOT=banner. product-manager plans from that prompt. A 908-char task had reached PM cut at 240 chars. state-patch.shreports a description cut. It still truncates and never refuses. After the write it printsdescription_truncated=<id>:<length>on stderr, on create and on meta.--task-meta --unset <key[,key...]>removes metadata keys. Before,--set '{"k":null}'was the only option, andhas("k")stayed true. It runs after the merge under the same lock. An absent key is a no-op. A pipeline key (stage,agent,model,effort, the three gates,workspace_path,isolation,base_ref,requires_screenshots) exits 2 and names the key.- Speed-over-exploration clauses removed from product-manager, technical-lead and stakeholder.
- team-lead clones PL0's resolved model/effort row instead of picking tiers; project-manager's
stray
F3step (the label means fallback layer 3 elsewhere) is renamed. - workflow-engineer names the six sanctioned ledger-row writers, and its State Patch uses the
--task-id <ID>/tasks.<ID>form. - The handoff edge table gains
QA→REfor a standard, secure or full run that excluded DC, and release-engineer's<PREV>rule matches it. prompt-audit's recall-suppression citation points attech-code-review § Phase 1.- FN and ST predecessor edges (#467):
QA→FNwhen DC and RE are skipped,DC→FNonly when DC ran and RE did not, andRE→ST/DC→ST/QA→STwhen FN is skipped. - Ledger readers (
corpflow_context_dir,mailbox-lib,brief-compose, the model-switch hooks) no longer hand a cwd nested inside the plugin checkout that checkout's live.context; they refuse like the #457 writer does. A linked worktree at its toplevel still resolves the main ledger (#458). - The screenshot-gate block text names the Bash entry point
(
skills/dv-screenshot-capture/scripts/capture.sh --task-id <id>) instead of a skill route DV does not hold; thehook-monitoring.mdexample follows. - The
writing-styleskill description fits the 250-character cap. The Core rules section ofskills/shared/writing-style.mdis split under two sub-headings to fit the 1000-character section cap; no rule changed. - The 0600 mode tests in
tests/shell/skills/append-labels.batsandtests/shell/skills/pipeline-counts.batsno longer fail on macOS, wherels -lappends@for extended attributes. The scripts were already correct. tests/selection/matrix.tsvrow R17 listsskills/shared/writing-style.mdwith the other reviewed prose docs that have no executable contract, so test-selection M5 passes again.- The
## Visual evidenceblock goes between## Test planand## Merge danger, the order inskills/shared/git-conventions.md.skills/dv-screenshot-capture/SKILL.mdno longer names## Notesas the end point. - The header comments of
attach-visual-evidence.shandadhoc-visual-evidence.shnow say that the Visual evidence block goes between Test plan and Merge danger. - Doc references that did not resolve now pass the doc-option check: team-lead's path-character
regex is anchored, technical-lead cites
skills/worktask/SKILL.md, the settings file is named per project, and the git-conventions branch examples move out of code spans. No rule changed.
Tests
tests/lib/test_helper.bashpointsCLAUDE_CONFIG_DIRat an empty sandbox for every suite, so an operator's real~/.claude/CORPFLOW.mdnever reaches a test. The Python parity suite pins it the same way.
[4.1.0] — 2026-09-28
Added
- Portable Agent Plugins and Codex compatibility manifests, Codex-native hook registration, and
same-named
$skilladapters for all 28 existing slash-command workflows. - A Codex runtime contract mapping Corpflow agent orchestration and model tiers to Codex controls:
haiku →
gpt-6-luna, sonnet/opus →gpt-6-sol, and fable →gpt-6-astra. - Provider-neutral
BASE_PLUGIN_ROOTandBASE_PLUGIN_DATAresolution, including safe plugin-path joining and support for portable, Codex, and Claude package markers. skills/shared/plan-content.md: one canonical plan-content rule ported from superpowers v6.4.2. A plan records decisions (files, signatures, the spec's values, test assertions), not a transcript of the code; it adds a step scan and a proportion check to self-review./prompt-auditgains a planning-stage plan-content rule and a consistency check that flags unreferenced asset files underskills/for deletion.
Changed
- PL0 (
pl0-procedure.md, product-manager), AR (software-architector), TL (team-lead) andrequest-planpoint atplan-content.mdby section instead of carrying their own wording: each names the decisions its artifact records, and DV writes the implementation bodies. - technical-writer: a project keeping its guidelines in
AGENTS.mdgets aCLAUDE.mdthat imports them with@AGENTS.md, since Claude Code skipsAGENTS.mdwhenever aCLAUDE.mdexists. - Live benchmark: both arms also switch off the
plugin-authoring@builtinplugin that Claude Code 2.1.289 ships, so the WITHOUT arm again loads zero plugins.
Compatibility
- Claude Code commands, agents, hook registration, and legacy
CLAUDE_*boundary inputs remain supported. Codex-specific behavior is isolated in additive manifests, adapters, skills, and hook configuration.
[4.0.32] — 2026-09-11
A control that fails silently is indistinguishable from a control that passed, and an evidence token that asserts more than it observed is worse than no token — the theme 4.0.31 opened, applied here to the completion loop itself, two continuity mechanisms that existed but were never wired, and the ledger lock's ownership.
Correctness pass across agent, command and skill surfaces: seven documented defects fixed (divergent per-stage dispatch table, unchecked parity assertion, duplicated routing glob, missing flag documentation, stale section citations, lint regressions, and stale version line), two parity test methods added, and five distinct stale citations across eleven occurrences repaired. Eleven follow-up items filed out-of-scope.
Breaking
test_summary_lineis now required of DV and QA artifacts whenevertests_executed > 0.handoff-harness.sh --validate-frontmatterfails an artifact reporting executed tests with no verbatim runner summary line to corroborate the count. No migration: an artifact that already carries the line is unaffected.- A present but non-numeric
tests_executednow blocks at the same gate, closing the gap where a claimed execution count was accepted without ever being checked. oracle.cases_digestmoves tosha256:80652591(benchmark eval harness). Records either side of the retiering that fixed six mistiered cases and closed the era-stamp gaps refuse to pair;era.prompt_contractis unchanged. Unreleased since it landed, folded in here rather than shipped silently under a patch line that never mentioned it.
Added
- A terminal
failedtask status. Escalation counts are tracked per full task id (escalation_counts); at the cap the task is writtenfailedwithlast_error.class: "exhausted". Joinscompleted/skippedinSETTLED, so the loop exits instead of spinning on a stage it can neither settle nor dispatch. Additive — existing ledgers load unchanged. PostCompactandSessionEndregistered inplugin.json.PostCompactdrivespost-compact-recovery.sh, recurrence-guarded by a new inverse manifest-parity assertion, but the live firing is not yet observed — a real compaction cannot be simulated in CI.SessionEnddrives the newhooks/session-end-finalize.sh: reports any task stillin_progressat teardown to an audit row, never mutates status, always exits0.- The ledger lock carries an owner token and refuses a foreign release, rather than silently freeing a lock another writer still holds.
- Session hygiene and command output hygiene guidance.
cost-optimization § 4dcovers noisy command output, which stays in context and is re-billed every turn; token-baselines addsBASH_MAX_OUTPUT_LENGTH,/autocompact,/mcp,/contextand/rewindvs/compact; context-compression gains a copy-paste "Compact instructions" block;/worktaskdocuments fresh-session,/rename+/clearand separate-session/loophabits. Docs only.
Changed
-
A per-dispatch effort raise or lowering now reaches the session, instead of staying an advisory audit field.
skills/worktask/scripts/effort-route.shdecides, per dispatch, whether the stamped tier differs from the target agent's owneffort:frontmatter tier; a difference routes headless through the newskills/worktask/scripts/headless-dispatch.sh(claude -p --agent … --effort, argv built from an allowlist, never a shell string), andskills/worktask/scripts/headless-poststop.shreplays the installedplugin.jsonSubagentStophooks the headless child never fires itself, so a headless DV row gets the same gates, state merge and audit trail an in-process one would.effort_transportgains a fourth value,env, for an operatorCLAUDE_CODE_EFFORT_LEVELpin that outranks a routed raise (skills/shared/stage-contracts.md § Resolver Effort Tier,commands/worktask.md § Step C.0a). Some stages now run at a raised or lowered tier where they previously ran at the session default; no version-floor change (--agent,--effortand--permission-promptsalready exist at 2.1.280). -
/cc-update0.3.0: two new standing passes — a Communication Surfaces Watch (cross-session, cross-agent, cross-plugin entries routed to their owning docs; the four "re-check at the next/cc-update" obligations closed as confirmed/unconfirmed each run; min-CC rule stated) and a Ledger Field Review (every new CLI flag, frontmatter key, tool param, orclaude agents --jsonkey decided as New field / Existing field / Row key / None againsttask.metadataand the headless flag bridge). Both run by default on every invocation. Version Source now names the real README carriers (badge line + Requirements row) instead of aclaude-code min version:token that never existed; thecurl/jqfetch fallback andclaude agents --jsonare granted inallowed-tools. -
Requires Claude Code 2.1.270 (was 2.1.251). The resume loop now trusts signals older builds got wrong: a reattach
okcould land in a phantomListAgentstwin (2.1.260), a send to an offline remote peer read as delivered (2.1.261), and a headless session running background agents reported "waiting for your input" (2.1.269)./megataskfan-out also relies on concurrent sessions no longer reverting~/.claude.json, which reset workspace trust (2.1.259). 2.1.270 rather than 2.1.269, which regressed read-only git commands into permission prompts. -
CC 2.1.252→2.1.270 band integrated. The
SessionEndhook entry carries an explicit 5 stimeout; the reattach result table handlesqueued(stay parked, never re-send); PL0 raises adecisionitem whenCLAUDE_CODE_SUBAGENT_MODEL_FORCEwould override every stage model pin. Docs cover Fable 5.1,maxEffortLevel, the 2.1.270claude agents --jsonkey set, permission-mode CLI choices,plugin validate/list --json, and a security hardening table.claude agents runis not a subcommand at 2.1.270; recorded as drift, the headless dispatch one-liner is a follow-up.
Fixed
-
The pre-compaction checkpoint hook no longer leaves an unconditional copy unguarded under
set -eu; a failed checkpoint now records the failure instead of leaving no trace. -
The compaction recovery script's two output paths are rooted on the same project-directory variable as its sibling script, and its header no longer claims a filtered selector it does not run.
-
estimation-methodology: a synthetic asset no longer downgrades the complexity tier it is scored against. -
benchmark eval harness: oracle discrimination restored — six mistiered cases moved from
impliedtospecified, four newimpliedcases added,failures[]now persists into the record instead of being dropped at write time, andcli_version/plugin_versionare stamped as a new era axis. -
request-plan: four review findings closed on the leaderboard fix plan.
-
headless-dispatch reference: per-stage effort table now matches
stage-codes.mdcanon on all rows (DV/DR corrected fromxhightohigh, QA fromhightomedium, RE frommediumtolow); module citation corrected tostage_table.py; four missing rows (PL, AR, TL, DC) added; test-pinned advisory cell preserved byte-for-byte. -
Parity test enforcement:
test_stage_table_ssot.pyextended with two new methods (test_efforts_match_stage_codes_mdandtest_headless_dispatch_table_matches_stage_codes) to catch effort column divergence on future edits; existing model-family test retained unchanged. -
Single-source-of-truth routing glob: restated verbatim in
agents/workflow-engineer.mdreplaced with a pointer to its canonical source inskills/worktask/references/pl0-procedure.md. -
Pipeline flag documentation:
--with-designnow appears in bothcommands/worktask.mdoptions table andskills/shared/state-ledger.mdschema, closing documentation gap where flag was live in README and procedure but absent from owning command's table. -
Section-length lint regressions: two branches-introduced sections split or trimmed under 1000-character cap (
commands/prompt-audit.mdandskills/worktask/references/handoff-protocol.md). -
Stale cross-file section citations: five distinct broken references repaired across nine files, including resume-procedure path correction, ceiling-count update, auto-delegation file/anchor fix, escalation-guard heading refinement, and four bare references/ path qualifications.
-
Release-tooling version line:
MEMORY.mdversion record updated to 4.0.33 with current-branch status corrected.
Also in this release
Batch 7 of the request-plan calibration set was pinned and its two defects repaired
(request-plan, 45 cases, 18 held out, cases 271–315) between the 4.0.31 tag and this one; it
carries no user-facing behavior change and is recorded here only because it landed in the same
unreleased window.
Eleven follow-ups filed out-of-scope (F1–F11 in the architecture stage artifact): redundant-but-agreeing
mirrored tables pinned by parity tests, compound section-path citation convention, with_design writer
gap, per-stage table's missing-row note, and three cross-file reference style questions.
[4.0.31] — 2026-09-09
Closes phases A–E of ttt-run-tictactoe-multiplatform-leaderboard-2026-09-09-fixplan.md — 21 of
the run's 26 findings. One theme runs through all of them, and it is the one 4.0.30 started: a
control that fails silently is indistinguishable from a control that passed, and an evidence token
that asserts more than it observed is worse than no token. Phase F targets a separate repository;
Phase G is filed upstream, not fixed here.
Breaking
tests_executedis a required DV frontmatter field, andtest_suite_compilesis required alongside it whenever the count is0. A DV artifact without it failshandoff-harness.sh --validate-frontmatter. No migration: the field records something no reader could previously derive.- A frontmatter block with no
handoff:key is refused bystate-patch.sh, which previously accepted a flat top-level shape through its awk fallback. An artifact with no frontmatter at all keeps the F3 fallback unchanged — only the malformed shape is newly refused.
Fixed
- F-01 — the dedupe gate no longer suppresses against a prior that executed nothing.
tests:0, the newdiscovered:<n>token,unrecordedand any unparseable evidence now allow and writetest_dedupe_skipped_zero_prior. This is the P0: a scheme with an empty test plan enumerated 49 cases, executed none, recordedtests:49, and then refused every later attempt to run them — so a platform reached a merge decision with zero tests executed. - F-01 —
test-execution-promote.shdistinguishes discovery from execution. A count with no outcome vocabulary anywhere in the runner output is recorded asdiscovered:<n>, nevertests:<n>.executedcounts as outcome vocabulary;executingdeliberately does not. The vocabulary is matched against the string leaves of an object response, never its serialised keys, so a response carrying anerror: falsefield cannot pass an enumeration off as a result. - F-02 — every denial names the controls still in force. The dedupe denial previously named neither the authority holder nor the resolved test mode, which is why four stages escalated in one run each proposing a remedy a different arm would have refused again.
- F-02 — DV records
tests_executedand, at zero,test_suite_compiles. Compilation is checkable without test-execution authority, and it is the datum separating gate-blocked from never built — a distinction that stayed invisible for ten hours. - F-04 — the
facts.decisions[]eviction spill has a reader:state-patch.sh --read-decisionsreturns ledger ∪ spill, ledger winning on conflict, a parse error or a symlinked spill being a failure rather than an empty set. Twelve decisions written for one task now all resolve; four were previously lost in silence, three of them cross-client parity controls. - F-04, F-15 — a partial
--factswrite records its casualties durably: rejected ids reachaudit.jsonlasfacts_items_rejectedrather than stderr alone, and the post-write check now distinguishes evicted to spill (recoverable) from lost (in neither place). The old message asserted eviction unconditionally, and on the run that produced this finding it named four ids a later write had restored while four different ids were the ones actually gone. - F-05 — one frontmatter reader,
skills/worktask/scripts/frontmatter-lib.sh, shared byhandoff-harness.shandstate-patch.sh. The two previously disagreed about what a valid artifact looked like, so a flat-shaped artifact was unreadable to the boundary gate and still wrote a healthy ledger row. - F-06 — the second transport is now a literal command in every sweep-obligated agent.
qa-engineer.md,project-manager.md,stakeholder.md,team-lead.mdandethics-reviewer.mdcarried no--factsinvocation at all, so every sweep item those stages emitted was structurally invisible to the FN gate — the mechanism behind this run nearly losing its only blocker. Bare "both transports" phrasing is gone from every shipping surface. - F-09 — the DC contract requires cross-stream claims to be marked
consistency-checked, not executed: under fan-out DC reads a tree that does not exist yet, and three of four documented paths were absent from the tree it was reading. - F-08 — security and networking claims in payload documentation must cite the stage and
evidence that verified them, and QA treats an unverified one as a finding. A README asserting
loopback binding shipped over a database on
*:5433with a default credential and passed developer review; only a live probe caught it. - F-10 —
base-sanitygains amulti_parent_payloaddegrade rung. Under fan-outfacts.files_modifiedcannot be the union of independently-staged streams, so the rule blocked by construction on every fan-out — 140 PR files against 19 ledger files, cleared only with the sanctioned override. - F-11 — DV claims the ledger as its first write after the worktree pin (§ D0.0b), generalizing
the
MANDATORY CLOSEpattern that previously existed only intechnical-writer.md. Three of four streams hit their turn ceiling with nothing staged, no artifact and the ledger stillin_progress; all three carried a degradation order and ran out before reaching it. - F-12, F-13 —
§ Step B.1routes any non-zero harness exit as amissing_inputcontract violation, with the named shapes demoted to an illustrative table, and states plainly that shape failures short-circuit so a fix may reveal further failures. The previous text enumerated three shapes and promised one-shot batching that shape-dependent checks cannot honour. - F-14 — the AR-reference warn distinguishes a malformed shape from a dangling file and names the expected bare form; both causes previously shared one message.
- F-16 — a non-canonical
--artifactname warns and names the canonical form.hooks/anchor-preflight.shfails open on an unmatched name, so such an artifact was written, ledgered, harness-passed and never anchor-linted — which hid two missing required anchors. - F-18 — orchestrator loop step 4.7a lands AR's untracked non-
.context/writes onto the ref DV forks from. AR holds no git grant, DV is inside the broken tree and FN runs last, so no stage could fix this; it has reproduced across three runs. - F-19 —
state-merge.shwrites onestate_merge_nooprow per run when a SubagentStop carries neither stage nor artifact; the sentinel is keyed onrun_index, so a later run in the same workspace gets its own row. Fail-open behaviour is unchanged; one run produced 335 of these and every sweep overaudit.jsonlreported that boundary clean. - F-20 — an empty
open_questionsnow requires the## elicitation-sweepheading. An empty array alone cannot distinguish "swept, nothing to ask" from "never swept", which is the rule's whole purpose. An absent field stays the required-field loop's single failure. - F-25 — the tree-fingerprint gap is documented accurately. Staged content is inside the digest, so committing is never needed to clear a denial; only untracked-file content is outside it. The finding's premise was wrong and is corrected rather than repeated.
Not changed, after checking
- The fixplan's proposal to add
stageandstatusto every agent's--factsstub was dropped:state-patch.sh's_sweep_defaultsalready derivesstagefrom the item id and defaultsstatustoopen, so the examples are correct as written and the addition would have been redundant tokens in eight agent files. agents/product-manager.mdwas not given a--factsexample: PL's payload is canonical inpl0-procedure.md § Union this stage's facts, which carries the literal command. The exemption is pinned by a test so it cannot decay into a hole.
[4.0.30] — 2026-09-08
Closes all thirteen findings from corpflow-fix-plan-2026-09-07.md across four parallel
development streams (dedupe/audit hooks, ledger-writer clamps, boundary harness, contracts/
commands/conventions/schema), plus gh#316. First authority-clean full-suite evidence of the
sequence: 1855 bats, all green, exit 0, 423 @test cases added over develop — earlier per-stream
tallies were taken mid-flight over a tree other streams were still editing and are not comparable.
Also carries a second parallel effort: closes all 26 prompt-audit findings (8 Critical, 18 Warnings) across 16 agents, 28 commands, and 23 skills (score 65/100). Two development streams addressed tool-grant coverage, normative corrections, and agent-template backfill. Full suite for this combined release: 1872 bats + 379 Python, exit 0.
Breaking
handoffsre-keyed<PREV_CODE>→<TASK_ID>. A stage split into four now writes four distinct edges instead of collapsing to one. No tolerant reader, no migration — readers must key on the new format.facts.decisionsandfacts.open_questionsclamps repartitioned per task (8 and 4 respectively), retiring the global rings of 8 and 12. A reader that assumed one shared ring across all tasks in a run will see a different item set than before. No migration.
Fixed
- F-18a —
dedupe_invocation's default arm now foldstool_inputinto the dedupe key, not just the tool name, closing the credential-in-arguments exposure (security finding T6). - F-18b — test-execution promotion is evidence-gated:
test-execution-promote.shderives one evidence token per run and a run whose evidence cannot be named is discarded, never promoted. - Phantom SubagentStop rows —
audit-subagent.shsuppresses and counts the ~31s-cadence repeats a dispatch fires, reporting them as onesubagent_stops_suppressedrow per window. The discriminator is a repeateddedupe_keywithin the trail: every stop the runtime delivers arrives with no stage and duration 0, so a predicate on those fields matched every genuine stop and emptied the trail — caught in review before release. - F-18c — denial messages now name the evidence they were rejected against, rather than a bare refusal.
- F-07a/b —
state-patch.shrejects--factsper item; the valid remainder persists and exit 2 fires only when something was actually rejected. - F-09 — the eviction spill covers resolved items, warns loudly on failure, and triggers off a live length comparison instead of a hard-coded bound.
- F-21a/c —
handoff-harness.shgains a reverse sweep-parity arm (a ledger id absent from the artifact frontmatter was previously invisible) and a decision-id divergence check. - F-06a/b/c — the decisions and questions ledger writers are re-partitioned per task id, closing the eviction-without-spill path that let one stage's writes silently displace another's.
- F-14 —
handoffsre-keyed<PREV_CODE>→<TASK_ID>(breaking; see above). - F-15b — the third-class sweep item now carries a concrete option set at the call site.
- F-13 —
plant()'s harness verification path fixed for the case this run's own review flagged (see Beyond-brief below for the residual gap). - F-17 —
cli-fallback.sh's.txtplaceholder floor replaced by loud failure instead of a silent stand-in. - F-10 —
attach-visual-evidence.shgains a distinctembed_capreason instead of an unlabelled truncation. - F-08 — four audit-log readers made tolerant and loud instead of silently aborting on the first malformed line.
- F-04a —
hooks/test-execution-gate.shgains ascoped_test_runarm on the Skill branch; a scopedbuild-testinvoked as a Skill was previously classifiedfull_test_runand denied regardless of its own--only/--filterscoping. The denial message now also names the resolvedtest_mode. - F-11a —
state-patch.sh's decision/question eviction spill covers resolved items (folded into the same fix as F-09's trigger correction). - gh#316, closed. The
facts.decisions[]clamp now spills to.context/decisions-<run_index>.jsonlon eviction — append-only, sibling of the existing questions spill, no reader by design (matches the questions-spill precedent). Listed as an open follow-up in the 4.0.29 changelog; closed here.
Prompt-audit remediation
Added
- Two new predicates in
tests/shell/skills/skill-refs.bats:ungranted_script_orders(Predicate A, 7 tests) detects execution orders without matching grants, covering interpreter-prefixed paths, plugin-root variables, and bare basenames;dangling_related_targets(Predicate B, 5 tests) resolvesrelated:entries and reports unresolvable ones. Both ship with planted-violation fixture trees and 2 repository-wide contract tests (14 named tests total, all green). - Agent template backfill:
commands/create-agent.md § Body sectionsgains required slots forExample InteractionsandConstraints (DO NOT)sub-sections; 15 agents backfilled with## Example Interactions(5–8 user phrasings each); 10 agents backfilled with### Rationalizationsand### Red Flags — STOPsub-sections. - Command structural gaps:
## Examplessections added toappstore(4 invocations),request-plan(4 per-class invocations),worktask(7 covering all unexampled options);## Output Formatsections added toappstore,megatask,worktask; completion criteria rewritten on 5 commands to name their artifacts. - Agent differentiation: two new
## Differentiation from Related Rolestables resolve description collisions betweenproduct-manager/stakeholderandteam-lead/project-manager.
Fixed
- Grant coverage (Critical C1–C3, C13): five worktask scripts (
branch-name.sh,refine-branch-target.sh,publish-pl-issue.sh,handoff-harness.sh,effort-ladder.sh) added tocommands/worktask.mdallowed-tools; three megatask scripts added tocommands/megatask.md;commands/request-plan.mdmatcher gainsbashprefix and:*;agents/designer.mdgainsToolSearchgrant with conditional step-1 delegation indesign-review.md. - Audit command write grants (Critical C2):
commands/prompt-audit.md,docs-audit.md,arch-debt.mdgrantedWrite, Editwith.context/audits/output paths;test-coverage.mdgrantedWrite; all four gain# tools:comments. - Tool-grant comments (Critical C13, Warning W19): nine bare
Bash/Taskgrants annotated with# tools:comments;appstore.mdnarrowed toTask(corpflow:release-engineer). - G3 invocation gate polarity (Critical C5):
agents/prompt-engineer.mdrestates G3 as "Does it lack standalone value…" (shared polarity with G1/G2); G4 tie-breaker re-anchored;skills/csv-export-templates/SKILL.mdandskills/preview-ensurer/SKILL.mdre-verified (both confirm pipeline-only viadisable-model-invocation: true);skills/dv-screenshot-capture/SKILL.mdscores G3 = no, gains flag. - Model tier correction (Critical C6):
commands/create-agent.mdandcommands/prompt-audit.mdchanged frommodel: sonnettomodel: opus. - Link resolution (Critical C7): seven
related:entries inskills/agent-coordination/SKILL.mdandskills/estimation-methodology/SKILL.mdrewritten to file-relative form (../worktask/SKILL.md). - Accessibility version (Critical C8): WCAG 2.1 → 2.2 across
agents/designer.md,commands/design-review.md,commands/design-accessibility.md. - Section ordering (Warning W22): example sections repositioned (W22) in
commands/milestone.md,ethics-review.md;tech-code-review.mdreordered with## Your Job (read before you review)preamble moved. - Argument-hint fixes (Warning W23):
milestone.mdgains|all,megatask.mdfixed to<N>. - Option examples (Warning W24): 13 new examples covering 16 previously unexampled options across 5 commands.
- Completion criteria (Warning W18): rewritten on
designer.md,product-manager.md,design-review.md,design-accessibility.md,design-specs.mdto name artifacts. - Disclosure (Warnings W11–W13): 15 agents gain
## Example Interactions; 10 agents gain constraint sub-sections;technical-writer.mdmoves "DO NOT omit examples" from prohibition to required template slot.
No code needed
- F-21b/d — the questions spill already existed since 4.0.29; the actual gap was that no command document read it. Fixed by documentation, not code.
- F-15a — the rejection message already named the enum; verified, not changed.
- F-15c — reopened at the user's request and reaffirmed as no-code:
defect/constraint/riskare observations without an option set, so they remain follow-ups rather than sweep items.
Beyond brief — defects this run discovered
Six defects surfaced while closing the thirteen findings, none in the original brief:
run-tests.shhas five widening paths to the full suite; only the weakest (WIDE) is guarded bydv_in_progress. A scoped invocation can self-escalate past DV authority through the other four. Deferred to its own worktask by user decision. Cost: until it lands, no DV full-suite evidence in any run is authority-clean.state-patch.sh --stage DV --task-id DVnwithout--artifactresolves to the stage's first instance, so a fan-out stream can silently inherit a sibling's artifact and summary.plant()proves something changed, not that every expression matched — a compoundsedpasses when only one half lands.cache-lint --anchor-lint's DV allow-list omits sectionsstage-contracts.md § DVrequires — two internal specs disagreeing with each other.scan-secrets.shexits 133 outside its documented{0,1,2}contract, bypassing its own fail-closed arm. QA established the trigger is cumulative (153 repo files plus one specific file), not the BSD-grep trap the script's own comment anticipates. No automated Pass was obtained for this run; SR's manual line-by-line pass over every added line compensates..context/state.jsonis 1859 tokens against a documented 500-token budget.
Process notes
- This run reproduced its own findings while fixing them. F-06 fired twice: AR0's eight
decisions evicted all five of PL0's with no spill and exit 0, and QA confirmed eight decisions
(
pd1–pd5,tl1–tl3) are gone from the final ledger because those stages wrote before DV1's rescope landed. This is not a defect in the delivered code — it is the self-hosting riskplanning-0.mdpredicted, and incidental proof the retired global ring was harmful. Those decisions are readable only fromplanning-0.mdandcoordination-0.md, not from the ledger. - Operator note: an operator with a stale installed
.claude/hooks/state-merge.shshould re-runhook-install.sh. That file is generated and gitignored; it was not edited by this change. - Post-review addition, user-directed and outside the finding set.
skills/worktask/SKILL.md § Dispatch on the same turn (BINDING)states that the orchestrator verifies a boundary and dispatches the next ready stage in the same turn, that a running backgroundTaskdoes not block it, and that only the plan gate, anescalate-class sweep item and the FN gate justify stopping mid-pipeline. It landed after DR, SR and QA reviewed the tree and after the version bump, so it did not go through the review the thirteen findings did. It is prose in an untested section — no code, test or other doc changed — so the suite evidence above still describes the shipped code exactly. - Six orchestrator rules moved from an operator's private config into the plugin, same
post-review status and for the same reason: a rule enforced from
~/.claude/CLAUDE.mdbinds one operator and nobody else, which is the finding class this release closes. Delegation-only and the build-tooling corollary →skills/worktask/SKILL.md § Delegation-only (BINDING); what counts as approval at the plan gate →commands/worktask.md § What counts as approval — and what does not; explicit per-stage model passing →model-selection.md § Worktask stages: explicit, never inherited; UI evidence from the running app →stage-contracts.md § UI evidence comes from the running app (tpl-dv); spec-outranks-git logand the no-AI-footer rule →git-conventions.md § This spec outranks git log. Prose only, in five files, reviewed by nobody but the user who directed them.
[4.0.29] — 2026-09-07
Three bodies of work, one still-unreleased version. A pull request opened against a branch the work never forked from
silently carries every commit of the intervening integration branch: a run whose own ledger
recorded 27 changed files opened a PR carrying 156 commits and 1008 files into develop, and every
existing finalization check passed — none compared the would-be PR diff against what the run itself
claims to have changed. Separately, thirteen remediation items from that same run's own findings
target a narrower theme underneath it: silent failure, writes that land nothing being
indistinguishable at the call site from writes that landed, and checks that pass vacuously. This
release adds the PR-diff comparison as a blocking check, surfaces fork-point evidence for
reconciliation rather than silent retargeting, corrects two documentation claims that made the gap
look covered, and closes out the remediation items — one of which was refuted on verification, three
of which shipped amended from how they were originally planned, and one of which was a fail-closed
false positive introduced by the fix itself and caught before it shipped. Dogfooding the new
base-sanity check during the remediation work itself turned up two more issues in the check, folded
in below as follow-ups rather than fixed here. Third: a plugin-wide shell-script simplification pass
consolidates plugin-root resolution, .context/logs/audit.jsonl appending, and .context/state.json
scalar reads behind three small shared libraries, extracts self-test bodies out of production scripts
into sibling harnesses, and fixes eleven defects found along the way. The honest headline is a shift
in where the code lives, not a bare line-count drop: production-only .sh line count fell by 3,448
(26,132 → 22,684), while harness/self-test files grew by 4,012 (1,949 → 5,961), for a net +564
lines across the corpus (28,081 → 28,645). The library extraction itself is net +302 lines — it
reduced the number of implementations, not the number of lines.
Added
fn-preflight base-sanity— a blocking wrong-base check. It compares the would-be pull-request diff against the run's ownfacts.files_modifiedledger record and fails when the PR is disproportionately larger (pr_files > ledger_files * 3andpr_files - ledger_files > 20); the second clause is a floor that keeps small runs out of the rule. The failure names both counts and the smallest-ahead candidate base, and states that the thresholds are wrong-base heuristics rather than diff-quality rules. It joins the compositeallrun in last position, so a block never suppressescontinuity's owndivergedrow. Seven degrade rungs — missingjq, no git, unresolved/unresolvable base ref, a guessed base, an unreadable ledger, an unreadable diff — each warn and exit 0, because a check that cannot see its inputs must not block a finalization. Six of the seven also write their own audit token. Thejq-unavailable rung writes no audit row at all — the audit writer readsworktask_idandrun_indexthroughjqitself, so on a host withoutjqthe row cannot be produced and the printed warning is the only evidence. Do not go looking inaudit.jsonlfor that one.- A documented, audited override.
FN_BASE_SANITY_OVERRIDEconverts the fail arm into a warning carrying both counts and writes anoverrideaudit row with the raw value. The check is deliberately blocking, so the escape hatch is explicit and leaves a trace rather than being a--no-verifyimprovised at the moment of frustration. fork_base()andbase_ref_source()inbranch-lib.sh.fork_base()ranks remote branches by commit distance fromHEADand returns the nearest parent — the branch the work actually forked from, which is not always the branch it is configured to target. It ranks every remote ref, with no containment filter, which is its known ranking defect (gh#364). It returns empty rather than a sentinel on every failure path, sov=$(fork_base)cannot kill aset -ecaller, and it is safe on a detached HEAD and in a repo with no remotes.base_ref_source()reports which rank of the resolution ladder produced a base, which is what lets a caller tell a confident answer from a fallback.fn-preflightgains astagingcheck. Catches unstaged-on-staged content that could otherwise reach a finalization silently. Built to this run's architecture-stage-amended predicate — the set intersection ofgit diff --cached --name-onlyandgit diff --name-only— rather than the original plan's bareMmatch, which would have fired on every normal FN run, since DC's own writes sit unstaged before FN'sgit add.hooks/test-execution-gate.shgains ascoped_test_runarm on the Skill branch. There was none, so any scopedbuild-testinvoked as a Skill was classifiedfull_test_runand denied regardless of its own--only/--filterscoping. The denial message now also names the resolvedtest_mode, rather than leaving the caller to guess what the gate thought it saw.state-patch.shregains a spill path with real teeth. Thefacts.decisions[]eviction spill now covers resolved items, warns loudly on failure instead of failing silently, and its trigger is a length comparison against the live clamp rather than a hard-coded12that could drift out of sync with the actual bound.- Three shared shell libraries:
skills/shared/lib/corpflow-base.sh(corpflow_script_dir,corpflow_plugin_root— mirrored byte-identically intohooks/lib/corpflow-base.sh, pinned by a parity test),skills/shared/lib/audit-lib.sh(corpflow_audit_row, the one.context/logs/audit.jsonlappender for the skills tree), andskills/shared/lib/state-read-lib.sh(corpflow_state_str/corpflow_worktask_id/corpflow_run_index, the read side of.context/state.json). All three fail closed underset -e, none usesreadonly(bats sources a library twice per process; a secondreadonlyassignment isrc 1and kills aset -ecaller), and each opens with an anti-execution guard plus an include guard. Conventions and the source-block idiom:skills/shared/lib/README.md. - 28 sibling test harnesses, extracting self-test bodies out of 22 production scripts so a production script no longer carries its own test runner inline.
- Step C.0a — a blocking
decisionitem is resolved, not asked (#360). Ablocks_next_stage: trueitem ofeffective_class == "decision"used to stop the run for a human at every stage boundary; on the nine non-exception stages it is now handed to the emitting stage's own agent, on its own model, dispatched one effort tier up, and the loop continues. Same model by design — the stage assignment already reflects the work's difficulty, so moving it would change two variables to explain one outcome; one rung up is the cheapest thing that is actually different from the reasoning that already declined. Four conditions gate it, all required: the stage is not PL/FN/ST/IR (those keep their own surfacing, and PL's boundary is the plan gate), the item still blocks and is unresolved,effective_class == "decision"after the § Step C.2 raise-only join, anddecision_gate == "auto". The join running first is the escalation guard: an item the orchestrator raises toescalatecan never reach a delegate, so nothing here widens what a run may do unattended. One dispatch per boundary over the whole set, never one per item. effort-ladder.sh— the effort ladder and the non-Opus clamp, as one executable copy.EFFORT_ENUM,effort_rank,effort_plus_oneandeffort_for_resolver, sourced by the Step C.0a tier computation, bystate-patch.sh's enum gate, and by both bats suites — the same one-definition-several-consumers reason assweep-stub-lib.sh, since a tier one caller accepts and another rejects is exactly the disagreement it exists to make impossible. The bump saturates atmaxrather than erroring, and is clamped tohighon a non-Opus model:xhighneeds Opus 5 or Fable 5, and Sonnet silently downgrades the thinking budget rather than failing, so an unclamped bump would run one or two rungs below what its own audit row claims. No current stage hits the clamp — every non-Opus stage sits atmediumor below — which is precisely why it is enforced in code rather than remembered: nothing in a run would show it if it started happening. The ladder is canonical inmodel-selection.md § Effort Levels; the bats suite asserts the two agree, andmatrix.tsvgains R43/R44 so an edit to either side re-runs it.effort_transporton every resolver audit row.metadata.effortbecomes--efforton the headless dispatch surface and is advisory in-process —Task()takes no effort argument — so the row says which surface it got:dispatch-flagorfrontmatter-only. In-process the tier is recorded, not applied, and the resolver still runs. Recorded-not-applied is not a licence to reach the number another way: substituting a higher-frontmatter agent would trade the domain expertise answering the question for a field value. Rows also carryeffort_requestedalongsideeffort_resolved, becausexhigh/maxrequested in a session with thinking disabled is sent ashighand no clamp can catch that — the same reasondispatched_agents[].model_resolvedexists.
Changed
fn-preflight base-sanityresolves its base with fork-point fill enabled; every other reader's base resolution is unchanged. Fork-point fill is opt-in —resolve_base_refconsultsfork_base()only when passed--with-fork-point, only after ranks 1–4 have all come back empty, and only to replace the terminalunresolvedresult.base-sanityis the sole opt-in caller. Every other reader of the ladder — branch naming, continuity, tree preflight — sees byte-identical output to 4.0.28. The default is deliberately the fail-safe one: extra evidence reaches the check built to reconcile it, and nothing else silently changes which branch it thinks it is working from.- Fork-point disagreement is reconciled at the plan gate, never applied silently. When PL0's
configured base and the detected fork point differ, the run raises a non-blocking sweep item
carrying both, for a human to settle.
--auto=[decision]keeps the configured base. A resolver that retargeted a branch on its own authority would trade a visible wrong base for an invisible one. - The host-declared target branch is now a documented rank in the base-ref resolution order. It
always participated; it was simply absent from the written ladder, so readers of
handoff-protocol.md,pl0-procedure.mdandworkspace-modes.mdcould not account for a base the code had actually chosen. All three now list the same ranks in the same sequence. state-patch.sh --factsrejects per item, not per payload. A mixed submission — some valid sweep resolutions alongside some malformed entries — now persists the valid remainder and exits 2, rather than discarding the whole payload because part of it was bad. Structural problems (a non-object payload, an empty object, an unknown key, a non-array value) remain whole-payload refusals; only item-shaped content gets per-item treatment. This was reproduced live against the pre-fix script during this run's own ledger writes before the fix landed.metadata.descriptionis capped at 240 characters on the two ledger write paths (--task-create --metadataand--task-meta --set) — truncated with an ellipsis, never rejected, since a refused--task-createwould break stage creation outright. The cap deliberately does not apply to the orchestrator's own dispatch-site description appends: those are transient and never persisted, and capping them would strip the test-scope, ban, and FN banners they carry.handoff-harness.shgains a reverse sweep-parity arm. Previously the parity check only caught an id present in an artifact's frontmatter but missing from the ledger; an id sitting in the ledger with no matching artifact stub was invisible. The arm is scoped by task, not stage: a split DV (DV0/DV1) shares one.stageslice, so a stage-scoped version charged each stream with the other's stubs. An artifact carrying stubs is charged only ledger ids under its ownsw-<TASK_ID>-prefixes; a stub-less artifact is charged the stage slice only when the ledger holds at most one task of that stage. It also gains afiles_touchedstructural cap (FILES_TOUCHED_MAX = 10: first ten paths plus one"+ N more"marker) and a check that a frontmatterkey_decisions[]entry does not diverge from its artifact-body counterpart — narrowly scoped to fire only when the two share no significant vocabulary or disagree on a quoted number, since an artifact's frontmatter summary is a paraphrase of its body by design.cli-fallback.shdrops its.txtplaceholder floor. A placeholder file that satisfies an existence check proves nothing about whether a capture actually happened; the floor is replaced by loud failure, and downstream consumers (adhoc-visual-evidence.sh) now accept only exit 0 plus a non-empty file on disk as evidence of a real capture.attach-visual-evidence.shreports a distinctembed_capreason. A cap-truncated evidence set was previously reported asreason: "ok", indistinguishable from an uncapped run. The body's cap note says "hosting is healthy" only when no row failed to host, since both can fire in one run and the reason already reports that asembed_cap+<host reason>.state-patch.shcarries a--factspartial rejection out of every paired---stageexit. The idempotent re-completion, absent-ledger, unresolved-artifact and unparseable-stage no-op returns all exited 0 and hid the exit 2 the caller was told to branch on; a rework round takes the idempotent path by design.- Four audit-log readers made tolerant of malformed lines.
stale-check.sh,post-compact-recovery.sh,audit-dedup.sh, andbuild-context-set.shnow skip an unparseable audit row, count it, and warn on stderr, rather than aborting the whole scan on the first bad line. - 10 of 11 audit emitters route through
audit-lib.sh's single appender, replacing their own inlinejq -ncconstruction with the shared one-writer, one-key-order, symlink-refusing path. skills/shared/plugin-root-resolution.mdnow names the one resolver that exists. It previously pointed at three "reference implementations" that were three different, disagreeing implementations of the same walk, and measurement during this run found only 4 of 69 scripts in the repository actually followed the rule the doc stated. It now namescorpflow_script_dir()/corpflow_plugin_root()incorpflow-base.shas the single implementation to source.prefix_lintreduced from ~29 subprocess forks per log line to 4 — a 20-line log that took 1,139 ms now takes 252 ms.publish-pl-issue.sh's header shrank from 161 to 58 lines, moving plugin-root resolution and ledger reads onto the two new shared libraries.- Dead-code shellcheck classes eliminated: 7 → 0.
metadata.effortis mandatory on a non-PL task row, and validated at the write (#360). It left the optional dispatch-metadata set when Step C.0a began reading it; the two axes are independent, and only one moved. As a ledger record it is now required — the resolver bumps it, and a per-stage override exists nowhere else, so agent frontmatter is the wrong fallback: a DV sub-task dispatched atxhighruns at a tierdeveloper.md'seffort: highnever mentions. As a dispatch flag it stays advisory, unchanged.state-patch.shchecks it againstEFFORT_ENUMon--task-createand--task-metaonly, so--task-status/--task-block/--task-replayare untouched; the test ishas("effort")rather than.effort // empty, because the alternative operator reads JSONnullandfalseas absent and a required field must not be erasable through its own gate. An off-ladder value would otherwise surface as a failed resolver dispatch a stage or more later, rather than at the write that introduced it. Absent still passes — older ledgers predate the field, and Step C.0a skips such a row (resolver_skipped,reason: "effort_unstamped") rather than guessing a tier, costing a round-trip instead of the run.stage-codes.mdgains anEffortcolumn on both tables, primary and support, as the lookup the orchestrator stamps alongsidemodel. Still no third copy.- Step C.3 drops the fable-plus-credit-fallback path. The FN-gate batch now resolves on the same
rule as C.0a — each item's own emitting stage's agent and model, grouped by originating stage, one
dispatch per group — and the
facts.capabilities.fable_dispatch == "credit_blocked"branch goes with it. Step A.4 is deliberately not folded in: PL is an exception stage whose boundary is the plan gate, where a user is already present undercheckpoint. - A resolver's
deep_readsis exempt from the B4 fan-in tripwire. It deep-reads by construction — the ≤200-token stub cannot carry anoptions[]body — so counting it would fire the signal on every run that resolves anything and make a real one unreadable. The two are told apart by the audit row the reads belong to.
Fixed
label-align.pyweighted the held-out tranche against a population it was never drawn from. Strata were re-derived as(split, grader_verdict)rather than taken from the drawsample-for-labelling.pyactually cut (dev/<verdict>over the dev split, plus test/held-out taken whole). All 18 labelled tranche cases therefore landed in a stratum whose population was the entire 82-casetestsplit and carried a weight near 4.6 — an extrapolation of a deliberately harder tail across cases it does not describe, when weighting assumes a random draw within the stratum. Two faults travelled with it:--min-idand--splitnarrowed the labels whilepopulationandp_obswere still built from every grade, so the documented "held-out TPR/TNR only" invocation returned corpus weights and the corpus pass rate; and--min-idalone never isolated the tranche, because batch 5 seeded 12 dev cases in the same id range.--samplenow takes the populations from the draw and refuses (rc 65) when the labels do not sit in the strata it records,--stratumselects a tranche exactly, adefercounts as drawn but not as sampled so one deferral no longer reads as a frame mismatch, and--p-obssupplies the observed rate when the gitignored responses are gone. Six regression tests. Published numbers restated: 0.3.0's corrected rate moves 87% [82–92] → 86% [82–90] and its held-out row 88% [81–88] → 89% [83–100] on one human negative; 0.2.0's corrected rate is withdrawn outright, since its draw records dev/pass 25 / dev/fail 17 against labels carrying 30 / 12. Every label-only quantity — TPR, TNR, the confusion matrices, the paired A/B, the flip and contamination analyses — is unaffected, as is the fully-labelled 0.0.1 baseline.build-review-page.py's label store was scoped by eval-set version but not by skill.localStorageis one partition across allfile://pages, so the moment a second eval set shared a version the two label stores would have merged with no symptom. The key now derives from the eval set's ownskill_name, as do the page title and<h1>.tests/python/test_eval_capture.pyran 49 of its 129 tests when executed directly. Anif __name__ == "__main__": unittest.main()block sat mid-file, so the 14 classes defined below it did not exist yet when the runner collected.run-tests.shuses discovery and was never affected, which is why it went unnoticed. Moved to EOF.evals/README.mddescribedfailure-labels.jsonlas a corpus rather than a contract. The file has never been written: Step 5b is an agent step, not a hook, so the 100-row taxonomy gate stands at 0 of 100. Also corrected two skill ids that resolve to nothing (evals-skills:error-analysis→evals:error-discovery,evals-skills:validate-evaluator→evals:validate-evaluator) and a version paragraph still claiming the spec pair reads0.2.0— bothSKILL.mdandeval_set_versionare0.4.0, while the last capture is0.3.0, so no number in the directory describes the shipping skill.fn-preflight continuityclaimed a guarantee it does not provide. Its description implied it would catch a wrong base; it checks that the branch has not diverged from its recorded base and cannot discriminate whether that base was right in the first place. The description now says so and namesbase-sanityas the check that does discriminate.- Three stale "Implemented in
fn-preflight.sh" attributions forresolve_base_ref, which lives inbranch-lib.sh. dv-tree-preflight.shno longer claims parity with the canonical ladder. Its comment now admits the reduced ladder it actually implements.- A round-1 fail-closed false positive, caught mid-run and fixed before shipping. The first pass
at the per-item
--factsfix rejected an all-empty--factspayload as "no valid items" and exited 2, aborting the paired stage patch — exactly the payload shape the closing-sweep contract tells every stage to emit when it has nothing new to report. The refusal now requires both nothing kept and something explicitly rejected; an empty, nothing-rejected payload is a no-op success. base-sanityresolved its base to a stale localdevelopref rather than its remote-tracking counterpart, found by dogfooding it in this run (0bd7df5). refs/heads/develop sat nine commits behind refs/remotes/origin/develop, andHEADwas exactly equal to origin/develop. Against the stale local ref the check reported "a PR against develop would carry 55 files" and blocked; against origin/develop the diff is 0 files — the check built to catch a wrong base picked the wrong base ref itself.resolve_git_refnow prefersorigin/<base>over a diverged local ref, covered by AC-4a intests/shell/worktask/fn-preflight.bats. The ladder still knows only theoriginremote, so a fork workflow whose base tracks a different remote is not yet correct (gh#353).cache-lint.sh'sextract_stagefailed open, not closed, on a yq failure. It fell back to anawkextraction only whenyqwas absent; whenyqwas present but failed on a given input, the function returned whateveryqproduced rather than falling back, and QA's own run this cycle produced zero anchor coverage from exactly this path. Deferred earlier in this release, then fixed before shipping: the fallback is now keyed onyq's exit code, not its presence.
Evals: label pipeline repair, batch-6 captures, and 0.4.0 calibration
-
map-and-filter.shhalf-closed join dropped every hook and script edit from the label pipeline (#359).build-context-set.shSource 4 resolves audit-log basenames against four shapes: hooks/, hooks/lib/, scripts/, and skills/*/scripts/. Rule 1 only emitted.mdprompt files as targets, making hooks and bundled scripts unmatchable by construction. Every user edit to a hook or helper script died at rule 17 DISCARD. Rule 1 widened to all four shapes, keeping test-file precedence and anti-recursion precedence intact. The fix is caller-scoped; the shared resolver is untouched.evals/failure-labels.jsonlreceived its first 7 rows (from 0 since 4.0.10);map-and-filter.batsexpanded 9 → 13 tests (all pass). -
Batch 6 (ids 213–270) and the first calibrated 0.4.0 rates. The 26-case
adjacentanalysis (seeevals/findings/request-plan-0.4.0.md) overturned the 0.3.0 reading: zero of batch 5's three labelled adjacent failures are genuine; the confound is the surface pool. Batch 6 was sized 30 buried / 18 adjacent / 10 absent to ground on the measured-genuine-negative pool (buried, registry-excluded surfaces) and the enumerated-registry surfaces (adjacent; confound repaired). No obvious or refute cases (the first yields no measurement; the second has a known false-negative floor). Corpus grew 201 → 259 (182 plan / 45 clarify / 32 refute). Two byte-identical captures recorded 259 cases each at imputed cost $207.10 and $209.65 (pair total $416.75, under $500 approved). Human labelling of a 60-case draw produced corrected 0.4.0 rates: 85% CI [81%, 91%] (TPR 92%, TNR 100% on 18 human negatives, full-set); held-out 83% CI [70%, 100%] (TPR 84%, TNR 100% on 4 human negatives). The held-out interval reaches 100% because n=4 — that is what four negatives look like. Corrected rates rest on capture #1 verdicts; the 17.1% run-to-run flip rate (below) means the CI covers sampling error only, not run-to-run variance. The 0.4.0 drifted-rule reconciliations (three copies of rule text, each contradicting the others) predicted a handful of affected cases against this noise band, so they were never measurable by a capture pair. No delta is attributed to them and theevals.jsongrading entry stands unamended. -
The run-to-run flip rate is the headline finding. Two byte-identical captures agree within 0.8 pp in aggregate (78.7% vs. 79.5%) while disagreeing on 44 of 258 paired-complete cases: 21 shifted pass→fail, 23 fail→pass. Flip rate: 17.1%. This reproduces 0.3.0's 16% on a larger corpus (201 → 258 cases), elevating it from an observation to a property of this eval. Evidence committed as
evals/findings/request-plan-0.4.0-verdicts.jsonl. With a noise floor this wide, any single capture verdict is roughly a one-in-six coin flip, and no A/B comparison can resolve an effect smaller than roughly 44 cases. -
Coverage decision for the other 22 invocable skills. 23 skill directories exist; 22 carry
SKILL.mdand are invocable. All 22 were ranked on whether a case output can be code-checked against an expected value (without human judgement or network requests) and whether the capture surface can produce that output — ranks 9, 15–18 are depressed by capture constraints (no fixtures, no simulator, no transcript replay), not output determinism. Top candidate:estimation-methodology, the only skill with an in-repo numeric oracle (estimate-calc.pycomputes expected story points from size and five factor inputs). No second eval set is committed until the failure-taxonomy gate (oracle run + ~100 label rows) has material; no build, schedule, or funding language appears in the coverage decision. -
Two grader defects repaired, ground truth only — no spec change and no re-capture (#359). The 60-case labelling pass measured the harness as over-strict and never lenient (18 human failures, all caught, zero false negatives), so both repairs subtract failures at no cost in recall. Mode A: nine cases carried
expected_outcome: planwhile being refutations of a false premise (78, 79, 104, 191, 216, 220, 225, 226, 252); they are relabelled ingen-request-plan-cases.py'sREFUTED_PREMISEtable, never inevals.json, whose case array is regenerated wholesale — the corpus is now 173 plan / 45 clarify / 41 refute and the held-out tranche 17 plan / 2 refute / 4 clarify, with none of its four genuine negatives (229, 231, 241, 262) affected. Registering case 191 first required word-boundarying the generator's echo lint and its sibling intest_skill_evals.py: the character strip set ate thesof\s, turningholds?intohold, which a bare substring test then found inside stakeholders. Measured: exactly one corpus-wide error dropped, none added. Mode B:classify_outcomeno longer lets a question mark below the section quorum decide the verdict — case 43 landed on both sides of it across two captures of the same response shape. The repair is one-directional by construction (planmay becomeclarify, never the reverse) and pinned by a property test over every stored response; the reverse form was measured and rejected because it read four genuine questions as plans. Across both captures 36 responses change class, 0 pass→fail.eval_set_versionstays 0.4.0, the two captures stay comparable, the recordedeval_set_sha256on the verdicts file is deliberately left pre-repair, and no corrected rate is republished — post-repair the harness disagrees with none of the 59 graded labels, so the correction collapses to the corpus observed rate and is in-sample only. The published 85% CI [81%, 91%] still reproduces. -
label-align.py --min-idis refused under--samplerather than silently mis-weighting (#359). A draw's populations are per stratum and whole; an id floor cuts inside one, which the drift check then reported as a moved grade set — the wrong diagnosis — and--allow-stratum-driftwent on to weight a partial stratum by its full population. The flag now exits 64 with a message naming--stratumas the selector to use instead. Unchanged on the--gradespath, where narrowing the population andp_obsby the same predicate is the correct behaviour and stays.
All label-derived quantities carry their denominators explicitly. The 85% and 83% rates measure the grader against humans, not a before-after comparison on the 0.4.0 rule changes.
Known follow-ups
- Three private base-ref ladders remain divergent from the canonical one in
branch-lib.sh—dv-tree-preflight.sh(now documented as reduced, not unified),attachments-preseed.sh, andadhoc-visual-evidence.sh. The adhoc one diverges further than the other two: its ownresolve_base_reffalls back through the literals origin/main, origin/master, main and master, which is the hardcoded-literal shape the canonical ladder documents as forbidden. Unifying them was scoped out of this release deliberately: the resolver is on the preflight hot path and each additional caller changes the cost profile. They are correct for their own callers today; they are a consolidation debt, not a live defect. facts.files_modifiedunder-recorded 15 files this run — the ledger listed 18 while 33 files were dirty in the working tree. That field isbase-sanity's other input; it did not trip the thresholds here (they need both>3xand>20), but it leaves the check's second input unreliable, and a larger gap on a smaller ledger could.- gh#316 remains open. The
facts.decisions[]clamp still evicts an entry with no spill file recorded — reproduced four times during this run's own ledger writes, most recently by the developer-review stage's own write, which evicted three of the architecture decisions its own confirmations rested on. A second spill path was judged out of scope here: it is a new eviction mechanism with no analogue to the existing "resolved-first" rule, and adding it inside the highest-risk edit of this run's sequence was the wrong trade. - The reverse sweep-parity arm may false-fail on a rework round, accepted as a follow-up rather than fixed this round.
- Two seam-dependent spill-failure WARN arms in
state-patch.shremain untested. Reaching either requires the caller's own merge filter to fail inside the spill function while succeeding in the atomic apply, which no external invocation can currently arrange; adding a test-only seam to the ledger's single write chokepoint was judged a larger risk than the untested branches themselves. - The decision-id divergence extractor does not match the
- **id — title.**bullet form used by this repository's own architecture artifacts, so it is near-inert against that style today. - Two P3s on the new spill-append regression test: its stderr assertion matches wording but not
the exact unrecorded count, and it relies on
chmod 0444denying a write, which would not hold on a CI image running as root. - Refuted, for the record: no change was made to the screenshot capture scripts. They were
reported as interpolating malformed JSON into
audit.jsonl; on inspection they already build every audit row withjq -nc --argjson, so there was nothing to fix. scan-secrets.shexits 133 on a repo-root scan. Pre-existing, reproduced during this run, not fixed here.- 49 mid-body bare
[[ ]]assertions are vacuous on bash 3.2 (macOS's default shell) but binding on bash 5.2 (CI). They pass silently on a contributor's Mac and only actually assert in CI. Left as-is; a fix needs either a bash-version floor bump or per-assertion conversion, both out of scope here. Tracked as a follow-up alongside the two above. cache-lint.sh anchor_lintdoes not yet recognizedevelopment-N-<stream>.mdfan-out artifacts.hooks/anchor-preflight.shalready routes the stream form into the lint (ARTIFACT_RE='...|development-[0-9]+(-[a-z0-9]+)*)\.md$'), butanchor_lintinskills/worktask/scripts/cache-lint.shhas no anchor set for it, so every stream artifact with legitimate extra sections (commits,verification,deferrals,defects-fixed,inherited failures,sr0-remediation, etc.) now fails--anchor-lint. The fix needs a relaxed rule perhandoff-protocol.md#anchor-allow-list— required DV anchors pluselicitation-sweepstay enforced, extra H2s are allowed — not a full exemption, since the ledger already points atdevelopment-0-<stream>.md#elicitation-sweepfromsw-DV0-1,sw-DV3-1, andsw-DV4-1.
Notes (shell-script simplification)
development-0.mdanddevelopment-0-defects.mdeach carried the same false claim — "every one of the 25 [workspace-root ladder] sites is classified" — and each is now retracted at its own site rather than only corrected here: five sites (publish-pl-issue.sh:85,:93,:113,attach-visual-evidence.sh:83,adhoc-visual-evidence.sh:53) were never classified. Raised by developer-review as P1-2's sibling finding; left uncorrected through security-review and QA and fixed in this documentation pass.- 17 self-test cases dropped during the harness extraction, each licensed by a mutation test and confirmed still covered by the surviving suite.
- 11 defects fixed along the way; six other reported findings were investigated and refuted with
evidence rather than fixed: the
state-merge.sh"committed duplicate" file does not exist (the installed copy is untracked, not committed), the here-doc test-gate hazard does not reproduce, six apparently-unused variables are live via dynamic scope (not dead code), a standalonemanifest-lib/selftest-libextraction was measured and judged not worth building, and the reported workspace-root call-site count was itself wrong (measured as 21 files / 25 sites, not the originally reported 14).
[4.0.28] — 2026-09-03
Twenty-six fixes from a six-angle code review of develop 6e09ea8..0dd3d6d, banded by the priority
the review assigned them, plus the closing-sweep transport-divergence fix found during OV-183.
Fixed
-
A sweep item's two copies could disagree, and nothing looked (OV-183).
sw-DV0-1carriedblocks_next_stage: falsein its own artifact andtruein its ledger stub;--validate-frontmatterpassed, the orchestrator read the ledger, applied the raise-only join and held a boundary gate before DR for a QA-scoping question its author had already marked non-blocking. Three defects, fixed together:state-patch.sh_sweep_joinORed the flag, so oncetruereached the ledger nothing could clear it — not even the author's own explicitfalse. Stickiness now fires only when the incoming stub omits the key, which is the case the comment above it always described.handoff-harness.sh check_sweep_ledgercompared ids and nothing else, so field divergence was invisible permanently, not just at the boundary where it fired. It now comparesclassandblocks_next_stagefor every id present in both transports and refuses — it never reconciles, because picking a winner would be guessing which copy the author meant.stage-contracts.md § Self-labels raise, never lowerhad no arm for transport divergence, so the orchestrator reached for the nearest rule. The lattice is now explicitly scoped to labellers; two copies with one author are a defect, not a lattice.
-
Contradictory escalation-under-bypass rules (P1a).
skills/shared/stage-contracts.mdcommanded opposite behaviour for the same escalation item across four surfaces depending on which one a reader consulted; the rules now agree everywhere. -
Lint/doc/data mismatches (P1b):
cache-lint.shrejected the two H2 headingsagents/software-architector.mditself mandates, failing every architecture merge on contact; the eval split manifest's held-out note had gone stale against the code;resume.mdprose contradicted what the implementation actually does. -
Six live-repro behavioural bugs, each now fails closed (P1c): an empty sweep-stub
refpassed all three validation checks;corpflow_resolve_pincould return a pin from an already completed row;stale-check.shexited with the "needs human decision" code (1) on a usage error instead of2; a contamination-detection pattern matched benign prose;eval-capture.py --dry-runprinted an argv it would not actually run;--budgetwas advisory rather than a hard ceiling. -
Model-switch gate trigger axis now fails open (P2), matching the destination axis it previously diverged from; dead
read_stdincode path removed.
Changed
blocks_next_stageis now a required sweep-stub field on both transports, enforced by the frontmatter shape gate and by--facts. Absent-vs-falsewas exactly the ambiguity that let the two copies disagree, so the author states the value rather than having it inferred from its own omission. Every per-stage template, agent payload example and schemarequired[]set carries it.- Consolidated duplicated logic (P3/P4): hook-side model-switch checks now share
model-switch-lib.sh; eval loaders now shareeval-engine.py. Hot paths were cut along the way — capability-registry load measured 2.38s → 0.69s with byte-identical output. - Deduplicated documentation (P5): the raise-only sweep lattice statement and the sweep-answers rationale each now appear exactly once, with pointers from every other site that used to restate them.
Corrected
development-0.md:52described R-7 as shippingheld_out_from: 168; that key was deleted during review remediation (ledger decisiondr-6) and the row was stale against the tree. The merged development artifact has been corrected to match what actually shipped: noheld_out_fromkey.
Known issues (tracked, not fixed here)
cache-lint.sh --anchor-lintstill fails on two## remediation — …H2 headings indevelopment-0.mdthat are outside cache-lint's DV anchor set. Left as-is rather than demoting the headings — they are the audit trail for two review blockers.- Test suite carries 4 pre-existing failures (2
AC-3 twin, 1PL-3intests/shell/skills/elicitation-sweep-contracts.bats, plustests/shell/worktask/section-lint.batscase 6 on 6 inheritedCORPFLOW.mdtemplate sections over the 1000-char cap), all red atHEADand outside the scope of these 26 fixes; verified at baseline parity, not quarantined.
[4.0.27] — 2026-08-29
Claude Code 2.1.234 → 2.1.251 integration. The band's theme is cross-agent and cross-session communication, and the plugin surface that moves most is the resume loop: almost every entry either makes a message's fate observable or removes a false negative from agent discovery.
Added
-
PreModelSwitch/PostModelSwitchhooks (hooks/model-switch-gate.sh,hooks/model-switch-audit.sh, sharedhooks/model-switch-lib.sh). The gate refuses a mid-worktask re-tier away from a stage's pinnedmetadata.model; the observer recordsmodel_switchedso cost is attributed to the model that ran. The gate fails open by construction, not by convention: a block is reachable only once the destination-model coalesce matches a real payload field, so a wholly wrong schema guess degrades to annotate-or-silent rather than wedging every session that switches models. The payload schema is unconfirmed — 2.1.251 postdates every doc in this repo — and carries a CONFIRMED/ASSUMED split in the script header per theheadless-dispatch.md § Schema Versioning Watchdiscipline. The pin is not guessed: it is read fromfacts.dispatched_agents[].model_requested. Three bats files (39 assertions), both anti-vacuity guards mutation-verified against a matcher that always returns empty and one that collapses every model to a single family. -
handoff.cross_session_ask— an optional handoff field naming who to ask and what, legal alongsideverdict: "blocked". A subagent'sSendMessageto another session delivers the reply into the parent conversation, so a stage that sent its own ask would wait for something that structurally never arrives; the stage names the ask and the orchestrator sends it (new orchestrator arm Step 6.5a3, resume branch § Reply routing). -
stale-check.shclassificationsreattach-undeliverableandhook-config-broken, with three new bats cases including a last-wins anti-vacuity guard. -
Closing elicitation sweep — every stage now has to ask. Before handing off, all thirteen stages emit a typed
open_questions[]sweep: 2–4 options with exactly one markedrecommended, a one-line rationale, and adecision/escalateclass. A stage with nothing to ask emits an explicit empty array plus a "nothing to elicit" line — silence is a contract violation, because an omitted sweep and an empty one are otherwise indistinguishable. Contract:skills/shared/stage-contracts.md § Closing Elicitation Sweep; schema:$defs/SweepItem/$defs/SweepStubinhandoff-protocol.md. Strict from this release: no warn-only mode, no opt-in environment variable, no per-stage exemption. -
Agents emit; the orchestrator alone asks. Non-planning sweeps accumulate in the ledger and render at the existing FN gate in batches of ≤4, grouped by originating stage, immediately before the approve/reject call — which fires last and unmodified. No new gate, no per-stage round-trip (
commands/worktask.md § Step C; loop step 4.9 (a1)(a2)(c+)). -
Sweep stubs reach the ledger because each stage writes them:
open_questionsis now part of every agent'sstate-patch.sh --factsexample, andhandoff-harness.sh --validate-frontmatter --statefails a stage whose frontmatter stub never reachedfacts.open_questions[]. The orchestrator runs that invocation at every stage completion (commands/worktask.md § Step B.1, loop step 6.5c), not only at DV, and an unreadable ledger fails the parity check instead of skipping it. Without that pairing the sweep was schema-valid, harness-clean, and silently dropped before the FN gate for 12 of 13 stages. -
A closing-sweep stub's
refanchor is now verified to exist:handoff-harness.shresolves each stub'srefagainst the artifact it names and fails on a dangling one. The stub carries nooptions[], so that anchor is the only transport of what the FN gate renders — a dangling ref would have made the orchestrator skip the item or invent its options. The three sweep checks are fail-closed: a yq read error, a non-string id, or a scalaropen_questionseach fail by name rather than pass, and a ref spelled with the artifact's own directory (.context/<artifact>-N.md#…, therefs:convention) resolves to the artifact itself. -
## elicitation-sweepis a mandatory H2 anchor in every stage artifact — the full items or the explicit empty statement — enforced bycache-lint.sh --anchor-lintfor all 13 stages with no grace for artifacts written before this release. It is a single universal-anchor constant, not thirteen per-stage rows. -
A sweep item can be answered at its own stage boundary.
blocks_next_stage: trueon an item routes it to a render immediately after its emitting stage completes, before the next stage is dispatched — because a next stage that builds on a guess is what the sweep exists to prevent. Everything else still batches at the FN gate. This adds no gate and changes no gate's firing condition; the flag is set per item, never per stage. It is orthogonal toclass, and joins raise-only by OR, the same monotone idiomdecision < escalatealready uses (commands/worktask.md § Step C.0; loop step 6.6). -
commands/worktask.mdandcommands/megatask.mdnow declareAskUserQuestioninallowed-tools; both called it from their bodies without declaring it. -
Stratum-weighted calibration. A labelling budget smaller than the corpus forces an enriched sample, and selecting on the grader's own verdict while measuring agreement with it biases both rates. Each case now carries population/sampled; verified against the 0.0.1 labels, an enriched sample reads TPR 42% unweighted against a population truth of 63%, and 63% weighted.
-
Seeded bootstrap confidence interval, stdlib only — no point estimate, no interval.
-
sample-for-labelling.py— picks the labelling sample by design: even split across harness strata, held-out tranche taken whole, seeded. -
scan-contamination.py— measures what a capture still leaks after the strip, by channel. The scan was itself wrong in both directions before being pinned by tests: 12% on false positives, then 2% on missed phrasings.
Changed
-
The resume loop checks the result of its own
SendMessage. Every row that said "reattach viaSendMessage" assumed the send succeeded; CC 2.1.234–2.1.238 makerefused,dropped,oversized,burst_limitedandsession_list_truncatedobservable. Anything but delivered leaves the stage parked, spends noretry_count, and — for a truncated session list — makes any "agent gone" verdict unconfirmed rather than established, so it can never justify a re-delegate. This is what the min-CC floor bump rests on. -
A
maxTurnspartial return routes to the reattach arm, not a re-delegate. Step 6.5a2's incomplete test gained an OR-branch on the partial marker, independent of whether a verdict was written, andmetadata.reasongained a third valuemax_turns_partial. Contrasted explicitly with a budget halt: both skipretry_count, but a budget halt is re-dispatched while this is reattached — the agent still holds the tree it edited. -
Handoff and escalation message formats are verdict-first: peer messages now collapse to a one-line preview, so a verdict below the fold is invisible.
-
minClaude Code 2.1.233 → 2.1.251; README requirements row now carries both justifications. -
Documented across the remaining band surfaces:
notify_when_idle, unconditional cross-session availability (never gate a handoff on provider or host OS),ListAgentslisting teammates and reporting a session's own name,claude attachvs--resume,CLAUDE_CODE_SUBAGENT_MODELas a default rather than an override, silentxhigh→highwhen thinking is disabled,experimental.cacheTtl/promptCacheTtl/subagentPromptCacheTtl,/cost's prompt-cache line as the way to verify the ≈60% cross-stage target rather than assert it, worktree lock-holding and.worktreeinclude, five security-hardening rows (TOCTOU on file tools, marketplace path-traversal rejection, project-settings tracing limits,--restricted, Bash arithmetic auto-approve). -
PL speaks the same sweep contract as every other stage.
skills/shared/figma-capture.md's auth-pending and persist-failure questions are full sweep items underplanning-<N>.md#elicitation-sweepwithid: sw-PL<N>-<n>, and the plan-gate batching section now describes items under that anchor rather than a numbered list in## summary, which keeps a one-line-per-item preview only. One resolution idiom across the pipeline: an answered item is markedstatus: "resolved"with itsresolution, never deleted — PL's plan-gate path used to delete while all twelve other stages marked. Step A.4 still also appends(auto-decided)tofacts.decisions[], a stated deviation from § Step C.5: AR/TL/DV read that array on stage entry, and ≤4 PL items cannot evict AR's newest-8 ring. -
The sweep stub no longer carries
summary.$defs/SweepStubrequires[id, class, ref]; the question text is read from the item'srefanchor body, whose existence the harness verifies.summarystays legal but optional; with the stub the only accepted item shape, an optional field needs no discriminator. The driver is the 200-token budget on the wholehandoff:block, a property of the block rather than of the sweep. -
facts.open_questions[]merges monotonically. A re-emitted stub carryingstatus: opencan no longer destroy a recordedresolution:open < resolvedis joined, never overwritten, and the matching writer-side rule is stated in§ Item shape. Scoped toopen_questionsonly —facts.decisionskeeps last-writer-wins. Both arms are needed: the ledger clamp can evict the incumbent a ledger-only fix would have refused against, and the artifact rule survives eviction. -
Sweep answers are recorded in
facts.open_questions[].resolution, never appended tofacts.decisions[]— that ring keeps only the newest 8, and a 13-stage run's sweep answers were evicting the architectural decisions it exists to hold.facts.open_questions[]is itself now clamped to the newest 12 withstatus: resolvedentries evicted first, matching the existing single-chokepoint clamp idiom. -
open_questionsmoved from Optional to Required for all thirteen stages in the per-stage required-field matrix, with the matching*_REQlists inhandoff-harness.shupdated in the same change.facts.open_questions[]gained the optionalstatus: open|resolvedthat eviction rule 2 already referenced but no schema defined. -
skills/agent-coordination/SKILL.md § Gate promptssaid the plan gate was the one checkpoint while the same file's comparison table said two. Reconciled to two; the FN gate is where the sweep renders. -
request-plan0.3.0 → 0.4.0 — three drifted rule copies reconciled. The 0.3.0 capture's 13 graded failures were first diagnosed as a missing rule each. That was wrong: the skill states each of these rules more than once and the copies contradict each other, so every failure is a model correctly following the wrong copy. All three fixes remove a copy; none adds a fourth.SKILL.md § 4's "absent from the file it was blamed on" is bounded. It licensed a plan for a defect the repo does not contain, which§ 1's ask branch prohibits. It now covers only a file that exists whose defect sits elsewhere; anything else is§ 1's, and§ 1wins.skills/request-plan/references/context-gathering.mdno longer states a stop condition. It carried the refuted 0.2.0 rule verbatim for a whole version whileSKILL.mdrejected it in as many words; eight responses stopped at a plausible neighbour. The section points atSKILL.mdinstead.- All three tier-trigger copies reconciled to canon.
handoff.mdlisted "security review, threat modelling" — topic flavour thatestimation-methodologyrules out by name — its--securetable row still framed the trigger as "work handling" a protected asset, the discovery reading canon replaced at 0.3.0, andplan-template.mdsaid a security-sensitive surface forces the tier, contradicting canon's "a surface you discover does not raise the tier". The asset list is now enumerated in exactly one place underskills/request-plan/. - Pinned by new parity contracts in
tests/shell/skills/request-plan-contracts.bats— stop-condition single-sourcing, asset-enumeration-exists-once, and a subset check whose two sides are both extracted (canon's surface-check pseudocode;handoff.md's--securerow) rather than restated in the test, so a novel non-canon term fails on membership and not on a deny-list. Each has a paired can-actually-fail test. The absence of any of them let the drift in. capability-registry.shcloses stdout with a class-level trailer naming what it does not enumerate. Complete-by-construction over six classes reads as complete over the repo unless the output says otherwise. Classes only, never a file, so no eval case becomes a lookup.
Per the spec-change rule this applies from a 0.4.0 capture forward; 0.3.0 labels are not re-flipped. It will not fix all eight stop-at-a-neighbour failures and cannot be cleanly measured — cases 202 and 203 are structurally identical and split, which is variance, not a rule difference. Repairing verified drift stands on its own merits; no re-capture should be commissioned to prove it.
-
request-plan0.2.0 → 0.3.0 andestimation-methodology0.2.0 → 0.3.0, resolving the two rule contradictions the 0.2.0 calibration measured. Both are fixed at the surface rather than by restating the losing rule, which is what created the contradictions in the first place.- The tier surface check reads the request, not the findings. It asked whether "the work"
handles a secret, which escalates on discovery: three plans took
--securefor a webhook URL, a CI login and a dependency scan that none of their requests named. It also left the tier undecidable at planning time, since two searches of one repo find different things. Now aligned withderive_route(), which can only ever see the prompt. SKILL.md § 1's branches are reordered. The fold-the-ambiguity branch was listed first and was broader than the no-surface branch, so it consumed the cases that branch exists for — four responses onabsentgrounding named the ambiguity and planned anyway, executing the first rule correctly. The no-surface branch now precedes it, and folding is bounded to requests whose surface was found.skills/request-plan/references/handoff.mdreconciled with both. Its flag-matches-the-body check keyed on anything the body argued, so a plan naming a credential path its search turned up would carry--secure— reintroducing the same escalation one layer down. It now compares the flag against what the body says the request needs. Found by the contradiction cross-read, not by a test.
Per the spec-change rule these apply from a 0.3.0 capture forward; 0.2.0 labels are not re-flipped. Until that capture runs, the effect of all three is argued, not measured.
- The tier surface check reads the request, not the findings. It asked whether "the work"
handles a secret, which escalates on discovery: three plans took
Removed
- Both pre-sweep
open_questions[]item shapes are gone. The free-text string ("q1: … (AR to decide)") and the bare{id, summary}object are no longer valid in any transport, and thenot: { required: [class] }disjointness guard that kept them apart from the stub went with them — with one shape there is nothing to discriminate.handoff-harness.shcheck_sweep_stub_shapeis now the shape gate and rejects each defect by name (not a map, id notsw-<TASK_ID>-<n>, class notdecision|escalate, noref), andstate-patch.sh --factsrejects anopen_questionsitem lacking a string.classor.ref. Existing artifacts carrying a legacy item must be migrated; there is no non-retroactive tolerance left anywhere.
Fixed
-
token-baselines.mdclaimed a per-session spawn cap of 200 that no longer exists, directly contradictingagent-coordination/SKILL.md § No total cap; concurrency is the one that bites. -
The audit
actionenum was missingstage_returned_incomplete, which the orchestrator has emitted since before this band — a pre-existing gap found while adding the new actions. -
The README hooks table claimed to mirror
plugin.json1:1 but omittedtest-execution-gate.shanddv-comment-density-gate.sh. -
cache-lint --anchor-lintrejected## summary, whichpl0-procedure.mdrequires, and the conditional## design-previewit mandates whenever the task carries a Figma URL.## summaryis now a mandatory PL anchor;## design-previewand PL's unmandated## test-strategyjoined the allowed-but-never-required set. -
cache-lint --anchor-lintrejected a review's own## re-reviewsection — the same gap that## rework-<N>had. Both are shipped conventions the DR gate reads; both are now allowed anchors. -
The
$defsnote attributed the schema injection to a section that does not perform it. It now states the obligation and says plainly that no shipped file implements it, rather than reading as verified.commands/worktask.md § Step Blikewise no longer describes its harness call as uniformly advisory: the three sweep checks inside it hard-fail regardless of--strict. -
agents/product-manager.mdhad no## Handoff Protocolsection, socache-lint.sh --frontmatter-template-lintfailed on it repo-wide — a pre-existing gap unrelated to the sweep, fixed here on an explicit user decision. It now carries the same pointer-only section the other thirteen stage agents use, citingstage-contracts.md#tpl-pl; no template copy was inlined. -
The eval capture measured the installed plugin, not the tree under test.
eval-capture.pypassed no--plugin-dir, so the CLI resolved/corpflow:request-planfrom the marketplace release whileskill_version()read this repo — a sweep could exercise one version and stamp another on all 156 records. Measured, not argued: with the old flags a command shipping only in the installed 4.0.25 was offered and one shipping only here was not. Dispatches now run in a detached worktree at HEAD, pinned with--plugin-dirand with the ambient copy disabled. -
The answer key was inside the searched tree.
evals.jsoncarriesexpected_outcome, and 7 of 114 responses in the 0.0.1 capture reached the corpus. The capture tree strips the answer key — but notevals/scripts/*.py, which six cases ground on. A worktree rather than a copy, because it excludes gitignored material by construction:.context/held a per-trace map of every known failure that the prompt-leak lint could never have seen. -
label-align.pycould not run. Its--gradesdefault pointed at a/tmppath nothing produces, so the only calibration tool in the repo was unusable. -
A rate-limited sweep lost 38 consecutive cases. Capture now retries transient dispatches with backoff; the never-fabricate contract is unchanged.
-
eval-grade.py --jsonomittedsplit, which both calibration tools key on. -
Five eval cases whose premise the repo had already answered converted to
refute; three repointed where the declared ground file did not own the behaviour.prompt_digestuntouched, so nothing needed re-capturing.
Notes
- First calibrated measurement of
request-plan0.2.0. Harness 127/156 = 81%; corrected 77%, 95% CI [65%, 85%]; dev TPR 93% / TNR 69%. TPR clears the 90% target, TNR misses the 80% floor and is reported rather than tuned.missed-the-real-surface— 15 of 22 failures in 0.0.1 — is now zero. Full record inevals/findings/request-plan-0.2.0.md. - The 0.0.1 baseline is retired as a comparison point. It ran on the un-isolated surface, so its records cannot say which skill version produced them. Its labels stay sound; its rates do not.
- The LLM judge is refuted a second time. Re-validated on labels it had not seen: TNR 0%, 0 of 10 failures caught. Verdicts committed as evidence.
- The held-out tranche has no negatives left, because its only three failures were among the five corrected cases. Held-out failure detection is still unmeasured.
Verified, unchanged
.claude-plugin/marketplace.jsondeclares no command, agent or skill path outside the plugin directory, so the 2.1.251 path-traversal rejection changes nothing here.
[4.0.26] — 2026-08-26
Command-surface reorganization. 38 → 28 commands, 26 → 23 registered skills, and no deprecation
aliases — seven commands are gone and five are renamed, so every saved invocation of an affected
name stops resolving. Shipped as a PATCH per this plugin's own precedent (MEMORY.md:53 records the
company-workflow → corpflow rename, breaking with no alias, shipping as one); the bump is
load-bearing either way, because the plugin cache is version-keyed and a rename without one serves
stale paths.
Two further changes ride along, both consequences of the first. request-plan 0.2.0 resolves the
rule contradictions the 0.0.1 eval capture exposed, extends the capability registry to the executable
surfaces every search miss grounded on, and rebuilds the eval signal that extension spends. And the
verification pass turned up a megatask defect that had been failing init-worktree.sh's own
self-test: the base branch was resolved against the caller's repository rather than the one being
initialised.
Removed
- Six commands with no runtime or no reader.
business-report,test-report,pm-prioritizeandpm-riskwere prompt specs with no aggregator behind them, and the last two duplicated judgement PL0 already makes./worktask-statusand/agent-reportwere working code that nothing in the pipeline referenced — greppingcommands/worktask.md,commands/megatask.md,skills/worktask/**, every agent and every hook returns zero hits for either. /context-status, which could not perform any part of its own contract. Its mandated output is a### Utilizationblock (Current Usage, Window Size, Utilization %) and a### Distributiontable of tokens by source — but a model cannot observe its own token accounting, and its grants wereRead, Globagainst no file in the repo that carries those figures, so every number it emitted was invented. Worse than/cost-report, which at least read a real path that happened to be empty. Separately--compress("apply compression") and--dry-run("without applying") were the same no-op, because there was noWriteorEditgrant to apply anything with, and its own## Integrationsection claimed use "byworkflow-engineerfor diagnostics" whileagents/workflow-engineer.mdnever mentioned it. Nothing is lost:skills/context-compression/stays (four agents,hooks/precompact-checkpoint.sh,skills/worktask/SKILL.md,skills/worktask/references/resume.md, and a bats suite consume it), the "> 50% window → summarize completed stages" trigger lives in that skill rather than the command, and surviving compaction is already automatic through thePreCompacthook with no command in the loop.- The whole cost-observability feature, not just
/cost-report. It aggregated.context/logs/cost-*.jsonland nothing in the shipped plugin has ever written those files — nocost-log.shexists in the repo, and none of the five registeredSubagentStophooks is a cost logger.skills/cost-optimization/SKILL.mdsaid so outright while still documenting the hook. Getting a single row required hand-writing the capture script from a snippet and registering it yourself, so the command has been inert since it shipped. The§ Per-Stage Trackingsetup instructions go with it: leaving them would keep advertising a hook whose only readers are being deleted. What stays is everything that needed no data — the model cost tiers and selection matrix, per-effort thinking budgets, the five reduction strategies, prompt caching, and the cost estimation formula thatestimation-methodologycites as canonical. - skills/worktask/scripts/status-view.sh and tests/shell/worktask/status-view.bats, in the
same commit as the command that wrapped them. They had to move together:
coverage-proxyC2 reddens on a script with no.batsand C5 on a.batsorphaned from its script. - skills/worktask/scripts/attachments-preseed-test.sh — superseded twice over and unreachable.
It was a hand-rolled harness for what became
attachments-preseed.sh, which is live, ships its own--self-test, and has a real suite. Its only references weretests/COVERAGE.md, its own bats (a test testing a test), and itself; it survived because commit719d6e7mechanically moved every references/*.sh into scripts/. - skills/worktask-status/ — the only skill directory a command removal deletes. Every other skill the removed commands touched has other consumers and stays.
Changed
- The
pm-family disappears.pm-milestone→milestone,pm-roadmap→roadmap,pm-sprint→sprint,pm-requirements→product-requirements. The prefix carried no information the bare name did not. dev-code-review→tech-code-review. The old name existed to disambiguate Claude Code's built-in/code-review; the new one does not collide, so the disambiguation note is now a pointer rather than a warning.- App Store publishing moved to the plugin that ships to that store. The three
appstore-*commands each opened with an> **Apple-only.**banner admitting they were misplaced in a platform-neutral plugin. They are nowapple-developer1.30.0'sgen-appstore-listing,gen-appstore-screenshots, andgen-appstore-iap, withskills/appstore-screenshots/re-homed as skills/tooling/appstore-screenshots/ and its 16 layout tests migrated rather than dropped.android-developer1.5.0 gainsgen-playstore-listingandgen-playstore-screenshots— original authoring, not a port: Play's field budgets and asset rules differ field by field from App Store Connect's. Play Billing IAP is deferred. - New
/appstore, a front door that writes nothing. All three old commands heldWrite; this one holdsRead, Glob, Grep, Task. It dispatchesagents/release-engineer.md, which owns platform detection, alias resolution, and the sibling dispatch — routing policy has exactly one home (skills/shared/routing-matrix.md) and no test reads a command body against it. Both apple and android markers present, or neither, stops and reports: silently picking a store and then writing to a live account is the failure this design exists to prevent. release-engineerabsorbs store publishing rather than a new agent being added. Its own capability table already claimed "App Store (iOS), Play Store (Android)"; that claim is now real. It had noTaskgrant and could not delegate to anything, so it gains a bare one, and thehaiku/maxTurns: 25budget sized for a non-delegating changelog writer becomessonnet/maxTurns: 40. Agent count is unchanged at 16.skills/shared/routing-matrix.mdgains a Release-engineer aliases section — a section of its own, not rows inside § Functional-role aliases, whose grammar is four roles × six platforms and would have demanded a web/backend/systems/ai release engineer that does not exist. There is deliberately no barecorpflow:release-engineeralias: it would collide with the agent of that name and redden the no-collision test immediately.senior-developer-reviewfolds intoestimation-methodologyasskills/estimation-methodology/references/estimate-review.md, and its step 8 stops being inert.estimation-run.md:16already listed "Senior review: platform-specific adjustments" while nothing on/estimate's default path loaded the reference.--detailednow runs it inline when its existing trigger fires (complexity ≥ 15, or AR/ML/Vision, BLE/hardware, real-time camera, unknown third-party SDKs, background processing), with--no-reviewto opt out;--reviewstays for reviewing an existing estimate with--focus/--update, which the in-run step cannot do. The adjusted SP feed### Budget Calculation, so the budget is post-adjustment —estimation-run.md's step order was corrected to match. Two sections that arrived there by accident of the skills/review/ rename (Dependency Upgrade Review, Review Feedback Hygiene) move tocommands/tech-code-review.md; neither adjusts a story point.- Content the survivors cited as canonical was inlined, not dropped. The business-case skeleton
moved into
agents/stakeholder.md(split as two H4s — it is ~1250 chars against a 1000-char leaf cap), the RICE input scales and worked example intoagents/product-manager.md, andpm-risk's scoring bands intoestimate-review.md § Risk Scoring. - README command taxonomy restated. The invariant claimed every non-orchestration command
carries a domain prefix; after this change
milestone,roadmap,sprintandproduct-requirementsare unprefixed and are not orchestration. They form a new Planning group with/request-plan. Thebusiness-,pm-anddev-groups are empty and gone. The README's own count was also wrong in both directions — it claimed 36 while the tables listed 37 and disk held 38, because/worktask-statuswas missing from the Core table; removing it settles the discrepancy rather than requiring a row. capability-registry.bats— floor raised 40 → 120 (it was set when the registry held 79 lines and had been vacuous since), plus per-class presence assertions, a negative assertion on the deliberately-unlisted directories, and fixture tests for each description extractor.skills/request-plan/SKILL.md0.1.0 → 0.2.0;evals.jsoneval_set_version0.1.0 → 0.2.0. The "found the surface → plan it" test moves from § 1's ladder into § 2 beside the registry, where the evidence arrives; content unchanged.- Eval case 4 reverted from
refutetoplanand its byte count corrected. TheCHANGELOG.mdsplit left the premise's number stale, not its request — it belongs with the correct-the-figure cases, not the shipped-work conversions. Cases 5 and 53 were re-audited against the same test and stayrefute: both cite shipped behaviour, not a stale measurement. - New eval cases covering the search surfaces the registry deliberately does not enumerate, plus
the three 4.0.26 behaviours that shipped with no coverage (
/appstoremarker ambiguity,/estimate --detailed,release-engineerdispatch when the platform plugin is absent). - Three cases still naming surfaces 4.0.26 deleted. Case 9 ("add a machine-readable summary to
cost-report") is retired: the command went with the whole cost-observability feature, so the
premise names nothing and no answer to it can be right — the same treatment 74/98/114 got, and the
id stays open rather than renumbering. Cases 16 and 22 stay
refute; only their evidence was stale, and both now cite what actually ships (/estimate --export csvand its 13-file pack;hooks/audit-subagent.shplusaudit-dedup.shas the reader) instead of/cost-report --exportand/agent-report. Corpus 157 → 156 (121 plan / 17 clarify / 18 refute). - The generator fails closed on a deleted command.
validate_named_surfaces()rejects any case whose prompt orpremise_refuted_bynames a/commandabsent fromcommands/. This table carried two dead names for three days after 4.0.26 and only a hand grep found them;premise_refuted_byis the evidence a refute case is graded against, so a dead citation lets the case keep passing while proving nothing.
Added
- The capability registry enumerates executable surfaces.
capability-registry.shnow also listshooks/*.sh,skills/*/scripts/*.{sh,py}andbenchmark/harness/**/*.py— 66 → 148 lines, ~3.2k → ~7k tokens. Every search miss in the 0.0.1 capture grounded on one of those classes, which behaviour search resolved two times in three while the markdown classes ran at 83–87%. Shell descriptions come from@descriptionor the first comment block after the shebang, Python from the module docstring's summary paragraph; a file with no description still emits no line.tests/,evals/,skills/*/references/andskills/shared/*.mdstay deliberately unlisted — the exclusion is on shared canon, not shared code. Two executable surfaces the first cut still missed are now listed:benchmark/run-benchmark.sh(the harness entry point) andskills/shared/milestone-helpers/scripts/milestone-helpers.sh, whose skill nests its code one level deeper than the flatskills/*/scripts/glob reached. 66 → 148 lines; the only executables left out are the fourhooks/lib/*-selftest.sh, which are test scaffolding. - Three rules for situations nothing covered.
SKILL.md § 2gains the six surface classes a behaviour can live in (a search that has not decided which class owns it has not finished) and turns "do not assert absence you did not check" into a structural requirement that every negative claim carry its scope.estimation-methodologygains the rule that a quiet local tree — no.context/, no livestate.json, a green local run — is not evidence about a failure the user is reporting. - A required surface-check verdict in the plan template.
skills/request-plan/references/plan-template.mdnow requires one line stating the tier verdict immediately before the trigger, the wayP2 — v1.1: nonerequires an empty phase row to be stated. Five of the graded routing failures shared one shape: the check produced no visible output, so skipping it cost nothing.skills/request-plan/references/handoff.mdgains the matching consistency rule — a body arguing for a security review and a plain/worktaskline are two different recommendations. tests/shell/skills/request-plan-contracts.bats— the three mechanical parts of the cross-surface read: no rule surface names a command that does not resolve (the check that would have caught/pm-prioritize), the command file makes no "no exception" claim and cites § 4, and the template carries the surface-check line inside its Recommended-next-step block.
Fixed
skills/worktask/SKILL.mdordered a skill that has never existed. Its DR step emittedSkill("dev-code-review")against commands/dev-code-review.md; there is no skills/dev-code-review/. This is the exact defect classtests/shell/skills/skill-refs.batswas written for — its header names this very call — but the checker could not see it:collect_skill_targetsglobbedagents/*.mdandcommands/*.mdonly, and the call lives underskills/. It silently no-opped and the DR stage fell back to the command doc viaagents/technical-lead.md. The line now names the command file, and the collector is widened toskills/**/*.md. Widening it also required reading only the first quoted argument of aSkill(...)call —platform="apple"indv-screenshot-captureis an argument, not a target.- Nothing validated a path-scoped
Bash(...)grant against the file it names. A grant likeBash(bash skills/foo/scripts/bar.sh:*)does not error when its path moves — it simply stops matching, and the agent silently loses the capability.skill-refs.batsgains a predicate asserting every such path in atools:/allowed-tools:line resolves to a real file. cross-plugin-refs.batswas passing while verifying nothing. It resolved sibling plugins at$PLUGIN_ROOT/.., which is a Conductor workspace directory locally and a bare checkout in CI — neither contains siblings, so the per-plugin loop found none and both contract tests were green against an empty set. It now takesCORPFLOW_SIBLING_ROOT, announces an empty sibling set on every run so green is never read as verified, and CI shallow-clones the six registered sibling repos before the suite. A clone failure warns rather than failing the job: a sibling repo's outage is not a defect in this one.request-planrule surfaces that contradicted each other.commands/request-plan.mdstep 2 licensed routing to a sibling command instead of the worktask, whichSKILL.md § 4forbids; it now names no sibling at all and points at § 4 as the authority — naming one is what made the file go stale twice (/pm-requirements, then/pm-prioritize). Step 3 claimed the trigger rule had "no exception" while § 4 has had exactly one since 0.1.0; it now cites that exception rather than denying it.skills/request-plan/references/handoff.mdstill told XL work to emit no command, contradicting both § 4 and step 3 — XL now names its sub-tasks and triggers the first.init-worktree.shresolved the base branch against the wrong repository. Step 7 correctly usesgit -C "$repo_root", butresolve_base_branch()shelled out tomilestone-helpers.sh base-branchin the caller's cwd, and that helper's develop-vs-master probe is agit ls-remoteagainst whatever repo it is standing in. Initialising a worktree in a repo withoutdevelopfrom a repo that has one pickeddevelop, then fetched it in the other repo and died:fatal: couldn't find remote ref develop, exit 128. It runs in$repo_rootnow (subshell, so the no-cwd-drift discipline holds). This is what the script's own--self-testhad been failing on.milestone-helpers.batsasserted a property of the developer's remote. The base-branch default test called the helper with no cwd control, so it passed or failed on whether the ambientoriginhappened to carrydevelop. Both arms now run in a fixture with a controlled remote, and the develop arm — which is what tells a working probe from one hardcoded tomaster— is covered for the first time.- The escalation canon called a live failure standard work.
skills/estimation-methodology/SKILL.mdsaid "a wedged task or a runaway batch is standard work" immediately above the test that decides it, so a reader who stopped at the example got the wrong answer. The example is now scoped to a task that has stopped, and a task that is both stuck and still failing escalates.
Notes
-
No commit in this release carries
!or aBREAKING CHANGE:footer, deliberately.version-bump-from-git.shcomputesmajorfrom either marker and sharesconventional-commits-lib.shwithchangelog-from-git.sh, so a breaking marker would put the release tooling in direct conflict with the chosen version number. The breakage is recorded here in prose instead. -
Eight eval grounding paths were repointed and two cases retired, because
tests/python/test_skill_evals.py:182asserts every case's grounding file exists and runs in CI. Prompts are untouched, so noprompt_digestmoved and no human label was invalidated. Cases 98 and 114 grounded solely on status-view.sh and skills/worktask-status/SKILL.md; with no successor surface there is nothing to re-ground them on, so they join theRETIREDtable and their ids stay open rather than renumbering. Case 74 retires for the same reason with/context-status: nothing else in the repo reports remaining context. -
Three spec versions are unmeasured, and a capture cannot attribute between them.
SKILL.md0.1.0 shipped in PR #325 with no capture. The 4.0.26 command-surface reorganization (PR #332, in this release) then removed seven commands, renamed five and repointed eight eval groundings — all inside the treeeval-capture.pyreads. 0.2.0 stacks on both. Whenever a capture eventually runs it measures all three at once and cannot separate them. Recorded here while it is cheap: a later reader comparing a 0.2.0 number against the 0.0.1 baseline will otherwise attribute the whole delta to whichever change they happen to be reading about.Separately, extending the registry converts most of the
buriedtranche from a search into a lookup, soburiedstops measuring search quality from 0.2.0 forward. The replacement cases ground only on unlisted surfaces; the note is also carried inevals.jsongrading.
[4.0.25] — 2026-08-24
Fixed
- The plan-approval carrier
PL0.metadata.approvedhad two readers and no writer. A real human approval at the plan gate appended only anapproval_receivedaudit row, so the first DV turn's approval check returnedblockedand the developer agent correctly refused to start — a prior run had to hand-stamp the field mid-pipeline.commands/worktask.mdnow stamps at each of the three arms where approval is actually established: the approval arm (user), the pure-bypass arm (auto— the explicitly requested bypass carrier is the approval), and the escalate-resolution arm (user, because a human really answered those items andautowould understate it). The auto-decision pre-pass at Step A.4 deliberately stamps nothing and now says so: it bypasses no gate and falls through to Step A.5, the sole stamping point. - The stamp precedes the audit row in every arm, and that order is load-bearing. Two files, two writes, no atomicity primitive spanning them. Stamp-first leaves a crash window with state approving and no row, which the resume path answers by re-prompting a human; row-first resumes into the stage loop with the carrier unset, reproducing the defect being fixed.
- Every batch lane hit the same block.
commands/megatask.md's per-issue planning seed now stampsapproved:"auto"alongside the gate keys it already sets. - Both reader sites in
agents/developer.mdand the one inagents/product-manager.mdnamed the field bare, which reads as the reading task's own metadata and would have left the fix inert. They now name the carrier,PL0.metadata.approved. The ledger schema entry gained adescriptionnaming PL0 as carrier and the orchestrator as writer. - New
tests/shell/worktask/approval-gate.bats(7 cases) extracts the writer payload, the reader's accepted value set and the schema enum from their own files at test time and cross-checks them, so drift on any one side fails the suite. Three guards make an empty or degenerate extraction a red test rather than a vacuous green. Reachable from all five guarded paths via L1 path references plus matrix rows R04, R07, R09, R10 and R15.
[4.0.24] — 2026-08-23
Changed
- The
request-planeval corpus was reset to a0.0.1baseline. The corpus had stopped being readable as one thing: captured responses spanned two skill versions (eval-grade.pyrefuses to average across them), labels were made under one rubric and partly re-flipped under later ones, and three findings docs each recorded a different headline for the same comparison. A staleness audit then found the deeper problem — the set was generated at27e542aand the ~20 commits that shipped the same evening read like a work queue against those very prompts.SKILL.mdversion:and a new top-leveleval_set_versioninevals.jsonboth read0.0.1and move together from here. - 32 of 121 cases were repaired before any re-capture, because capturing first pays to measure
invalid cases. 20 ask for work that has since shipped and are now
refutecases, each carrying the commit that closed its premise (REFUTED_PREMISEingen-request-plan-cases.py). 7 whose premise was never true are retired (RETIRED) —megatask --dry-runalready existed, andhook-install.shboth backs up on overwrite and writes no settings file at all. 3 more had only a stale byte count and keep their request. Retired slots stay open rather than renumbering: ids are positional, and the rubric, the split manifest and a dozen comments key on them. 114 cases remain, 77 plan / 20 refute / 17 clarify. - Capability requests whose surface already exists stay plans. A case becomes
refuteonly where it asks to build something that already ships; asking to find or reach an existing surface is whatburiedgrounding is for.SKILL.md § 2said a registry hit never licenses a short answer while§ 4said an already-shipping capability must stop without a plan — a model reading § 2 first failed every refute case for obeying the skill. § 2 now defers to § 4. - The LLM judge is retired. Measured against human labels it scored TNR 0%, catching 0 of
26 known failures, so
label-align.pydrops its column andjudge-traces.pyno longer claims to substitute for human labelling. The script stays for its isolation technique, and for the case that grounds on it. skills/request-plan/evals/failure-taxonomy.mdis retitled historical. The labels behind its rates were deleted; the seven category rules it produced are still live inSKILL.md.
Fixed
- The prompt-leak lint scanned only tracked files while capture runs against the working tree
(
tests/python/test_skill_evals.py), so an uncommitted findings doc holding a table of case ids beside their answers went undetected through a whole capture. It now scans untracked files too, covers.jsonl/.txt/.yml/.yaml, and reports every hit per case instead of stopping at the first. It remains blind to paraphrase, which is inherent to substring matching and is now documented rather than chased. gen-request-plan-cases.pysilently re-stratified every split when the manifest was unreadable, the loudest possible breach of the never-move-a-tranche rule delivered as success. A missing manifest is now an error unless--restratifysays the reshuffle is deliberate, and--restratifywrites the manifest it recomputed, so the freeze cannot be undone by the next plain run. The 0.0.1 manifest records that itstesttranche is nominal, not held out: every case predates the reset, so a genuinely unseen tranche needs genuinely new cases.
Added
-
evals/findings/request-plan-0.0.1-baseline.md— the calibrated 0.0.1 baseline. 114 cases captured at $71.73 and all 114 human-labelled. Human 81%, harness 57%; TPR 63% / TNR 68%, Rogan-Gladen corrected to 81% — the correction and the human count agree to the point, which says the harness's 57% is a measurement artefact. The harness false-fails 34 of the 92 responses a human passed, concentrated inrefute(95% human vs 30% harness) on adisputes-the-premiseverb list that rejects "already fixed".buriedsearch depth at 67% is the dominant real defect: 15 of the 22 human failures are plans that reached a plausible neighbour and stopped. -
request-plan0.1.0 — the already-ships rule is narrowed (spec change). The 0.0.1 capture showedSKILL.md § 4firing far too wide: 13 responses stopped at "this already ships" on cases whose request was to find or use a surface, and a human passed every one while the harness failed each on 3-6 template assertions. § 4 now stops only when the request asked to build/add/fix, the thing exists as asked, and nothing remains. Finding a surface you were asked to find is the answer to the search, not a reason to withhold the plan; a shipped headline with a live remainder gets a refutation of the stale part plus a plan for the rest. Per the spec-change rule inevals/README.md, 0.0.1 labels are not re-flipped — those responses were correct answers to the old contract. -
no-build-plan-for-work-that-existswithdrawn with its measurement attached: 12 firings, 12 human passes, 0 true positives, and it did not fire on the one refute case a human failed. TNR 0% is the pathology that retired the LLM judge. Replaced bycites-evidence(a path or a commit SHA — a floor all 20 refute responses clear), and its real question moved todeferred. -
disputes-the-premisewas blind to sentence-initial refutations.eval-engine.checkmatches withre.MULTILINEand notre.IGNORECASE, so a lowercase pattern could not see a response opening "Already fixed — no plan needed" — which is exactly where a refutation belongs. Patterns gained(?i); the verb list gained the misses the capture produced. Re-grading the same 114 responses moves TPR 63% → 71% and the refute tranche 6/20 → 14/20, with the corrected estimate unchanged at 81%.
Security
- The capture could read its own answer key. 7 of 114 responses reached the eval corpus
during the run and at least 3 read the expected outcome — one quoted
expected_outcome: "refute"fromevals.jsonback into its answer, another named its own case id.eval-capture.pydispatches against the working tree,evals.jsonlives there, and the prompt-leak lint exempts it by design because a case has to live somewhere. The exemption that makes the lint possible is the hole, and no further lint closes it — capture has to run against a tree that does not contain the eval set. Filed, not fixed: changing the capture surface after a paid run is a deliberate change of its own. Bounds the baseline on 7 cases.
Removed
- All
request-planresult artifacts:evals/labels/*.jsonl,evals/judgements/,evals/findings/*.md,evals/review/*.htmland the captured responses. This also closed two live prompt-contamination leaks, both of which lived in deleted files. Between the deletion and the first labelled 0.0.1 capture the harness is uncalibrated — TPR/TNR are unknown, so no grader pass rate in that window is trustworthy.
[4.0.23] — 2026-08-22
Added
.github/workflows/test.yml— the repository's first CI pipeline. The repo had no.github/workflows/at all. Two jobs run in parallel:testruns the full deterministic suite via./run-tests.shwithCORPFLOW_TEST_SELECT: "0"(selection is never the required check), andlintruns the four repo lints on a single pass —desc-lint.shandsection-lint.shagainst the repo as their real subject,cache-lint.shandpr-body-lint.shin--self-testmode because they consume aprompt-log.jsonl/PR body that nothing here emits. Checkout depth is pinned per job:fetch-depth: 0ontest(several bats shell out to git and diff against origin/master) andfetch-depth: 1onlint.permissions: contents: read, no repository secret is referenced, andconcurrencycancels superseded PR runs only — never amasterrun, which would leavemasterunverified.tests/shell/meta/ci-workflow.bats— the workflow's content as an executable contract, 16 assertions covering the permission surface, action pinning, the four lint invocation modes, the load-bearing2>&1, the per-jobfetch-depthsplit, and theshell: bashkeys. New test-selection matrix row R28 (tests/selection/matrix.tsv) routes.github/workflows/*edits to this file; without it such an edit falls through to FULL, which is safe but hides ownership.- Prompt-body authoring doctrine in
agents/prompt-engineer.md, two sections peer to the existing description grammar: a four-rule body doctrine (information hierarchy on a branching test, completion criteria graded on clarity and demand, negation by diagnosis rather than by default, no-op pruning) and a four-gate conjunctive invocation classification that DV2 applies verbatim across the skill manifests.commands/prompt-audit.mdgains the matching per-asset checks plus a--skillsscope so the body rules reach skill manifests;commands/optimize-agent.mdandcommands/arch-decision.mdcarry their counterparts. - PL0 separates facts from decisions (
skills/worktask/references/pl0-procedure.md): any question the filesystem, git, or a tool can answer is resolved by PL0 — dispatching a subagent when the lookup is slow — and never reaches the approval gate; a question that depends on another still-open question defers to a later round instead of batching beside it. An ADR-warranted gate was added alongside, with fixtures underskills/worktask/references/fixtures/plan-gate-questions/.
Changed
- Manifest keywords reduced 120 → 15. The array had accumulated release-note tokens
(
opus-4-8,todo-tools-removed, …) that CHANGELOG.md already carries; the remaining 15 describe the plugin.plugin.jsonis canonical andmarketplace.jsonmirrors keywords,description, andversion, now pinned by a parity assertion intests/shell/worktask/manifest-parity.bats. - Plugin
descriptionshortened 353 → 201 characters in bothplugin.jsonand the twomarketplace.jsonsites, dropping the enumeration of stage roles for one sentence naming the pipeline, the state ledger, cross-plugin delegation, and/megatask. description:frontmatter removed from 15 non-skill shared docs underskills/shared/— they are reference documents read by path, not invocable skills, so the key had no consumer.- Four stale "not wired to CI" claims cleared across three documents now that CI exists:
skills/context-compression/SKILL.md,skills/worktask/SKILL.md, andskills/worktask/references/handoff-protocol.md(two claims). The replacements are deliberately narrow — CI runscache-lint.sh --self-test, which gates the lint's own parser and fixtures; no captured prompt log is checked until an emitter exists, and anchor-lint is still not a CI check. - Two pipeline-only skills marked
disable-model-invocation: true—skills/preview-ensurer/andskills/csv-export-templates/— each with the gate-G3 answer recorded as a# G3:comment directly above the key: neither has standalone value outside the run that supplies its input.
Fixed
- Red test suites now report red, not green (#322, DR-P0). The suite step ran
./run-tests.sh 2>&1 | tee run-tests.logwithout ashell:key. GitHub Actions' default shell on Linux runners isbash -e {0}—-o pipefailis applied only whenshell: bashis written out explicitly. Without it the step exited withtee's status, which is 0 whateverrun-tests.shdid, so a failing suite reported success to the workflow engine. Declaringshell: bashon the suite step and on the skipped-phase summary step restorespipefailand gates the job on the suite's actual result. Two assertions cover the property rather than the literal string, matching an anchored YAML key inside a bounded step window (this file's own comments sayshell: bashin prose); both were mutation-checked — removing either key turns them red. - Colliding
G<n>gate labels disambiguated inagents/prompt-engineer.md. Its### Invocation classificationsection numbers its gates G1–G4 while the adjacent### Description grammarnumbers its rules G1–G7, and an exemption rationale closing on "so G1–G5 keep their purchase" sat eight lines below the table defining G1–G4, so a reader resolved the nearer referent and got the wrong set. Fixed at both ends rather than by relabelling — the invocation gates keep G1–G4 because DV2 consumes the procedure verbatim: the section now states up front that its labels are local and that the grammar's G1–G7 are an unrelated set, and the exemption sentence names its owning section inline. - The ENOSPC disk guard never fired on Linux.
disk_guard()inskills/worktask/scripts/state-patch.sh— shipped runtime code every worktask executes — probed free space withdf -Pg.-gis BSD-only; GNU coreutils rejects it outright (df: invalid option -- 'g', exit 1, nothing on stdout), soavail_gbcame back empty and the guard took its "df unparseable → degrade silently, never block" path. TheDISK_MIN_GBhard halt was therefore unreachable on Linux at any threshold, and had been since the guard was introduced. Nowdf -Pk, which is POSIX on both platforms, with the conversion to GiB moved into the awk program. macOS behaviour is unchanged. run-tests.shgained a re-entrancy guard, and the Apple-only phases now gate on frameworks. Re-entering the deterministic entry point for a tree already under test previously recursed silently until the job timed out; it now fails loudly. The guard keys on the tree rather than on the script and sits below the selection phase, so--print-selectionand sandbox copies stay callable from inside a run. Separately, the Swift phases gated onswiftbeing onPATH, which a Linux toolchain satisfies while not shipping the SwiftUI the benchmark template imports; all three call sites now compile the template's imports instead, and every resulting skip is named with its reason underSKIPPED PHASES:rather than passing as a quietly smaller green run.
[4.0.22] — 2026-08-21
Changed
- Description-frontmatter authoring switched to a trigger-first grammar — 42 assets (16 agents
- 26 skill definitions) rewritten so
description:leads with when to invoke, not what the asset does;commands/*.mdstay exempt from the grammar (length cap only applies). The doctrine is now recorded once, normatively, inagents/prompt-engineer.md, with pointers fromcommands/prompt-audit.mdand from the linter's own failure message — read it before hand-editing anydescription:field.
- 26 skill definitions) rewritten so
- Rationalization tables + Red Flags lists added as sibling H3 sections to six agents
(
developer,product-manager,project-manager,qa-engineer,technical-lead,workflow-engineer) — purely additive, no prior rule removed. - PL stage is now proactive: it announces its sizing decision, tags plan-shaping questions with
assumptions, and exposes a one-way mid-run re-sizing ratchet
(
requests_stage_escalation, an artifact-frontmatter field the orchestrator reads — never a ledger key) plus stage-task right-sizing. Seeskills/worktask/references/pl0-procedure.md,skills/estimation-methodology/SKILL.md,agents/product-manager.md,skills/request-plan/SKILL.md,skills/worktask/SKILL.md. - DV0 routing override re-keyed from directory to file kind: DV routes to
corpflow:workflow-engineerwhen the change touches worktask-infrastructure files that are executed (the plugin tree's**/*.sh,**/*.bats,hooks/**, JSON consumed by those scripts) rather than the old path list; markdown routes by contract, not directory. Anchored to the plugin/worktask tree — a product repo's shell script keeps the platform route. Seeskills/worktask/references/pl0-procedure.md § DV0 routing override.
Fixed
skills/worktask/scripts/desc-lint.shwas a single check (250-char cap) — extended to six checks (G1–G6) covering the full description grammar, and asset discovery switched from a depth-fixed glob tofind skills -name SKILL.mdso nested skills can no longer escape the lint. Coverage:tests/shell/worktask/desc-lint.batsgrew from 10 to 26 cases.- One skill's
description:frontmatter was YAML-invalid before this change; it now parses.
[4.0.21] — 2026-08-21
Added
skills/shared/routing-matrix.md— single source of truth for alias→target routing. 32 virtualcorpflow:*aliases (6 platform entry points, 24 functional roles, 2 support plugins) map to qualifiedplugin:agentdefault targets. Stage-agent inline tables become mandated copies validated by the newtests/shell/skills/routing-matrix.bats; the matrix's default targets are additionally resolved against sibling checkouts incross-plugin-refs.bats.- Project-level routing override. A
CORPFLOW.mdat the user project root may carry a## Routingtable whose rows win over the matrix, alias by alias — resolved once at worktask init intostate.routing(worktask/SKILL.md § Validation check 12), withrouting_override/routing_override_partialaudit rows. Template:skills/cross-plugin-handoff/templates/PROJECT-CORPFLOW.md; the## Routingheading is now reserved and forbidden in the plugin-sideskills/cross-plugin-handoff/templates/CORPFLOW.md. Location decides which of the two same-named files applies (plugin-contract.md § Project-level routing override). - Stage-dispatching agents (
developer,software-architector,security-reviewer,qa-engineer) now carry a bareTaskgrant instead of ~60 literalTask(plugin:agent)grants, so any override target dispatches; the guardrail is the mandatory delegation audit row. Adding a plugin no longer touches agent frontmatter (plugin-contract.md § Cre-ordered accordingly).
Fixed
plugin-protocols.mdandroid rows still used pre-1.4.0 bare agent names (code-fixer,security-auditor,test-generator,dependency-manager) — dispatching those either fails or silently resolves to apple-developer's agents. Nowand-*, matrix-validated.plugin-protocols.mdapple-developer table lacked the DR row every other plugin has.- commands/pm-milestone.md (now
commands/milestone.md) defaulted--platform appletoios-developerwhile every other platform (and every other file) defaults to the entry router. compatible-plugins.mdstill listed the retiredworkflow-integrationskill column (replaced by the rootCORPFLOW.mdcontract in 4.0.13).
[4.0.20] — 2026-08-21
Changed
- Prompt surface compressed 19.7% (240,429 → 193,168 words) across all 38 commands, 16 agents,
and 111 skill files (#313). Behavior-preserving: one canonical copy per fact —
skills/shared/and skill references are canon, commands/agents point instead of restating — tables over prose, one worked example per pattern, report samples reduced to section skeletons plus content rules. Frontmatter, tool grants, trigger descriptions, gate/AC language, schemas, script contracts, and every test-pinned string and cited section anchor preserved verbatim (605 citations re-resolved in a closing cross-file audit) — the sole frontmatter edits are the three correctedargument-hintfields listed below. Every rewritten file carrying aversion:field takes a minor bump (structurecategory peragents/prompt-engineer.md § Self-Improvement Patch Application). Estimation run detail split to a new on-demandskills/estimation-methodology/references/estimation-run.md.
Fixed
- Defects surfaced by the compression sweep: three dead
/release-noteslinks (renamed command), a factually drifted stage→model lookup table instage-codes.md(FN/IR swapped vs shipped frontmatter), a stale atomic-merge snippet instage-contracts.mdmissing the mandated mkdir spinlock, an emergency stage list missing DR, six dangling section anchors, a staleskills/README.mdindex (12 missing entries), and mis-labeled rule-range headings inskills/self-improvement/references/target-mapping.md. - Three
argument-hintfields that had drifted from their command's documented options:appstore-iapandappstore-screenshotsgained the--dry-run(and--path <dir>) flags they already accept, andappstore-infodropped a positional<app name or bundle ID>it no longer takes in favour of its real--lang/--path/--readme-only/--dry-runset. milestone-helpers.shcitedmegatask § Priority Sorting, a heading this release folded into§ Dependency & Blocker Resolution (DAG). The unqualified<skill> §shorthand fell outside the citation audit, which resolves the qualified<path>.md §form.- An unescaped
||inside a code span insecurity-review-process/SKILL.md § Secure Coding Patternssplit the Authorization row into five cells, truncating the rule where it rendered.
[4.0.19] — 2026-08-17
Fixed
- A DV brief built from the repo's own README ran the full suite and pre-empted QA's gate. The
orchestrator had no rule about where a stage brief's test invocation comes from, so the most
reachable command — a full-suite
xcodebuild testpasted out of a projectCLAUDE.md § Core Commands— reached DV four times on atest_mode: scopedplan.testing-strategy.md § Dispatch disciplinenow sources the invocation from<plan_file>frontmatter (test_mode,always_required_tests) and requires the brief to state the resolved mode and the exact selector, with DV recording any identifier-grammar reconciliation indevelopment-N.md § Decisions. The mechanical deny (hooks/test-execution-gate.sh) already covered this shape, including quoted multi-word-destinationvalues; this closes the prose layer that produced the command in the first place.
Changed
-
build-onlyis now selected for a comment/doc-only diff, not merely available. The mode existed for exactly this case but was opt-in and never reached for, so doc work paid full simulator cost. When every planned hunk is a comment, a prose file, or a non-executable string, PL sets it and the marker-coverage precondition does not apply — nothing executable changed, so no markers are standing in for a run. One executable hunk anywhere disqualifies the diff. The downstream consequence is stated with it: a doc-only change landing after QA (typically DC, which holds no execution authority at all) does not re-trigger QA, and FN commits on QA's existing evidence. -
FN unwinds build-tool churn as the last action before staging. Build tools mutate tracked files nobody edited — auto-extracted localization keys, scheme and build-configuration rewrites, lockfile touch-ups — and those edits reached commits because no stage owned removing them.
project-manager.md § Pre-commit scope checkrequiresgit status --porcelainto show only the run's intended files, reverts the rest immediately beforegit add, and forbids running anything that builds afterwards, since a build re-creates exactly what was just removed. FN holds no build path, which is what makes it the right stage to own it. Reverted paths are listed incomplete-summary-N.md; one that recurs every run is a repo defect worth its own issue.
[4.0.18] — 2026-08-17
Fixed
-
Absolute local paths leaked into published PR and issue bodies from two directions. The Pass-1 strip in
publish-pl-issue.shand its byte-identical copy inpr-body-lint.shknew only the home-directory mounts (/Users,/home,/tmp,/var,/opt,/etc,/root), so a checkout under/Volumes,/mnt,/media,/private,/srv, or a Windows drive letter walked through both. The alternation now covers all of them, andlocal-path-regex-parity.batspins the two copies identical. Separately,fn-preflight.sh pr-bodyskipped all work under batch and incident routing, publishing unsanitised bodies; the strip now always runs and only the blocking behaviour is dropped for those routes. -
The comment-density gate blocked legitimate per-declaration DocC. A 20-case enum with one on-budget
///per case measured 48% and was rejected. Runs of one-line comments no longer count as a block; every existing fixture keeps its prior verdict. -
Change→test selection failed closed to FULL for
hooks/lib/self-test bodies.sel_alias_fornow resolves<name>-selftest.shto its owning hook's.batsby chaining through the alias table, restoring scoped runs for hook work. -
commands/agent-report.md was never registered in
marketplace.json, leaving the command invisible to the marketplace since it was added. -
Six red tests, all pre-existing.
env -uafter aNAME=VALUEoperand is a no-op on BSDenv(macOS took-uas the command name); thestale-check.shwrite guard matched the wordSendMessagein its own advice prose rather than any call, and now matches invocation syntax with a falsification test beside it;fn-preflightF23 asserted a message the batch exemption stopped emitting when it moved ahead of the sanitiser.
Added
-
A duplicate GitHub issue is caught before
.context/exists. The existing anchor and exact-title guards are reachable only after the context exists, so a paraphrased request opened a second issue every time./worktaskStep 2a now scores keyword overlap against open-issue titles locally and offers at most three candidates. Advisory and human-confirmed only: it never links, comments, or writes on its own, and any failure — nogh, no auth, API error, timeout, zero hits — proceeds to a new context. Batch, incident, and non-interactive runs skip it;PREFLIGHT_ISSUE_SCAN=0disables it. -
The test-execution gate denies a run that already happened. Once an authorized invocation is allowed, the identical invocation is denied while the tree is byte-identical, scoped to
run_index. The fingerprint is HEAD plusgit status --porcelainandgit diff HEAD— the diff is load-bearing, since porcelain reports only names and status letters. Checked on the allow path only, so suppression can never widen what the gate permits; every unresolvable input allows.CORPFLOW_TEST_DEDUPE=offis the hatch. -
Unit tests for
conventional-commits-lib.sh, which shipped without a dedicated.bats— 17 cases pinning breaking-change detection, section mapping, bump aggregation, and record collection at the library rather than through its two consumers.
Changed
-
Every markdown section is back within the 1000-char cap (38 were over, across 28 files). Sections were split, not cut: leaf semantics mean a subheading turns one over-cap section into two compliant ones without losing a word. The largest cluster was the nine copies of
State Patch — REQUIRED before return, each split at its--factsobligation. -
The gate no longer forks
sed, and resolves the stage only for test commands. Classification runs first;sedis gone from the classify path entirely (5 forks to 0). Measured: benign call 13ms → 10ms, test command 36ms → 27ms.classify_segmentis 216 lines, down from 270. -
Hook comments compacted to
skills/code-comment-standard, and self-test bodies moved tohooks/lib/— four hooks shed 660 lines of test code,test-execution-gate.sh1280 → 1078. Each dispatcher fails closed: a missing body reports a failure rather than "OK". Code lines are byte-stable in every file. -
request-planv0.3.0, from an open-coded error analysis of 96 human-labelled traces (skills/request-plan/evals/failure-taxonomy.md— 9 categories, rates, and the case lists). Five rules address the format and routing categories: decide once between asking and planning, confirm a file owns the behaviour before planning against it, never assert absence you did not check, always emit all three phase rows, and end with exactly one/worktaskline.estimation-methodologygains a surface check above the size logic, so--secureand--emergencyare decided by what the work touches rather than by how large it is.The analysis then found that half of all failures were one root cause wearing two faces: 16 of 32 were
buried-grounding cases (50% failure rate, against 17–29% elsewhere) that either planned against the wrong file or gave up and asked — both the same search stopping before it reached the owning module. § 2 had licensed exactly that, telling the model to "stop gathering once more reading wouldn't move scope, phases, or effort" — a condition unknowable from inside the failure, since a search that has not found the right file cannot tell that more reading would change the plan. It now stops on evidence (name the owning file and what you read in it), searches by behaviour rather than filename, treats one hit as a hypothesis rather than a finding, makesExploremandatory rather than preferred, and gates the "no surface" branch on the search having actually run. -
The eval grader stopped excusing the failure it was built to catch. An unexpected clarifying question was reported as
CLARIFYand dropped from the denominator, hiding 9 of 32 known failures and computing the headline over 87 cases instead of 96; it is now scored against the case'sexpected_outcome. Thecites-a-repo-paththreshold dropped from 2 resolving paths to 1: at 2 it failed 8 plans a human passed, because a plan citing the handed file by full path and its neighbours by bare name resolves exactly one. Together these move the harness from TPR 83% / TNR 100% to TPR 94% / TNR 100% against the human labels.
[4.0.17] — 2026-08-16
Changed
state-merge.shships fromhooks/like every other hook. It was the lone occupant of.claude/hooks/, a split that bought nothing and cost a carve-out in every rule that quantifies over hooks. Only the plugin-root source path moved: the project-local install destination written byhook-install.shis still<project>/.claude/hooks/state-merge.sh, because that is Claude Code's per-project hook directory, not a corpflow convention. The file is now executable, which is whatmanifest-parity's AC-9 contract already required of everything underhooks/; the exemption that excused it from the mode bit is gone.
Fixed
-
The relocation would have silently disabled the Layer-2 net.
state-merge.shlocatesstate-patch.sh— the only merge implementation — through$CLAUDE_PLUGIN_ROOTfirst and a relative arm second. That arm read${HOOK_DIR}/../.., which is the plugin root from.claude/hooks/but its parent fromhooks/, so the shipped copy resolved a path with noskills/tree under it. A SubagentStop hook must never block a stage transition, so the not-found branch exits 0 — the failure mode is not a crash but a merge that quietly stops happening, the same silent-net class as the cwd bug fixed in 4.0.16. Both arms (dispatch path and--self-test) now go up one level. Six of the elevenstate-merge.batscases caught it; the two that stayed green were the ones passingCLAUDE_PLUGIN_ROOTexplicitly. -
Version parity was red before this release.
plugin.jsonread 4.0.16 whilemarketplace.jsonandREADME.mdstill read 4.0.15, somanifest-parityAC-4 failed on a clean checkout of 4.0.16. All four sites now agree. -
The
state_repairaudit row recorded an absolute backup path. 4.0.16 moved the hook onto a resolvedWORKSPACE_DIR, which madeSTATE_FILE— and with it the backup path derived from it — absolute, so the row leaked the worktree it happened to fire in.handoff-protocol.mdF4 names the backup.context/state.json.corrupt.<iso-ts>and the siblingartifactfield in the same row is workspace-relative; the row now matches both. Filesystem operations still use the absolute path — only what is written to the trail changed. -
The AR-gate test pinned an index the gate deliberately does not name.
handoff-harness.shmatches anyAR[0-9]+("did AR run", not "did AR0 run"), so when no entry exists there is no index to report and the warning saystasks.AR<N>. The test assertedtasks.AR0, contradicting the any-instance rule it was meant to guard. The test now matches; no behaviour changed.
Documentation
-
A green test run no longer reads as a measured skill eval.
tests/python/test_skill_evals.pydescribed itself as grading "captured responses," andevals/README.mdsaid every case "is graded by binary, code-checked assertions" — but no capture step exists: nothing dispatches the case prompts and no responses are stored. What runs offline is the assertion engine against hand-built fixture plans plus the eval-set lint. Both files now say so, andbenchmark/README.md's totals line no longer counts them as "skill-eval tests green". -
Published test counts were stale and mutually contradictory.
tests/README.mdandbenchmark/README.mddisagreed on the same suites (202 vs 344 harness tests, 37 vs 52 skill-script tests). Every figure is re-derived: 924 bats across 60 files, 52 Python skill-script, 350 Python harness, 48 Swift — 1374 total. The hand-maintained coverage roster intests/README.mdis replaced by a pointer totests/shell/meta/coverage-proxy.bats, which computes the target set at run time; a prose count that nothing enforces is what drifted. -
Two sections were over the 1000-char lint cap.
skills/agent-coordination/SKILL.md's plugin-hook writers table (1178) had the same lifecycle-field sentence duplicated across two rows and an inline aside on thestate_transitionhook path; both are factored into aPlugin-hook row fieldssubsection.shared/state-ledger.md's write-operations section (1137) gained anIdempotency and key creationheading over its existing trailing paragraph. No content removed.
[4.0.16] — 2026-08-15
Fixed
-
scan-secrets.shno longer fails open. The regex fallback swallowed grep's exit status with|| true, making "no match" (exit 1, normal) indistinguishable from "the engine crashed" (exit >1). On macOS, BSDgrepcan trap on a pattern it cannot compile (Trace/BPT trap: 5, status 133); the scan then exited 0 — the documented "no Critical/High findings" code — so a crashed scan read as a clean bill of health to any reviewer following the skill's own instruction to invoke the script rather than reason through the patterns by hand. The status is now discriminated exactly asgitleaks_scanalready did: >1 reports the failing pattern and file on stderr and returns 2 (the script's existing hard-error code). Clean (0) and findings-present (1) are unchanged. This is the same failure class an existing comment in the file describes one layer down, where a2> /dev/nullonce hid a dead pattern for the life of a regex. -
state-merge.shresolves the workspace instead of trusting cwd. The hook fires onSubagentStop, and the subagent that just stopped is very often a DV stream — the one stage for which worktree isolation is mandatory. Its cwd is a linked worktree, where.context/does not exist, because the directory is gitignored and never carried into a worktree checkout. Resolving.context/relative to cwd therefore made the documented "Layer 2 safety net" structurally unable to patch the ledger for the only stage that always needs it, and it failed silently — ano artifact resolvedline written into a shadow.context/logs/inside the worktree, a directory deleted along with it. Had an artifact resolved, the completion patch would have merged into a throwaway ledger. The hook now resolvesWORKSPACE_ROOT, thenCLAUDE_PROJECT_DIR, then the parent ofgit rev-parse --git-common-dir(which points at the main checkout from inside a linked worktree), before falling back topwd. The corrupt-ledger repair path derives its temp file from the resolved ledger so the two cannot land on different filesystems. -
The test-execution denial now names the supported way to build. Stages without test-execution authority were told only to record
requests_test_evidenceor return blocked.--no-test— accepted by every platform plugin'sbuild-testcommand, and already classifiedbuild_onlyand allowed by this gate — went unmentioned. In a measured run two streams were denied, neither discovered the flag, both invented--build-only(which no command accepts, so the classifier reads it as a full test run and denies again), and both then fell back to invoking the toolchain directly, whichagents/developer.mdforbids. The denial now names--no-testfirst, warns that--build-onlyis not a real flag, and states explicitly that falling back to the raw toolchain is not an option. -
publish-pl-issue.shno longer silently mislabels the complexity tier. Tier extraction required the word in parentheses, so a plan writingScore: 46 / 50 — Critical tier.did not match, and a silent default labelled the issuecomplexity:moderate— the amber label on the run's highest-risk issue, indistinguishable in the audit trail from a real match. A bare-word arm (word-anchored, so it cannot fire on "critically") now runs when the parenthesised form is absent, and applying the default emits a stderr line. -
dv-tree-preflight.shreports split worktree parents. When linked worktrees live under more than one parent directory, anything that reconstructs a worktree path by convention rather than frommetadata.workspace_pathmust guess a prefix — and can hand a stage another stream's tree. In a measured four-stream run a stream assigned .claude/worktrees/dv3-web was given .worktrees/dv0-service at spawn on four consecutive dispatches, because .worktrees/ held exactly one entry for the glob to land on; normalising to a single parent fixed it on the next dispatch. The preflight now names the condition and the parents involved. Advisory, not blocking: the stream that runs it has already passed the assigned-tree check, and the condition harms a different one.
[4.0.15] - 2026-08-15
Claude Code 2.1.233 removed the Todo/task-tracking tools — TaskCreate, TaskGet, TaskUpdate,
TaskList, TodoWrite — on Opus 4.8, Sonnet 5, Fable 5, Mythos 5 and newer. Every model this
plugin dispatches is on that list, and the orchestrator loop was built entirely on those four
tools, so on 2.1.233 every worktask and megatask run was broken before it started. Verified
empirically rather than inferred: the tools are absent from an Opus 5 session on 2.1.233.
CLAUDE_CODE_ENABLE_TODO_TOOLS=1 restores them. This release deliberately does not depend on
that. Instead the plugin moves to a single durable ledger, with no mirror and no fallback path —
one mechanism, one code path, correct whether or not the tools exist.
Two premises worth recording, because both were checked rather than assumed:
CLAUDE_CODE_ENABLE_TASKSis not the switch. It was set in the observed environment and does nothing for these tools. Confusing the two costs a debugging session.state.jsonalready held most of the ledger.stages{}carried the same status enum,retry_count,error_file,artifact,verdict;state-patch.shalready owned locking and the atomic write; and 13 of 16 agents already held its Bash grant. The cutover is smaller than the tool removal makes it sound.
Changed
- BREAKING —
state.jsonisversion: 2.tasks{}, keyed by numbered stage id (PL0,DV0,DV1), is now the sole stage ledger. It replaces both the Task System and the oldstages{}map. Aversion: 1ledger is not migrated and not tolerated — readers fail loud. There were no in-flight ledgers on disk when this landed, so no converter ships. - The numbered key is the point.
stages{}was keyed by bare stage code and could not represent parallel DVN tracks, which is whydispatched_agents[]had to be keyed separately.DV0andDV1are now simply distinct keys.hooks/test-execution-gate.shgains from this: two parallel tracks of one stage resolve to a single unambiguous stage code where bare codes forced a fail-open. - BREAKING — the four Task tools are removed from all 16 agents'
tools:frontmatter, and from commands/worktask.md, commands/megatask.md, commands/cost-report.md, commands/context-status.md, commands/test-report.md, commands/improve-yourself.md. - BREAKING —
metadata.context_filesand the F1 fallback are deleted.context_filesexisted solely as the degraded path for "state.json is absent". The ledger is mandatory now, so that path cannot occur and a second context-delivery mechanism is exactly the kind of dual path this release removes.context_refs+state_fileis the whole contract. - skills/shared/task-system.md →
skills/shared/state-ledger.md, with all 33 inbound references updated (two of them structured frontmatter deps, not prose). .claude-plugin/plugin.json— thePostToolUseaudit matcher moves fromTaskUpdate|TaskCreate|Write|EdittoBash|Write|Edit. Without this the stage-transition audit trail silently stops, andaudit.jsonlis load-bearing: it drives all nine resume branch tables.hooks/audit-tooluse.shnow recognises astate-patch.shinvocation, recordstask_idandstatusfrom it, and drops every other Bash call so widening the matcher does not turn the audit log into shell noise.workspace.jsonworktask.task_prefix("t1") is removed with no replacement. Thet{track}-{n}id scheme it namespaced no longer exists, and no key-level namespace succeeds it: every track's ledger keys are the plain<STAGE>0ids, isolated by the track's own worktree.
Added
state-patch.shledger operations, replacing the retired tools:--task-create <ID> --metadata,--task-status <ID> <status>,--task-block <ID> --on <ID[,ID...]>,--task-meta <ID> --set.--task-createis idempotent and--task-blockunions rather than appends, so re-running a seed is safe. A new--task-idoverrides id resolution; without it the script resolves a stage code to the open instance of a split stage, else the highest existing, else<CODE>0.- Self-test groups T14 (ledger ops, blocked_by union, idempotent create) and T15 (split-stage id resolution, explicit override); three bats tests covering the audit hook's ledger filtering.
Bash(bash skills/worktask/scripts/state-patch.sh:*)granted todesigner.prompt-engineerandworkflow-engineeralready held unrestrictedBash.
Changed — Claude Code 2.1.221→2.1.233 integration
- The 200-subagent per-session spawn cap is gone (2.1.224). "Three independent ceilings" is now
two — depth and concurrency.
/megatask's ~18/~22-issue batch ceiling and the advice to raiseCLAUDE_CODE_MAX_SUBAGENTS_PER_SESSIONor split the batch are deleted; batch size is bounded by concurrency, disk, and rate budget alone. - Subagent forking is on by default (2.1.232) —
CLAUDE_CODE_FORK_SUBAGENT=1is no longer needed, and a fork inherits the parent's prompt cache, making it the cheapest available handoff for a stage that needs whole-orchestrator context. - Worktree git isolation is runtime-enforced (2.1.222), promoting a corpflow convention to a guarantee. The v4.0.14 conflict-recovery precondition stays as written — it is conditioned on the host refusing destructive git, which is still the right framing.
- Documented:
/commit-push-prno longer auto-approving dangerous git flags (2.1.229); background sessions preserving work perCLAUDE.md(2.1.221);/reviewas a/code-reviewalias withultraand background high-effort runs (2.1.223, 2.1.232); org-restricted family aliases stepping down within the family and the restricted-model warning (2.1.222, 2.1.223);Notificationfiring for permission prompts under Desktop/VS Code (2.1.233); PreToolUse auto-allow no longer widening an agent's grant list (2.1.222); immediate plugin activation,"."skills paths, thearchivesource with SHA-256 pinning, andplugin validatefrontmatter checking (2.1.221, 2.1.224, 2.1.233); cross-sessionSendMessage/ListAgents(2.1.224-2.1.232);CLAUDE_CODE_WORKFLOW_PREFIX_STAGGER_MSsibling staggering (2.1.229); andCLAUDE_CODE_TOOL_MEMORY_LIMIT(2.1.233, Linux only). - Minimum Claude Code raised 2.1.220 → 2.1.233, pinned to the band top per the v3.35.0/v3.37.0 precedent.
MEMORY.mdtrimmed from 37KB to under its own documented ~5KB cap.
[4.0.14] - 2026-08-15
A completed 9-issue parallel /megatask batch, turned into tooling. Nothing here is speculative:
every item below is a collision that actually happened, was paid for once by trial, and is written
down so the next batch does not pay for it again. Two halves — codifying git mechanics already
learned (REQ-1..4), and reducing the collision rate by giving parallel tickets visibility into
shared seams and by making the one purely mechanical conflict class self-resolving (REQ-5, REQ-6).
Three of the six changes exist because a premise in the originating post-mortem turned out to be false when checked against the tree, so the corrections are part of the release:
- The deny-list premise does not hold in this repo. The proposal assumed a
settings.jsondenyinggit merge/git reset --hard/git push --force. This repo ships no such file. That refusal was a property of the host environment the batch ran in. A playbook that asserts a file the reader cannot find loses its credibility on the first line, so the playbook is written condition-first instead. - Git has no per-worktree exclude.
.git/worktrees/<name>/info/excludeis simply not consulted (verified on git 2.54); only the common dir's info/exclude works. So the exclusion is necessarily checkout-wide, and the release documents that consequence rather than hiding it. - The "one-line script fix" was not one line. Three candidate mechanisms with materially different blast radius, one of which pollutes every future commit.
Added
## Conflict Recoveryplaybook inskills/megatask/references/git-integration.md. Five ordered steps for the case where the environment refuses destructive git: rebase locally, push under a new branch name, open a replacement PR, close the superseded one, merge the replacement. Step 2 is the non-obvious one and gets its own subsection — re-pushing the original name would need--force, which is exactly what is refused. A further subsection reconciles the local rebase withgit-conventions.md § Merge Strategy: that rule governs how a PR is integrated, not whether a branch may be rebased before review.- Scratch metadata excluded at worktree creation, not reactively.
init-worktree.shgainsexclude_scratch, writing/workspace.jsonand/.worktrees/into the git common dir's info/exclude beforegit worktree add, behind an exact-linegrep -qxFguard so repeated init cannot duplicate entries, with a provenance header naming the script that wrote them. The motivating failure was an unscoped stage-everything command during a conflict resolution that committed the batch's own scratch file to the shared branch. It never touches the tracked.gitignoreand never writes a stored setting;--dry-runannounces and writes nothing. The checkout-wide reach is bounded and documented: an ignore rule can never mask a tracked file, and the patterns are anchored, so a nested src/sub/workspace.json stays visible. ## Conflict Resolutionrules inskills/megatask/SKILL.md. Rule 1: DI-container and coordinator-shaped conflicts are hand-resolved by a human and never script-merged — a scripted "keep both sides" resolver mis-joined an argument list (a missing separator, because the other side's block opened with a comment) and duplicated a closing brace, twice, both caught only by a later build. Rule 2: a real build and test run before pushing any resolution. A script is permitted only where its class needs no interpretation and it refuses what it does not recognise — which is the barresolve-pbxproj-membership.shbelow is held to.## Shared-Seam Registryconvention — a decision rule, not advice. Exactly one registry per batch, hosted by the sole level-0 issue, or by the milestone / orchestrator issue when there are several level-0 issues or none; the rule is total, so no batch shape is ambiguous. The schema requires a verbatim declaration block with ordered parameter labels, plusconsumers:andchange-protocol:. The ordering is load-bearing: the motivating failure was two parallel tickets inventing the same abstraction under near-identical names with a reversed argument order, and a name-only schema would not have caught it.commands/megatask.md § Step 3restates the resolution inline so the per-issue prompt is self-sufficient. Convention, not a gate — nothing validates the registry this release, by design.skills/megatask/scripts/resolve-pbxproj-membership.sh— sorted-union resolver forPBXFileSystemSynchronizedBuildFileExceptionSet.membershipExceptions, the one project-file conflict class that needs no interpretation. Dropping either side silently unregisters test files: green build, tests never run. All-or-nothing — any out-of-class conflict anywhere refuses the whole file with exit 1 and leaves it byte-identical (structural, not defensive: the parse goes to a temp buffer andmv -fruns only on accept). It refuses a conflict outside the list, a diff3|||||||base section (resurrecting a deliberate deletion is worse than refusing), a non-entry line inside a side, nesting, an unterminated conflict, and a list close inside a conflict. Ships with--dry-run, a 35-check--self-testand dual--flag value/--flag=valueparsing, matchingbuild-orchestrator.sh's established pattern, and is registered in § Canonical Scripts.
Changed
skills/shared/git-conventions.mdrecords the comment-character trap. The#NNNcommit-subject convention collides with git's default comment character; in the source batch it destroyed the subject on four separate invocations, promoting the body's first paragraph to the subject each time. The scope is now stated precisely, because the imprecise version is what makes people distrust the note:stripcleanup deletes#-leading lines on editor-driven invocations only —rebase --continue, barecommit,--amend, conflict commits — while-mand-Fusewhitespacecleanup and are unaffected. The remedy is per-invocation (git -c core.commentChar=…, or--cleanup=verbatim); writing it into a stored configuration file is explicitly forbidden. Stated once, in the source of truth; the megatask docs reference it rather than restating it.
Fixed
- The test-execution gate was inert for every stage after PL, of every worktask.
hooks/test-execution-gate.shresolves the acting stage from.context/state.jsonalone — deliberately, never fromagent_typeor an env var, so a nested delegate inherits the stage and the deny reaches its own leafBashcall. But the orchestrator loop marked stagesin_progressin the Task System only: the sole state.json mirror sat in the step-6.5a2 incomplete-return path, which fires after a stage returns, not at dispatch, and stage agents only ever patchcompleted. So from dispatch to completion nothing in the ledger named an acting stage,resolve_stage()returned empty, empty means allow — correctly, since guessing in the deny direction would deadlock unrelated sessions — and every deny the hook implements was unreachable in a live run. Nothing was unsafe: this is a fail-open backstop by design, and the dispatch-time ban banner and tool-grant narrowing are the primary controls. The backstop simply was not covering.skills/worktask/SKILL.mdstep 5 now mirrors the mark into state.json immediately beside theTaskUpdate, with the reason stated inline so the two writes are not separated again. Found by the gate firing twice against a hand-written ledger during this release's own run, while two real full-suite invocations at banned stages had gone through untouched. Both directions are pinned by tests — a non-authority stage denies, and a ledger with nothingin_progressfails open — plus an assertion that step 5 still carries the mirror, since that instruction is the fix and nothing else guards it. - Separated
stat -fis a GNU/BSD trap, and it was in the new code.stat -f '%Lp' "$f"is correct on BSD, but on GNU coreutils-fis--file-systemand takes no argument, so the format and the path become operands: stat prints a filesystem block to stdout while exiting non-zero, the|| stat -c%afallback sees only the status, and the capture becomes that block concatenated with the fallback value.chmodthen fails and the script aborts before itsmv— on Linux, every accepted resolution died. All four sites now use the repo's attached idiom,stat -f%Lp "$f" 2>/dev/null || stat -c%a "$f" 2>/dev/null || printf '644', which is safe because GNU getopt rejects-f%Lpat option-parse time and writes nothing to stdout — the same form ten pre-existing call sites already use. Two guards make the class fail locally next time: a runtime^[0-7]+$check before thechmod, and a self-test probe asserting the token is single-line and purely numeric. - Resolved files keep their permission bits. The temp buffer is created 0600; the target's mode
is captured before the
mvand re-applied. Tests assert a distinctive0640survives, not the 644 default that would pass by accident. - No temp-file residue.
$( )subshells reset traps, so buffers allocated inside one were unreachable by the cleanup trap, andTMPDIRalone does not help (macOSmktemp -tignores it). Buffers now use an explicit template directory underRESOLVE_TMPDIR/TMPDIR, with a module-level array and a single EXIT trap. Measured across a self-test run: 659 temp entries before, 659 after; previously +34 per run.
Known limitations
- Not executed on Linux. The full suite (913 bats + 52 + 350 unittest, 0 failures) and all
self-tests ran on macOS / BSD userland. The
statfix above was validated against a faithful GNU-getoptstatmodel with a positive control that reproduces the original break, but that models exactly one binary —mktemp,chmod,awkvariants,sed,grep -qxFand git'srev-parse --git-common-diranswer are unexercised on a real GNU userland. This repo has no CI configuration, so nothing catches a Linux regression automatically. A Linux job is recommended before the plugin is relied on off-macOS. - The worktree exclusion is verified on git 2.54 only. It depends on the per-worktree info/exclude not being consulted while the common-dir one is — empirically established, not a documented guarantee. A future git could change it; the self-test would catch it, but only when run.
skills/worktask/scripts/state-patch.shstill carries the separated-statshape at its lock mtime read, where it degrades silently (|| printf ''). Pre-existing at HEAD and deliberately out of scope for this release; tracked as a follow-up.
[4.0.13] - 2026-08-13
The same rename, a second time. v4.0.0 moved igrsoft → company-workflow because the plugin id
was the last artifact carrying the vendor name; this release moves company-workflow → corpflow
because the repository has since become IGRSoft/corpflow and the plugin id was, again, the last
thing to catch up. The v4.0.0 entry below is the template this pass followed — the same three-role
split, the same runtime-literal traps, the same sibling ordering.
The word's three roles, and separating them is again the whole substance. Plugin identity
moves: every company-workflow:<agent|skill> invocation id, the marketplace.json plugin entry,
plugin.json's Stop matcher and its notification title, the Task(company-workflow:…)
frontmatter grants, and the install key (corpflow@igrsoft). Vendor identity does not: the
author block (IGRSoft, support@igrsoft.com), the github.com/IGRSoft/… owner segment, the
com.igrsoft.* bundle IDs in /appstore-iap, and the marketplace name — still igrsoft, so the
cache path becomes ~/.claude/plugins/cache/igrsoft/corpflow/<version>/ with only the second
segment changed, exactly as last time.
The third role is new, and is what made this pass different from v4.0.0: the repo slug.
GitHub had already been renamed, so README.md's git clone https://github.com/igrsoft/company-workflow.git
was a published command that no longer resolved, and the cache-discovery glob in
skills/shared/plugin-root-resolution.md pointed at a directory that will never exist again.
Five sites matched the literal string at runtime and would have failed silently rather than loudly — the first four are the same four v4.0.0 called out, which is itself the argument for writing them down:
hooks/dv-screenshot-gate.shguards on exact equality (!= "corpflow:developer"). Left stale, the DV screenshot gate would no-op on every run with no error.skills/self-improvement/scripts/build-context-set.shmaps agent refs to file paths via an awk field compare ($1 == "corpflow")..claude-plugin/plugin.json'sStopmatcher must track the plugin name or the PL/FN approval-gate push notification stops firing.skills/worktask/scripts/publish-pl-issue.shcarries four identical copies of the plugin-prefix leak regex that strips internal agent ids out of published GitHub issues; they stay byte-for-byte in lockstep with the list inskills/shared/compatible-plugins.md.- New this time:
skills/agent-coordination/SKILL.mddocuments thatsubagent_typematching is case- and separator-insensitive, using the literal exampleTask({ subagent_type: "Company_Workflow:Developer" }). A lowercase-only pass rewrites the rest of that line and leaves the example standing — the one illustration on the page would have become the only stale id in the repo. The residual sweep is now case- and separator-insensitive (grep -niE "company[-_ ]?workflow") so this class cannot survive again.
Changed
- Redundant phrasing collapsed.
company-workflow workflowdoubled the noun; under the new name it would have readcorpflow workflow. The trailing noun is dropped instead — "this agent is inside a company-workflow workflow" becomes "this agent is inside corpflow" — across ~78 sites in the six sibling plugins' agent intake blocks. The separated formcompany-workflow 11-stage workflow systembecomescorpflow 11-stage pipeline, reusing the word the README already uses for the stage chain.
Breaking
Recorded as breaking despite the PATCH version number, which is user-owned (the same call as at 4.0.3). Anyone reading semver alone will not be warned by the number.
company-workflow:*agent and skill ids no longer resolve. There is no back-compat alias. The six sibling plugins (apple-developer,system-developer,android-developer,frontend-developer,backend-developer,ai-engineer) are updated in the same pass; merge this release first, since their docs referencecorpflow:ids.COMPANY_WORKFLOW_*environment variables renamed toCORPFLOW_*with no fallback read —TEST_GATE,TEST_SELECT,AR_REF_STRICT,PR_BODY_STRICT, andCOMMENT_DENSITY_{MAX,WARN,MIN_LINES}. An eighth,COMPANY_WORKFLOW_DIR→CORPFLOW_DIR, lives in scripts/validate.sh in four of the siblings. Update shell profiles and CI jobs.- Stale installs must be reinstalled.
~/.claude/plugins/cache/and theknown_marketplaces.json/installed_plugins.jsonindexes still key on the old plugin name until then. audit.jsonlis not comparable across the boundary — thesubjectfield changes prefix, so pre- and post-rename audit trails cannot be diffed directly.
Not changed
Pre-4.0.13 CHANGELOG.md entries and benchmark/results/** keep their original text. A changelog
row records what shipped under the name in force at the time; rewriting it would make the history
lie. benchmark/results/result.html still reads "igrsoft benchmark" for the same reason and
regenerates from report.py on the next make report.
[4.0.12] - 2026-08-12
Activating guards that were already written. A full /worktask run (OV-184, shipped as PR #436)
burned a whole DV cycle: the agent pinned itself to a stale worktree of a different clone and
could write nothing. The agent's logic was not at fault. /worktask never stamped
metadata.workspace_path — only /megatask did (commands/megatask.md) — and that single
omission silently disabled three guards that already existed and were already tested:
| Guard | Why it no-opped |
|---|---|
| orchestrator workspace-root cross-check | _task_root="${_task_root:-$_orch_root}" compared a value to itself |
dv-tree-preflight.sh resolve_assigned() | returned empty → warn + exit 0, by design |
agents/developer.md path-prefix check | gated on "when task.metadata.workspace_path is set" |
dv-tree-preflight.sh would have caught this exact failure — its header names the scenario
verbatim — but nothing invoked it: a prose reference with an unfilled <workspace> placeholder,
no hook, no orchestrator injection.
Added
metadata.workspace_pathstamped on every run. Seeded intostate.jsonatcommands/worktask.mdStep 3a (git rev-parse --show-toplevel, elsepwd) and onto the PL0 task at Step 4, besideisolation. Documented in the ledger schema (handoff-protocol.md § metadata.workspace_path) and intask-system.md.dv-tree-preflight.shinvoked from the DV dispatch surface. A new Step 4.8 assigned-tree banner instructs DV to run it before its first edit and treat exit 1 as blocking — following the precedent Step 4.8a states outright: prose loses to the dispatch surface.- Step 6.5a2 — incomplete-return arm. An agent that yields mid-sentence with budget remaining
has not errored, so 6.5a does not fire and control fell through to F3, which hard-codes
status:"completed", verdict:"ok"over a stage that never finished. The arm keys on the evidence (artifact absent, or present with nohandoff.verdict) rather than on message shape, marks the stagein_progress, emitsstage_returned_incomplete, and resumes the agent viaSendMessageinstead of re-delegating. A normally-completed stage still takes the Layer-2 path. --emit pridempotency.attach-visual-evidence.sh --emit prcaches its emission at.context/logs/visual-evidence-pr-<worktask_id>-<run_index>.mdand replays it on any later run for the same run index;--forcere-hosts. Running it twice previously uploaded two full asset sets and orphaned the first on GitHub.fn-preflight.shtreats the newreusedaudit result exactly likeok, so a replay cannot retire the "## Visual evidence" heading requirement.agents/developer.md§ The voluntary yield — never end a turn to announce what you are about to do next; an intent sentence is not a handoff. Plus a sixth Artifact-Complete Gate box.agents/developer.md§ Manifest row shape — DV is told the manifest is written by the skill and so never sees a column name. Adds the canonical 9-column row and the two-digit-index rule, and says why a malformed row is worse than a missing one.
Changed
- The cross-check no longer defaults to itself. An unset
workspace_pathis now a loud stop with a named audit row, not a silent pass. skills/dv-screenshot-capture/references/examples/README.mdcorrected to the canonical 9 columns. It was a 7-column table (noCaptured, noDesign Ref) that would fail--validate-manifest— and it is the fileattach-visual-evidence.sh's failure diagnostic sends a stuck DV agent to. Its AC-coverage table became a list, since the validator judges every pipe table in the file it is pointed at.workspace-modes.mdgains the sibling-worktree hazard beside the existing sibling-clone hazard, with the table of why D0, D0.0, and the stated Edit/Write rule all pass on the wrong tree. Its claim that stale worktrees are auto-cleaned with no reuse of prior-session worktrees is corrected — this run disproved it.agents/developer.mdD0/D0.0 name the failure geometry: once the harness pins a worktree,git rev-parse --show-toplevelreturns that tree, so D0 is self-consistent and cannot detect the problem; and a stale worktree passes D0.0 because it genuinely is isolated. Isolation ≠ assignment. D0.0a's<workspace>placeholder is replaced with the concrete resolution order.agents/product-manager.mdno longer framesworkspace_pathas a megatask-mode detector.skills/worktask/references/resume.mdclassifies the yielded agent, which sat between its live-mid-work and agent-gone rows.pr-body-lint.shusage errors print to stderr.usage()sent the help header to stdout while the one-line diagnostic went to stderr, so under this plugin's own; true/ piped convention a caller saw help text and read it as "ran, nothing to report". An explicit-h/--helpstill writes to stdout.
[4.0.11] - 2026-08-10
Fail-loud worktask tooling (#431 retrospective, 9 approved self-improvement proposals). The
defect class: a tool that reports success while doing nothing. state-patch.sh's existing
exit-0-on-unresolved behaviour is correct for most callers (a hook probing for a stage that
never ran), but for the 13 stage agents patching their own just-written artifact it meant a
missed ledger patch returned success — the same silent-skip shape recurred three separate times
in this repo's own history before this worktask.
Added
- Fail-loud self-patch assertion (REQ-2).
state-patch.shnow exits 3 when the artifact does not resolve,--previs present, and--viais absent — the documented signature of an agent patching its own predecessor edge (handoff-protocol.md:846). Every other combination — a hook,--via, or a never-run stage — keeps the exact exit-0 no-op byte for byte;state-patch.bats:51is unmodified.--allow-missing-artifactsilences the assertion for a caller that legitimately wants no edge, but writes nothing — documented as such at all four citing sites after a review-round finding that the first pass had it write a synthesized entry. - Resolution-only alias basenames (REQ-1).
DR: review,QA: qa,FN: finalizationresolve in a new sibling function, never inside the canonical 13-entry map, so the existing parity guard keeps reading exactly 13 primaries.AR: analyzingis deliberately not added — a prior artifact-naming generation used it, and re-accepting it would let a stale file answer for the current one. USERas a valid--prevpredecessor (REQ-3). Matches the already-canonicalUSER→PL/USER→IRedges;USERstill owns no artifact and is never a valid--stage.dv-tree-preflight.sh, new D0.0a gate (REQ-7). Asserts the resolved git root equals the assigned workspace before DV's first edit. Worktree isolation alone doesn't catch this — a stale worktree is perfectly isolated, which is how a correctly-specified edit once landed on the wrong tree.- Screenshot-manifest schema at the writing stage (REQ-8).
attach-visual-evidence.sh --validate-manifestis a new read-only mode;dv-screenshot-gate.shANDs its verdict into the pass condition and now blocks a table-free manifest sitting beside real capture files (the parser reports the fact, the gate applies the policy — only the gate can see the directory). A genuine table-free skip-rationale manifest with no captures beside it still passes. - Explicit issue-close check on non-default integration branches (REQ-9).
fn-preflight.sh issue-close-requiredcompares the merge target against the repo's default branch and prints the exactgh issue close <N>command when they differ, so a non-default integration branch can no longer leave the tracking issue silently open. FN's procedure gained the acting step that runs it and executes the printed command.
Changed
- Tool grants for 10 of 13 stage agents, not the plan's 6 (REQ-5).
software-architector,technical-writer,team-lead,stakeholder,ethics-reviewerheld noBashgrant at all;product-manager,technical-lead,security-reviewer,release-engineer,project-managerheld narrow allowlists with nobash. All 10 gain the scopedBash(bash skills/worktask/scripts/state-patch.sh:*)token. This grant is empirically unverified against the permission matcher under enforcement — the probe session that ran it enforced no denials at all — so it is not the fix by itself. - Mandatory documented Edit-direct fallback for all 13 agents (REQ-6), the load-bearing half.
$CLAUDE_PLUGIN_ROOTwas confirmed empty in the model's Bash tool environment (exit 127 on a probe), so no grant string keyed on it — and the cache-install path is version-keyed, so no stable literal exists either way. Every agent's State Patch section now states an ordered ladder: run the script; on exit 3, write the artifact and re-run; if the tool cannot run at all, patch.context/state.jsondirectly withEdit— both thestages.<CODE>entry and thehandoffs["<PREV>→<CODE>"]edge — and record the occurrence undermetadata.pl_tooling_gaps. The SubagentStop hook is explicitly not a substitute: it never passes--prev, so it repairs the stage entry but drops the handoff edge.
Fixed
attach-visual-evidence.shtable separator detection now accepts alignment rows (:---), removing a false schema block on a valid model-authored table.fn-preflight.sh's unresolved-issue line no longer promises agh issue closecommand in the same breath as reporting it cannot be printed.state-patch.sh's basename resolver requires the filename stem to equal the basename exactly, so e.g.qa-notes-3.mdcan no longer answer for the newqaalias.
[4.0.10] - 2026-08-07
Eval-audit release (#279). The benchmark's only quality signal was the arm's own
swift test — self-graded, and across every stored live record it never once
returned fail. Nothing measured whether generated output was actually correct,
user corrections to delivered work were discarded with each run's gitignored
.context/, and skill eval sets carried prose expectations no grader could
check. This release makes each of those signals held-out, durable, or binary.
Added
- Held-out oracle (
benchmark/harness/benchmarkkit/oracle.py,benchmark/oracle/cases.json). Both arms' prompts embed a scripted CLI contract (_cli-contract.txtis the SSOT; a lint pins both prompts to it verbatim). After measurement the harness release-builds each arm'stictactoeand scores it against 30 cases whose goldens are captured fromttt-template, never hand-written. Cases are tiered:specified(24, restates the contract — the floorpass_failreads) andimplied(6, derivable but unstated — the discriminating signal, reported beside the verdict). A mutant that sweepsspecifiedwhile failingimpliedis built and asserted intest_oracle.py. - Comparability eras. Every record now stamps
era(harness generation, prompt-contract version, per-stage model pins);bench-analyzecompares against the previous live record automatically and caveats each differing dimension — the v3.37.1 silent-repin failure mode, closed. - Committed failure-label dataset (self-improvement Step 5b,
evals/failure-labels.jsonl). Classified user edits persist as append-only JSONL labels viaappend-labels.sh(idempotent per worktask run, opt-outSELF_IMPROVE_LABELS=0, no diff bodies);label-stats.shaggregates per target/category. Runs regardless of proposal approval. - Binary skill eval sets.
skills/request-plan/evals/evals.jsonreplaces prose expectations with code-checked assertions (contains_all/contains_none/regex_all/regex_any);tests/python/test_skill_evals.pyis the assertion engine plus a lint keeping every eval set gradeable. Interpretive criteria are parked indeferred, not graded badly. - Skill-reference contract (
tests/shell/skills/skill-refs.bats). Four repo-wide predicates: every orderedSkill(call has theSkilltool grant, every Skill target names a real skill, everyskills/<name>/…citation resolves, and every agent citing plugin paths carries a## Plugin pathsresolution block. Each predicate is falsified against a synthetic tree. - Findings docs:
benchmark/results/AGENT-GRANT-ENFORCEMENT.md(tool sets bind in headless dispatch;Bash(cmd:*)scoping andmaxTurnsdid not — probe specified before any fix),benchmark/results/KNOWN-BAD-RECORDS.md(stored records excluded from comparisons instead of edited),benchmark/results/VARIANCE-STUDY.md(the n=3 procedure; the only step that spends money).
Changed
- Per-arm budgets and per-stage projections (
benchmark/harness/benchmarklive/budget.py). Paired runs split--budgetinto equal per-arm tallies (a shared purse let the first arm starve the second — the defect that invalidated the first paired A/B), projections use calibratedSTAGE_EXPECTED_TOKENS(DV ≈ 10× a light stage; the flat figure admitted budgets that could not finish), and the running tally reserves the heavier of the projection and the heaviest observed stage. - Fail-closed verdicts. An unmeasured or degraded arm is never green;
where the oracle ran, it — not the arm's self-written suite — decides
pass_fail.coverage_pctisnullwhen unmeasured, never a fabricated 0.0. - Live retention is unbounded (
rotation.RETENTION): live records and theirbenchmark/results/runs/live/detail files are kept and tracked in git — deterministic records stay latest-3 and gitignored. - Agents: every agent carries the
## Plugin pathsresolution block;developerandstakeholdergain theSkillgrant their bodies order (dv-screenshot-capture,self-improvement— both silently never ran);Skillinvocations use the realSkill({skill: "company-workflow:…"})syntax;technical-lead's DR gate reads commands/dev-code-review.md as a command instead of invoking it as a nonexistent skill.
Fixed
- Each arm's verdict reads its own degradation flag; a WITHOUT-only breach no
longer fails a complete, oracle-conforming WITH arm (pinned by
PairedVerdictSymmetry). - Label dedup hashes
(worktask_id, run_index, path, added, removed, summary), so a correction recurring in a later worktask counts as the recurrence it is instead of being dropped.
[4.0.9] - 2026-08-06
Every test run was the whole suite. Editing one agent doc, one hook, or one script ran all
55 .bats files, so the dev loop paid full-suite latency for a one-file change and the only
lever was to skip testing entirely. The obstacle to fixing it is that a wrong selector is
silently green — it under-runs and reports success — so the change is built to over-select
by construction: three independent layers unioned, a floor that every run includes, and
seven triggers that abandon selection and run everything. Selection is opt-in and off by
default; a bare ./run-tests.sh is byte-identical to what it was.
Added
- A three-layer change→test dependency matrix. L1 is computed live from the tree — every
repo path a
.batsnames literally, plus theX.sh→X.batsconvention resolver, which matters because 13 scripts have more than one consumer and convention alone returns one of them. L2 istests/selection/matrix.tsv(24 rows), for dependencies that are a pattern rather than a path, where the glob lives inside the script the test invokes. L3 is the ALWAYS floor, a constant intests/lib/select_lib.bash: lib/test-helper, meta/coverage-proxy, skills/plugin-root-refs, and worktask/manifest-parity — 47 of 811 tests, included in every scoped run. The engine lives intests/lib/select_lib.bashandtests/bin/select-tests.sh. - An opt-in runner surface (
run-tests.sh,Makefile).--changedruns the selection,--base <ref>picks the diff base, and--print-selectionprints the plan and runs nothing.make test-changed,make test-changed BASE=master, andmake test-selectwrap the three.COMPANY_WORKFLOW_TEST_SELECT=0disables selection entirely. Passing--changedwith--coverageis a hard exit 64, not a silent full run. - Fail-closed triggers F1–F7. An unrecognised path, an unresolvable base, an empty
changed set, a delete under tests/, hooks/, .claude/hooks/ or a skill scripts/
directory, a change to the runner or the selector itself, and an unparseable matrix each
yield a
FULLverdict and run everything.--print-selectionnames the trigger id, so a widened run says why it widened instead of looking like a slow selection. - A >50% widening cap with a hand-off to QA (
run-tests.sh). A selection covering more than half the file count emitsWIDE. If.context/state.jsonshows the DV stage in progress the run exits 65 and hands the suite to QA, since DV has no full-suite authority; for everyone elseWIDEis informational and the run proceeds. - A 17-guard suite (
tests/shell/meta/test-selection.bats). It drives the real selection entry point rather than asserting on the matrix data, refuses a matrix row without a rationale, refuses a glob matching no tracked path, refuses a.batsno layer can reach, and plants violations to require the checker to name them — the fail-closed and cap behaviours are falsification tests, not assertions about output text.
Changed
- The test-execution gate now classifies by argument (
hooks/test-execution-gate.sh).run-tests.shhas a selection surface, so a bare invocation is stillfull_test_runwhile--changed/--baseread asscoped_test_runand--print-selectionasbuild_only. DV may therefore run a scoped suite it previously could not. Matching is padded-segment and anchored, socat run-tests.shdoes not classify as a test run. The widening cap is deliberately not enforced here: only the runner knows the selection size, and this hook stays a cheapPreToolUseclassifier. tests/shell/meta/coverage-proxy.batsnow drives the resolver extracted intotests/lib/select_lib.bashthrough shims instead of carrying its own copy, and its script-inventory check iterates the alias keys. The resolver'sfind | head -1filesystem-order dependence is fixed withLC_ALL=C sort, so a multi-match is stable.
Deliberately not shipped
- No
make coverage-changed, and--changed --coverageis a hard exit 64 rather than a no-op (tests/COVERAGE.md). kcov's denominator is the source set and does not shrink when fewer tests run, so a scoped coverage run lowers the numerator only — the bash branch has no percentage gate to catch it and would report a drop that looks like a regression in the code rather than in the measurement, while the Swift branch's ≥85% gate would fail for a reason unrelated to the change under test. Coverage is a full-suite QA activity. - No module correlation. A
SKILL.mdedit does not pull in that skill's script tests. This is the release's main residual false-negative risk: a reference-doc edit whose path no test names literally selects only the ALWAYS floor. It is bounded by the reachability and non-script-coverage guards, and the fix for any gap found in practice is a targeted matrix row, not a blanket rule. - No
/test-selectslash command — manifest parity asserts the marketplace command list against the commands directory, so adding one carries its own blast radius.
[4.0.8] - 2026-08-06
state-patch.sh skipped its write whenever a stage was already completed at the same
verdict. A review-remediation loop is exactly that shape — DV completes ok, DR requests
changes, DV remediates and re-completes ok — so the second call short-circuited and left
handoffs["PREV→CODE"] describing the pre-remediation artifact, with no flag to correct it.
The stage record and the artifact reference stayed correct, so nothing failed loudly; the
inline summary a later stage reads just described work that had since been redone.
Fixed
- The idempotency guard now compares the whole patch, not just status and verdict
(
skills/worktask/scripts/state-patch.sh). It additionally comparesstages.<CODE>.artifactand, when--previs given, the handoff edge the call would write. Identical inputs still exit early and leavestate.jsonbyte-identical; a changed artifact or summary now re-merges and logsre-merge: ... artifact/handoff differ. Observed twice in one worktask (AR→DVandDV→DR), once on a review agent's own patch.
Added
- Regression coverage for the remediation loop — self-test
T10(same-verdict re-merge refreshes the edge; a third unchanged run stays byte-identical) plus twobatscases intests/shell/worktask/state-patch.bats. Verified against the pre-fix script, which leaves the stale edge in place.
[4.0.7] - 2026-08-06
Branch naming's --goal took the raw task description, so a multi-sentence description
silently truncated mid-phrase into the branch slug — the original incident was operator
error compounded by doc wording that discouraged the cheap fix (a free preview) and hid the
free one (query modes). This release ships the branch-naming R1-R4 fix, R6's linked-worktree
rename behaviour flip, and documentation reconciliation together.
This release changes default git behaviour on every linked-worktree machine (Conductor
and similar host-workspace setups) — see ### Changed — R6 below before upgrading.
Changed
--goalnow takes a concise imperative title, not the raw task description (commands/worktask.md § Step 3c,skills/worktask/SKILL.md § Validation check 10).BRANCH_NAME_PRINT=1is documented as a free preview — no rename, no audit row — and the "do not re-invoke" prohibition is narrowed to rename mode only, so the query modes (--check/--print-types/--print-target) stay free to use.- Truncation is now visible end to end:
branch-lib.shgainsslug_body/slug_budget/slug_is_truncated(derive_slugrefactored onto them);branch-name.shemitsslug_truncated=1conditionally; Step 3c writes abranch_slug_truncatedaudit row; the Step A.5 plan-gate summary surfaces the derived name and the truncation warning.
Changed — refinement window & invariant reconciliation
- The planned branch name gets one pre-commit refinement window. New
skills/worktask/scripts/refine-branch-target.sh, invoked at new Step A.4b, refines the planned target once from the approved plan's owntitle:before the issue is published — six gates, a closed 15-token noop reason set, onebranch_target_refinedaudit row, atomic ledger write, no git mutation, exit 0 always. - Once-only rule reconciled across 8 sites (skills/shared/git-conventions.md, skills/worktask/references/handoff-protocol.md, skills/worktask/references/resume.md, agents/project-manager.md, agents/product-manager.md, commands/worktask.md, skills/worktask/SKILL.md, skills/shared/task-system.md): the rename happens exactly once, at the start of planning; the planned name on the ledger may additionally be refined at most once per run, pre-commit, ledger-only, with no git mutation.
Changed — R6: linked worktrees now rename their own branch
Behaviour flip, opt-out available. branch-name.sh now renames the local branch inside
a linked worktree (Conductor and similar host-workspace setups) instead of deferring to
whatever name the host assigned. is_host_workspace still detects a worktree, but its early
return 0 is gone: a hit now sets IN_WORKTREE=1 and falls through to the existing
target_exists → git branch -m tail, same as any other workspace. On success branch=
and target_branch= are equal.
R6 — why, and the risk
The deferred behaviour assumed the host owns a meaningful, stable branch name. Falsified
live, in this very worktask — Step 3c saw cape-town (a placeholder), and by the
finalization gate Conductor had silently renamed the branch mid-run to
which-stages-ran-tests (derived from the chat topic), so facts.branch diverged from the
actual local branch with nothing detecting it. Neither name was shippable, and nothing in the
old design could tell. Risk, stated plainly: a host that keys its own workspace tracking
to the branch name may need to re-sync after this rename. Documented rather than discovered.
R6 — the opt-out and the new guard
Opt-out: BRANCH_NAME_WORKTREE_RENAME=0 (literal 0 only) restores the previous
defer-to-host behaviour. Subtractive at guard-ladder position 8 — it can only restore a
behaviour a higher guard already permitted, never authorize a rename a higher guard refused.
This is the line an upgrading user most needs to find if their host relies on the old
behaviour. New already_named guard at ladder position 4, keyed on the branch_renamed
audit dedupe key rather than the branch name — a host rename makes the branch
non-conventional again, and a name-based guard would have authorized a second rename.
R6 — divergence detection and disclosure
New fn-preflight.sh branch-divergence (also runs on resume): non-blocking, exit-0-
always, excluded from all. Compares local HEAD against the to of the last
branch_renamed / ok audit row, classifying expected vs third_party, and surfaces only
third_party. Mandatory disclosure: a stdout line at rename time names the host re-sync
risk and the opt-out; the Step A.5 gate summary carries a matching line. Both reach a user
already running the pipeline — neither reaches a user deciding whether to upgrade, which is
why this CHANGELOG entry is the third disclosure surface. 12 reconciliation sites, including
Step 3c's "never revert this step's rename" rule, which was previously moot inside a worktree
and is now live there too.
Fixed
- Audit-scan
fromjson?guard completed against well-formed non-object lines. R1's AD-9 hardening closed the unparsable-line half of this class but left the well-formed non-object half open (123,[1,2],"str"parse cleanly, then abort on the first field access) — and the failure is position-dependent: whether the bad line sits before or after the row a scan is looking for.
Fixed — all four scans, objects load-bearing in each
| scan | jq form | bad line before match | bad line after match |
|---|---|---|---|
branch-name.sh already_named | slurped | rc 5, fails open | rc 5, fails open |
fn-preflight.sh branch-divergence | slurped | rc 5, detection suppressed | rc 5, suppressed |
refine-branch-target.sh once-guard | streaming | rc 0, holds | rc 5, fails open |
refine-branch-target.sh incumbent-truncation | streaming | rc 0, holds | rc 5, fails closed |
Fixed — why "after the match" is the common case, not the edge case
Audit logs are append-only, and the streaming pair's own rows land unusually early: the
once-guard scans for branch_target_refined / ok, written at Step A.4b, and the
incumbent-truncation check scans for branch_slug_truncated, written even earlier at Step
3c — both in Phase 1, before any stage dispatch. Every later stage's row necessarily
accumulates after them, so "bad line after the match" isn't merely the common case for
these two scans, it's very nearly guaranteed. Demonstrated end to end against the
un-hardened once-guard: a second refinement was authorized (ok_rows=2, the ledger value
overwritten); hardened, it correctly declines already_refined (ok_rows=1). All four scans
now use fromjson? | objects; the same gap existed in three test helpers, which is why the
new cases initially failed against already-correct production code — a fixture line must not
be able to break the assertion helper and mask the real result.
Fixed — COVERAGE.md re-measurement
tests/COVERAGE.md's bash@testtotal was stale three times this release: first after QA closed three P2 coverage gaps (AC-7/23-25) inrefine-branch-target.bats(680→683), then a self-contradictory683/53after R6, then a fourth-assertion undercount from a race between two parallel stages. Superseded by a single direct re-measurement taken after both stages settled: 787@testacross 54.batsfiles,branch-name.shnow the highest-density target at 90.
Known limits (deferred, not shipped this release)
- R5 — ticket derivation for a bare GitHub issue number (
#N) is deliberately deferred pending an explicit grammar decision: mint anissue-390-segment, or document that repos minting their own issue numbers stay ticket-less. Never fabricate an alphabetic tracker key from a bare issue number. cache-lint.sh:92(--anchor-lint'sextract_stage()) pipes whole markdown files toyqunder|| trueand is non-deterministic on artifact content — confirmed by review: it passed against one stage artifact and failed against another for no reason but body length. It cannot serve as a gate for any stage until:92is routed through the existing frontmatter-scoped helper at:485. Not fixed here — out of this release's scope; routed to DV for the next touch of the script.skills/agent-coordination/scripts/audit-dedup.sh:84has the identical well-formed- non-objectfromjson?gap this release's audit-scan fix closed elsewhere ([inputs | select(length > 0) | fromjson?] as $rowsthen.value + {__idx: .key}, with noobjectsfilter). Pre-existing, not introduced by R4 or R6, left unfixed here — same one-token fix as the four scans above.
Known limits, continued
state-patch.shidempotency short-circuits when a stage is alreadycompleted, silently dropping a remediation cycle's handoff summary. A remediation round in this very release needed a manualstages.DV.statusreset to re-apply. Not fixed here; needs a design call (always re-merge summary fields vs. document the manual-reset procedure).
[4.0.6] - 2026-08-05
The test-execution gate advertised "DV may not run the full suite" but did not enforce it for any
runner whose mandatory flags are non-selecting. A full xcodebuild test with -project, -scheme
and -destination sailed through, because the classifier saw surviving arguments and concluded the
caller had narrowed the run.
Fixed
- Full-suite runs carrying only non-selecting flags now deny at DV. The classifier strips each
runner's mandatory-but-non-selecting flags before the "did an argument survive?" test. The strip
is quote-aware, which is what the reported incident actually needed:
-destination "platform=iOS Simulator,name=iPhone 16 Pro". Verdicts invert allow→deny forxcodebuild testwith project/workspace/scheme/destination/sdk/arch/result-bundle/derived-data flags,dotnet test <solution>,gradle test -p .,npm/pnpm/yarn test --ci, andcargo test --release. The gradle task token is found order-independently (mirroring the xcodebuild action scan), so the flags-before-task spellinggradle -p . testdenies identically rather than slipping through on the project-dir positional — and the same scan removes a pre-existing false deny:gradle -p . assembleAndroidTest(a flags-first build-only task) no longer reads as a test run at banned stages. Genuine selectors (-only-testing:,--filter,-k,-t,-run, a real positional) still classify scoped and are allowed, at DV and everywhere else. -only-testing:selector spelling. The explicit selector list spelled the Apple selector with two leading dashes; the real flag takes one, so that limb never matched a real invocation and scoped Apple runs classified correctly only via the positional fallback.- Two pre-existing false denies removed.
-cis not a selector forgo test(compile-only) orrspec(--colour), joining the existingbats -c(--count) build-only carve-out. - The DV authority-matrix note documented the bug as the contract. It claimed the deny was "mechanical for the bare-runner form only" and that any trailing flag classified scoped. Rewritten to describe what the gate now does, with its known limits stated plainly rather than buried.
- The finalization stage was instructed to do what it is forbidden — and unable — to do. Its
duty list ("Run final builds and tests"), its completion checklist ("All tests passing in final
build") and the finalization row of the shared pipeline stage table all contradicted the
prohibition the same agent carries. FN holds no test-execution authority, no build/test Bash
grant and no
Skilltool, so even a build-only rewrite would have been unreachable. It now verifies QA's recorded evidence fromtesting-N.mdfrontmatter, mirroring the stakeholder stage. Therequests_test_evidenceescape path is unchanged.
Added
- Narrowest-run default in the developer contract, with its cost attached: verify with the narrowest run that proves the change — build-only for a compile check, a selector for behaviour.
- Fixtures on both surfaces, 24 → 40 self-test assertions and 58 → 80 bats cases, including a finalization-stage deny case kept as a doc-parity lock rather than a bug catch.
Known limits (accepted, documented)
- Whole-tree positionals (
go test ./...,pytest tests/) still classify scoped. Flipping the positional limb's polarity has a different fail direction and is tracked separately. npm|pnpm|yarn test -- -c <spec>denies although genuinely scoped (one shared arm). Moving-cinto the valueless arm would strip the flag and leave its value surviving as a positional, so any full run with a valueless flag before a positional would classify scoped and be allowed — reopening the hole this release closes. These package managers hide the underlying runner, so the ambiguity is irreducible.npm test -- <spec>is unaffected.
[4.0.5] - 2026-08-05
Test-suite stringency hardening. The suite went from 45 files / 501 tests to 53 / 680, every claim in it re-derived by mutation rather than ratified, and four production defects the old tests had characterised as "known bugs" were fixed. One of those four is a security disclosure for downstream consumers and is listed first.
Security
-
DISCLOSURE —
scan-secrets.shnever detected credentials in database URLs. If you have run this plugin's security-review stage in your own repository, your past scans may have missedmysql://,postgres://andmongodb://credentials, and a clean result did not mean they were absent. This is not a newly introduced regression; it is a defect that shipped from the beginning.- What was wrong. The built-in fallback scanner stored each rule as
label|severity|regexand recovered the pattern with${entry##*|}— a greedy strip to the last|. Exactly one built-in pattern,database-url, contains an alternation, so its regex was truncated mid-group tomongodb)://[^@[:space:]]{3,}@— an ERE with an unmatched). - It failed on every dialect, but by two different mechanisms — and neither was noisy. GNU
grepandugrepreject the pattern (exit 2, parse error). BSD/usr/bin/grep, which is what this script actually resolves at runtime on macOS, accepts it and treats the unmatched)as a literal character — exit 1, no diagnostic, matching only the impossible text "mongodb)://…". Either way nothing real was ever matched, and because thegrepcall sent stderr to/dev/null, even the GNU parse error was swallowed. The scan reported completion with no finding and no diagnostic on both paths. - Do not assume a loud failure would have alerted you. On the BSD path there was never anything to see: no error was produced at all, not merely suppressed.
- Which versions are affected: all of them.
git log --reverseon the script shows a single prior commit,01c6f6f(2026-06-29), which introduced both the file and the broken pattern. No released version ever detected these credentials, on anygrepdialect — on GNU because the pattern was rejected, on BSD because it was accepted as a literal that matches nothing. - Blast radius.
scan-secrets.shis a shipped artifact, so every repository that has run the security-review stage since 2026-06-29 has run the broken pattern. The other five built-in patterns (AWS keys, private keys, generic tokens, and the rest) were unaffected and continued to fire normally; only thedatabase-urlclass was blind. Repos configured to usegitleakstook the gitleaks path and are not affected. - What to do. Re-scan with this version. The fixed scanner reports
<file>:<line>:Critical:database-urlfor all three schemes. - Bounding the claim for this repository. A retrospective scan was run here and is
clean: 234 tracked files plus a 200-revision history sweep produced no real finding. Every
hit is either a test fixture on an RFC 2606
.invalidhost or a deliberate literal in the scanner's own--self-test. That result bounds this repository only and says nothing about yours.
- What was wrong. The built-in fallback scanner stored each rule as
-
scan-secrets.shnow validates all six patterns at startup (an O(6) compile check) and no longer sendsgrep's stderr to/dev/null, so a malformed pattern is fatal and named instead of silently producing an empty scan. This is necessary but not sufficient, and would not have caught the original bug — BSDgrepaccepts "mongodb)://" as a valid ERE, so the compile check passes it. Semantic truncation is caught only by the new per-scheme specimen tests. -
Known limitation, unfixed and disclosed:
scan-secrets.sh --self-teststill exercises only 2 of the 6 built-in patterns, and--self-testis exactly what runs on hosts withoutbats. The script's self-contained check remains weaker than its external suite; that coverage was narrowed by zero in this change. -
Known limitation, introduced here: removing the stderr suppression means an unreadable file now emits
Permission deniedsix times (once per pattern) and the scan still exits 0. Not fixed, because the only lever that makes a file unreadable is permissions, and such a test would pass vacuously under root.
Fixed
build-orchestrator.shproduced false dependency cycles. Theblocks?regex also matched "Blocked by", adding a spurious reverse edge and failing the DAG build with exit 5. Now\bblocks?\b.build-context-set.shsilently dropped agent paths on macOS. GNU-only\sescapes mangled leading whitespace under BSDsed, andset -ethen exited the pipeline. All 7 affected sites now use[[:space:]](the original report named only 2).detect-user-changes.shdouble-counted every change. A redundant secondgit diffran in both the patch and the numstat blocks.milestone-helpers.shbase-branch strip was case-sensitive while its detection was not, so aBASE_BRANCH:declaration was detected and then not stripped.sedgains theIflag.state-merge.shcould not recover a corrupt state ledger. It now rebuilds the skeleton and falls through to the unchangedstate-patch.shdelegation, backing up the corrupt file first and aborting without modification if the backup cannot be written.- The Python phase's exit status was discarded, so a failing Python suite could not fail
make test. The suite rc now propagates. - Documentation drift:
tests/README.mdandtests/COVERAGE.mddisagreed with each other (36/36 vs 34/34 targets) and both disagreed with the tree. Both are regenerated from recorded commands. Dead references toskills/agent-coordination/scripts/audit-dedup.shand to acost-log.shthat was never shipped are corrected.
Added
RUN_TESTS_REQUIRE_SWIFT=1— opt-in gate making a skipped Swift phase a failure. The default exit status is unchanged; without it a Swift-less host still exits 0 and reportsSKIPPED PHASES.tests/shell/meta/coverage-proxy.bats— a standing gate requiring every shell script to have a dedicated.batswith ≥3 real scenarios. Exemption list is empty.skills/worktask/scripts/attachments-preseed.sh— renders both Conductor attachments from their canonical templates, replacing six manual writer steps.
Changed
run-tests.sh's clean-clone guarantee now namespython3 >= 3.10. The guarantee was always conditional on it and never said so:validate-export.shembeds a validator using PEP 604X | Noneannotations evaluated at runtime, which raiseTypeErroron 3.9. macOS ships 3.9.6 as/usr/bin/python3, so on a stock Mac the suite returns 1 until a newerpython3is onPATH. This is a pre-existing defect in an untouched file — documented here, not fixed.
Known issues
make coverageis unreachable on macOS: the kcov branch runs away (352 MB of output in 90 s without completing a single target) andbenchmark/ttt-templatesits at 84.5 % against an 85 % gate behind it. The binding green criterion is./run-tests.shrc 0.state-merge.sh's repair writes through a$$-suffixed temp path that still follows a dangling symlink — same weakness class as the one fixed in its backup path, different code path.
[4.0.4] - 2026-08-05
Gate-revision semantics. The spec carried one concept ("a PL invocation") where the pipeline has two — starting a new run (allocate N+1, reset facts, create tasks) versus revising the run in flight (reuse N, edit in place, preserve facts, update tasks). Only the first was specified, so following the plan-gate rejection line verbatim silently forked the run. Both gates now specify their revision path. Plus four rule fixes traced to observed failures in the 4.0.3 worktask.
Fixed
- Plan-gate rejection no longer forks the run (data loss). Re-dispatching PM after a rejection
allocated
planning-1.mdwhile the run was still onplanning-0.md, which (1) wipedfacts.*via the Step-4 state.json reset — erasing thefacts.decisions[]the user had just supplied at that gate, (2) froze the plan under revision as "historical" and split AC baselines across two files, (3)TaskCreated a second stage chain atrun_index: 1, stranding the original taskspendingforever, and (4) bumped the<worktask_id>:<run_index>:gh_issuededupe anchor so the revision read as a later run.commands/worktask.mdnow routes the rejection to § Plan-revision re-dispatch:run_index/plan_filefrozen for the life of a run, PM re-dispatched withplan_revision: true, a four-row BINDING invariant table (edit in place / patchfacts.*additively /TaskUpdatenotTaskCreate/ no issue re-publish),metadata.revision_countbookkeeping with oneplan_revision_dispatchedaudit row, and a re-entry into the plan gate on return. agents/prompt-engineer.mdcould not run its own embedded-command contract. NoSkilltool in the frontmatter grant, so a DV-stage dispatch carrying a mandatorySkill("skill-creator:skill-creator", …)contract had to escalate and apply the skill body manually as a disclosed substitute.Skilladded.- AC verification commands could ship syntactically broken. The
(unverified — dry-run required after theme lands)mark excused everything, including invalidity — a shipped plan carriedgrep -viv -e … -e …(triple negation, a no-op reporting everything "clean") and three downstream stages independently re-derived the correct form. The mark now covers only the RESULT's representativeness, never the command's validity.
Added
- FN-gate rejection resume path (
skills/worktask/SKILL.md § FN gate rejection) — the same conflation at the second gate, which previously ended at "STOP (do NOT delegate FN)" with no defined resume. Now symmetric with the plan gate: feedback routed to the owning stage (DV fix round / DC re-stamp / plan amendment),run_indexfrozen, completed stages never re-run wholesale,revision_countbookkeeping, and the FN gate re-presented on completion. - DV-stage yield discipline for
agents/prompt-engineer.md— across one worktask it stopped mid-theme at least four times, each needing an orchestrator nudge to finish its own stage contract. Now points atagents/workflow-engineer.md § Batch-Completion Discipline (DV execution), the rule that already existed but was unreachable from this agent. file:linecitations are starting sets too (agents/product-manager.md) — the staleness rule that already covered enumerated FILE lists now covers LINE numbers cited in theme prose: cite as an approximate locator, require DV to re-locate the anchor by quoted text before editing.
[4.0.3] - 2026-08-05
Full legacy-logic removal. Round 1 removed every rules-surface deprecation whose sunset window had
elapsed from agents/, commands/, skills/ with zero semantics change. Round 2 (user-approved,
aggressive scope) then deleted the remaining legacy logic across hooks, scripts, tests, and the
benchmark harness — accepting loss of pre-4.0 .context/ read-support. Load-bearing current
contracts (the F1–F4 fallback family, the plan_file path-vs-basename boundary, hook-vs-agent audit
authority, CC --json camelCase coalescing, visual-QA degradation invariants) are kept but reworded
away from the misleading word "legacy" — they are current behavior, not compatibility.
BREAKING
- Legacy
--auto-plan/--auto-finalizationflags removed. Only--auto=[plan, decision, finalization]remains. These aliases are no longer recognised or documented; no rule inworktask.mdgoverns an unrecognised top-level flag, so the effect of passing either legacy flag is undefined by the docs going forward. requires_ui_testsback-compat mapping removed. A resumed pre-4.0.context/planning-N.mdthat still presentsrequires_ui_tests: truewith notest_modenow falls to thescopeddefault andui_visual_check: false— a legacy plan that asked for full regression plus Design Comparison silently gets neither, with no deprecation note surfaced.- Dual dedupe-key mechanism deleted.
metadata.dedupe_key_extendedis no longer written by any hook andskills/agent-coordination/scripts/audit-dedup.sh(the base/extended mode selector) is removed. The 3-segmentdedupe_keyis the only documented shape; readers dedupe on it directly.parent_agent_idis still captured, for observability only. - Pre-run-index artifact names no longer resolve. The artifact resolver's bare-basename rung
(
.context/<base>.md) is gone fromstate-patch.shand from every documented resolver chain — only<base>-<run_index>.mdand the newest-glob fallback remain. - Pre-#375 issue-title probe removed.
publish-pl-issue.shno longer buildsTITLE_LEGACYor re-probes the old slug title, so an issue published under the pre-#375 scheme is no longer found by the anchor-loss recovery search and a duplicate may be opened. state.json .git.base_branchfallback removed from the integration-branch chain; the surviving order is$FN_BASE_REF→state.json .metadata.base_ref→workspace.json .git.base_branch→origin/HEAD→ unresolved.- Single-file visual-evidence marker shape dropped.
attach-visual-evidence.shreads only the per-issue marker directory;GH_MARKER_FILEis no longer honoured. - Redaction supersets removed — leak risk accepted.
publish-pl-issue.shno longer strips theigrsoft:plugin prefix or the pre-3.42.0analyzing-N.mdartifact name. A worktask resumed from a pre-4.0.context/can therefore publish those internal identifiers into a GitHub issue body. - Strict benchmark token decoding. A
tokensblock present in a benchmark record MUST carry all five keys (in,out,total,cache_read,cache_creation); a partial block now raises instead of decoding with silentNones.benchmark/results/history.jsonwas migrated in place (key-presence-only, explicitnulls). state.mcp_session,--severitylegacy aliases, and the bare-name agent shim dropped.metadata.agentandsubagent_typeMUST be fully-qualifiedplugin:agent;--severityaccepts onlyP0/P1/P2;mcp_sessionis gone from the state-ledger schema.
Removed
- Legacy
--auto-plan/--auto-finalizationaliases and the#### Legacy aliases (deprecated)table (5 files +README.md). - Sunset
requires_ui_testscompat-mapping table (2 files, 6 sites). igrsoftrename residuals outside the vendor-identity allow-list (7 files).- Stale pre-4.0 (
v3.x) version gates: 21 of 42 matches removed in round 1; round 2 removed the rest, including everyv3.x+header peg across the 8 remaining hook/skill scripts. - Dead code:
_inline_merge(the ~105-line inline state merger in.claude/hooks/state-merge.sh) and its call site —state-patch.shis the single merge implementation. - Files: skills/agent-coordination/scripts/audit-dedup.sh, skills/shared/legacy-fallback-f1.md (folded into handoff-protocol.md#f1-fallback), tests/shell/hooks/audit-dedup.bats, benchmark/harness/tests/test_history_backcompat.py, and 3 audit fixtures.
norm_dkcross-shape key normalisation fromskills/agent-coordination/scripts/audit-dedup.sh(it normalised a 4-segment key shape no writer ever emitted).- The
fallback_legacyvalue from theartifact_path_resolvedaudit enum (now{ok, fallback_glob, miss}) — unreachable once the bare-basename rung was removed.
Changed
- Compaction pass across
agents/,commands/,skills/: removed change-narration, deduped repeated canon to a single source + pointers, collapsed redundant structure. Total tracked markdown: 34,119 → 34,035 lines (agents/4,722 → 4,694;commands/9,329 → 9,292;skills/20,068 → 20,049). - Reword sweep (rename-only, no behavior change): F1 is now "
context_filesmode" and is documented as required behavior rather than backward compatibility; thehandoff-harness.shA/B arm is "baseline" rather than "legacy";milestone-helpers"Legacy Command" is "Non-worktree Command"; visual-QA degradation invariants are canonical invisual-qa.mdwithtesting-strategy.mdpointing at them. build-context-set.shno longer exits non-zero when a probed candidate path does not exist (each qualified agent ref probes agent/command/skill paths; a miss is normal).
[4.0.2] - 2026-08-03
A live worktask in a Conductor workspace shipped PR #382 from ov166-skin-score-all-layers
— a branch name that fails this plugin's own branch_is_conventional predicate, on a repo
that merges rather than squashes, so the name is permanent history. The design was already
right (FN pushes HEAD:refs/heads/<facts.branch>, decoupling the PR head from the local
branch); it was fed a bad value. Scripts, tests, and docs only — no stage, agent, or gate
semantics change.
Fixed — the derived branch target is no longer discarded on the no-op arms
branch-name.sh derived the conventional target inside the rename arm alone. Every other
arm — upstream tracked, target exists, jq unavailable — passed the current branch to
emit_branch, which prints branch= empty when the name fails the predicate. The right
answer was computed and thrown away, facts.branch landed empty, and FN's documented
fallback pushed the non-conventional local name as the PR head.
- A second stdout line,
target_branch=<name>, emitted by every arm that can derive one, independent of whether the local rename happened. Derivation moved above the no-op arms;derive_targetsplit out as a pure function.branch=keeps its exact semantics and stays the final stdout line — four docs and every consumer specify that parse, so the new line goes before it, not after. Both lines pass the samebranch_is_conventionalgate before emission, preserving the shell-injection hardeningemit_branchexists for. - Arms that must not propose a target still emit it empty: already-conventional
(
branch=is the answer), the integration branch (never a PR head under any name), and batch/incident routing (which owns its own naming). - Step 3c now stamps the target it already computed. The post-check wrote
derived_targetinto an audit row and discarded it; the orchestrator now stampsfacts.branchfromtarget_branch=wheneverbranch=is empty or fails--check, and the post-check reuses the captured value instead of re-invoking the script (which wrote a duplicate audit row). A non-emptyderived_targetin that warning row is now a bug report, not a shrug.
Added — host-workspace (linked-worktree) arm
Nothing in the guard ladder could see a host workspace: Conductor leaves no workspace.json
in $PWD, so all five fn_batch_scope signals miss it. New arm, detected as
git rev-parse --git-dir != --git-common-dir: derive and emit the target, skip the local
git branch -m, audit branch_renamed / skipped with reason: host_workspace_worktree. The
host's branch↔workspace mapping stays intact and the PR gets a conventional head — what
workspace-modes.md § Host mapping caveat previously described as a manual equivalent is now
the automatic behaviour. facts.branch deliberately differs from the local branch name there;
that divergence is documented in handoff-protocol.md and agents/project-manager.md as the
designed outcome, so no reader "repairs" it back to a git rev-parse.
Fixed — the host-session authorization was invisible at the point of decision
workspace-modes.md § Host session authorization ("invoking /worktask satisfies a host's
no-rename session rule") was referenced from exactly one file: the FN agent, which runs
long after the rename and never renames anything. The orchestrator at Step 3c — the only
actor that decides — had no pointer, and Step 3c's own "MUST NOT block, no STOP, no retry,
no rename" text reads as tolerate the divergence. An orchestrator holding Conductor's
session rule and Step 3c had everything it needed to revert a correct rename. It did. Step 3c
now carries the authorization inline as a BINDING note, in commands/worktask.md and
skills/worktask/SKILL.md.
Tests
Twelve new cases in tests/shell/worktask/branch-name.sh.bats (57 total, all green): the
host-workspace arm over a real git worktree (local name kept, target derived, HEAD
untouched, audit reason); the acceptance trio executed through the documented Step 3c
stamping rule rather than paraphrased; both cwd cases for the detection itself — a
subdirectory of a plain repo must still rename (git answers --git-dir absolutely and
--git-common-dir relatively from there, so the comparison resolves both to physical
paths), a subdirectory inside a worktree must still detect; target_branch= on the
upstream-tracked, target-exists, and jq-unavailable arms; empty on the arms that must not
propose one; batch routing still winning over the host arm; a shell-hostile current branch
reaching neither emitted line; and the BRANCH_NAME_PRINT dry run unchanged.
[4.0.1] - 2026-08-03
Two publication-surface bugs, both found the same way: a live worktask shipped output nobody
would have written by hand — a fix/-prefixed branch after fix had been removed from the
vocabulary, and a GitHub issue titled with a kebab slug. Neither was a failure; both paths
degraded silently and stayed within their non-blocking contracts while doing it. Scripts,
tests, and docs only — no stage, agent, or gate semantics change.
Fixed — worktask branch naming (four defects on one path)
A live worktask shipped fix/catalog-image-blinking — a branch whose type prefix had been
deliberately removed from BRANCH_TYPES. The root cause was not the vocabulary but the step
that enforces it.
- Step 3c was skippable.
commands/worktask.mdtold the orchestrator to runbranch-name.shbut nothing verified that it did, so on a workspace already sitting on a plausible-looking branch an orchestrator could reasonably conclude "already named, skip" and stamp the pre-existing name intofacts.branch. Step 3c is now unconditional, with the reason stated (the "already conventional" arm is a no-op, so always running it costs one process and is the only correct way to decide), a BINDING line that conventionality is decided bybranch_is_conventional()and never by eye, an explicit pre-existing-branch clause, and a non-blocking post-check that emits onebranch_convention_checkwarning row naming both the actual and the derived target when the stamped value fails the predicate. - The grammar gained an optional ticket segment —
<type>/[<ticket>-]<slug>. Repos using this plugin already ship bugfix/ov-156-…, which the spec (documented as the single source of truth) previously forced to drop its issue key. The newderive_ticketreads the first\b[A-Z]{2,}-\d+\btoken from the goal text only, lowercased; no key found means unchanged behaviour.branch_is_conventionalaccepts both shapes, so no existing branch becomes non-conventional and gets churned. Branch ticket and PR closing keyword are complementary, not alternatives. derive_slugno longer truncates mid-word.cut -c1-48produced…-blinking-before-r; truncation now drops the trailing partial segment and always keeps at least one whole word. The ticket prefix is budgeted inside the 48-char cap, so a long issue key cannot starve the slug, and the key is stripped from the slug body so it appears once.derive_typeno longer misclassifies bug reports as features. The*"fix "*arm required a trailing space, so "…investigate and fix" and "…and fix." fell through tofeature.fixis now matched as a word (without swallowingprefix/fixture), and the bugfix arm gained the defect vocabularyblink,flicker,glitch,broken,regression,incorrect,wrong,fails,failing. The hotfix-before-bugfix ordering is unchanged.
Guard ladder unchanged — every arm is still a no-op or a refusal, never a failure. Commit-type
table untouched: feature stays branch-only. +26 bats cases across branch-lib.bats and
branch-name.sh.bats.
Fixed — publish-pl-issue.sh published a garbage title and an empty Summary
Issue #375 was published as OV-164 ov-164-catalog-image-blinking with an empty ## Summary.
facts.goal was the ONLY source for both, and it is OPTIONAL in the handoff protocol — written
only when the PM agent patches state.json, so orchestrator-inline seeding, a hand-authored
.context/, or a regenerated state left it unset and degraded both outputs at once, silently.
- Title and Summary now resolve through independent chains, first non-empty wins. Title:
facts.goal→ plan frontmattertitle:→ plan first H1 → first sentence of## summary/## problem→worktask_id. Summary:facts.goal→## summary→## problem. The worktask slug is deliberately not a Summary rank — an empty section is honest, a slug posing as prose is not. - New readers, POSIX sh + awk in house style:
extract_frontmatter_field(leading---fence only, so a thematic break mid-body is never mistaken for frontmatter),extract_first_h1,first_sentence(skips bullets; a terminator must be followed by whitespace, so3.5sis not a sentence end). Thehead -1 | cut -c1-100 | sanitise_bodypipeline still applies to every rank. - Reaching the
worktask_idrank is audited, not silent: one advisory row withreason: "title_fallback_worktask_id"andmetadata.title_source, on the<dedupe_key>:title_sourcesuffix so it never masks the canonical outcome. Still non-blocking. - External-ticket extraction gained ranks (winning title source, then plan frontmatter
issue:) and its no-double-prefix guard is now case-insensitive and accepts-as a separator — the exact-case check is whyov-164-…was treated as unprefixed and doubled. - Recovery-search compatibility: changing title generation orphans issues published under
the old title, so
resolve_context_issue_search()probes the current title first and the legacy one only on a miss. Documented as removable once that corpus is closed.
Secondary (separate commit): commands/worktask.md Step 3a now seeds facts.goal from the
task description, skills/worktask/references/initialization-patterns.md carries it in the
seed snippet, and agents/product-manager.md states that a non-empty facts.goal is part of
the PL state-patch contract. Nothing on the patch path actually wrote the field before. The
script fix stands on its own regardless — a missing optional field is not a reason to publish
a broken title. +5 --self-test blocks (13a–13e) with three new plan fixtures.
[4.0.0] - 2026-07-31
The plugin was declared as "name": "igrsoft" while the repository had already become
IGRSoft/company-workflow — the plugin id was the last artifact carrying the vendor name as its
identity. This release renames the plugin, and only the plugin.
The word appeared in three distinct roles, and separating them is the whole substance of the
change. Plugin identity moves: every igrsoft:<agent|skill> invocation id, the
marketplace.json plugin entry, the plugin.json Stop hook matcher, the Task(igrsoft:…)
frontmatter grants, the bare-name resolution shim, and the Claude Code install cache path.
Vendor identity does not move: the author block (IGRSoft, support@igrsoft.com), the
github.com/IGRSoft/… URLs, the com.igrsoft.* bundle IDs in /appstore-iap, and the
marketplace name — which stays igrsoft, so the cache path becomes
~/.claude/plugins/cache/igrsoft/company-workflow/<version>/ with only the second segment
changed, and the install key becomes company-workflow@igrsoft.
Most of the ~330 references were prose or ids, but four sites matched the literal string at runtime and would have failed silently rather than loudly:
hooks/dv-screenshot-gate.shguards on exact equality (!= "company-workflow:developer"). Left stale, the DV screenshot gate would have no-opped on every run with no error.skills/self-improvement/scripts/build-context-set.shmaps agent refs to file paths via an awk field compare ($1 == "company-workflow")..claude-plugin/plugin.json'sStopmatcher must track the plugin name or the PL/FN approval-gate push notification stops firing.skills/worktask/scripts/publish-pl-issue.shcarries four identical copies of the plugin-prefix leak regex that strips internal agent ids out of published GitHub issues; they are kept byte-for-byte in lockstep with the list inskills/shared/compatible-plugins.md.
Breaking
igrsoft:*agent and skill ids no longer resolve. There is no back-compat alias. The six sibling plugins (apple-developer,system-developer,android-developer,frontend-developer,backend-developer,ai-engineer) are updated in the same pass; merge this release first, since their docs referencecompany-workflow:ids.IGRSOFT_*environment variables renamed toCOMPANY_WORKFLOW_*with no fallback read —TEST_GATE,AR_REF_STRICT,PR_BODY_STRICT, andCOMMENT_DENSITY_{MAX,WARN,MIN_LINES}. Update shell profiles and CI jobs.- Stale installs must be reinstalled.
~/.claude/plugins/cache/and theknown_marketplaces.json/installed_plugins.jsonindexes still key on the old plugin name until then. audit.jsonlis not comparable across the boundary — thesubjectfield changes prefix, so pre- and post-rename audit trails cannot be diffed directly.
Earlier releases are archived per major line: CHANGELOG-3.x.md (3.0.0–3.43.0), CHANGELOG-2.x.md (2.0.0), CHANGELOG-1.x.md (1.0.0).