Skip to content

The cycle's missing stages are built: a spec artifact, an implement workflow, a command per workflow (0.12.0) - #154

Open
betmoar wants to merge 16 commits into
mainfrom
claude/workflow-ux-spec-d6fupz
Open

betmoar wants to merge 16 commits into
mainfrom
claude/workflow-ux-spec-d6fupz

Conversation

@betmoar

@betmoar betmoar commented Sep 20, 2026

Copy link
Copy Markdown
Owner

What

The engagement cycle gains the two stages it never had — a spec artifact and an implement workflow — plus the command surface that reaches them, the stage derivation that tells a session where it stands, and the gitignore migration the spec artifact needed. Ships as 0.12.0. Design rationale: docs/CYCLE.md.

Commit
b7e4e3f docs/CYCLE.md — the spec stage (§2–§4), plus three dangling docs/spec/ refs repaired
445f570 §5 — the implement stage, measured
7764d00 §5.4 decided: dispatch.js declines to choose rather than promoting
6cff0bc code — the dispatch.js resolution ladder
5beb34b codeworkflows/implement.js + check_implement_packet
b223b2d code — a command per workflow, resolving the tiers itself (#75)
f6d9b6f codescripts/lib/stage.sh + the SessionStart banner (#157)
8631e8d rung 3's premise measured, not assumed (#158)
9e63e8f the SC2034 directive stage.sh shipped without (CI red on f6d9b6f)
beb2fb9 the charter names the workflow; SC2329 pre-empted; a CLAUDE.md extraction (#159, #160)
25c4caf code — the gitignore scheme takes a third version without destroying a second (#156)
beb1c36 codescripts/ops-spec.sh, the spec artifact and its approval stamp (#155)
4c77101 code — the SPEC and PLAN rungs the artifact made derivable (#157)
44cba6f review round 1 — the append-fusion P0 and three more
3fed949 review round 2 — check_cr_strip_parity's vacuity, CHANGELOG, 0.12.0
f025c04 review round 3 (Copilot) — the open-questions check's false name, a vacuous dispatch assertion

Gap 1 — the spec stage had no artifact (§2–§4) — BUILT (#155)

The handoff from divergence to planning was a source comment in workflows/brainstorm.js pointing at docs/spec/, which does not exist (emptied in 0.11.9; three tracked refs still pointed at it — the F61 class, regressed, repaired here). So args.spec was a string in the operator's context: no file, no provenance, no stamp, no ledger row, nothing that survives a compaction.

scripts/ops-spec.sh writes .operator/specs/<slug>.md (--new), checks it against the schema (--check) and stamps approval (--approve). The stamp writes a SPEC-APPROVED row to DECISIONS.md and the BAR block to VERDICTS.md, both under the SAME .operator/.lock that ops-verdict.sh and ops-adopt.sh take, in that order — so a concurrent verdict row lands whole, never inside the BAR block. check_root_parity now holds FOUR copies of the project-root block, check_lock_parity THREE writers.

Gap 2 — the implement stage had no workflow (§5) — BUILT (#158)

$ grep -rn "IMPLEMENT" workflows/ ; echo $?
1
$ grep -n seat_add scripts/ops-render.sh
81:seat_add mechanic IMPLEMENT default

One of the four canonical tiers, the tier the implementer is bound to, dispatched by nothing. Every stage that only READS ran as a workflow with a tier map; the one stage that WRITES CODE was a plain Agent call against a hardcoded alias.

  • dispatch.js — the ladder is args.modelargs.tierno model key at all. model || JUDGMENT dispatched the IMPLEMENT-bound mechanic on the judgment default and logged that it had: honest, not correct. Tier validation went lazy, which is required, not incidental — with four tiers nameable and one reached per call, an eager loop resurrects what PR #76 sequence: single source of truth by removing responsibilities, not policing copies #78 removed.
  • workflows/implement.js — refuses an incomplete packet with zero agents dispatched, naming every defect at once; serial, structurally (no parallel( in the file, because one implementer at a time is [D:CHART-r6] and prose does not hold it); four-status protocol as a schema enum, changed as an array; stops on a dead seat; returns the unioned CHANGED and stops at the sandbox boundary.
  • check_implement_packetPACKET_FIELDS is the fourth hand-copy of the charter's packet and the first in code: both directions pinned, plus the application, because a field required by the refusal and dropped before the prompt is worse than one never required.

Rung 3's premise is measured (2026-09-21). Two seats, same agentType (frontmatter model: opus):

seat recorded opts served by
omitted no model key claude-opus-5
explicit "model": "haiku" claude-haiku-4-5-20251001

Gap 3 — a command per workflow (#75) — BUILT

commands/{spec,brainstorm,plan,implement,review,crawl,debate}.md, each resolving the bindings with ops-tiers.sh --json and passing the line through. That removes the hand-paste that is #55 at the call site. dispatch gets none on purpose. Pinned per command: the resolution, the grant covering the prescription (#104), and that each dispatches its own workflow — coverage, not equality: the first version demanded the literal ops-tiers.sh in allowed-tools and failed when implement.md's grant got wider.

The artifact route and the tutorial stay open on #75, explicitly deferred.

Gap 4 — nothing could say which stage an engagement is in (#157) — BUILT

scripts/lib/stage.sh is a pure function — it opens no file, so it adds no reader, no byte cap, no second copy of the partition rule. That is what lets the Stop hook (every scan) and SessionStart (only the pending one) share one derivation: they differ in what they KNOW, and the unknown input gets a narrower answer, never a guess. Rungs BLOCKED > IMPLEMENT > HANDOFF > SPEC > PLAN > CLEAR, each making the next unreachable; a foreign task is reported, never a stage; an unscanned deviation gate (-) is said, never assumed clean. The result goes in the SessionStart banner — the only channel a session has after a compaction.

No ops-stage.sh CLI: the derivation ships where it is consumed.

Gap 5 — the gitignore allowlist could not be extended (#156) — BUILT

v1→v2 REPLACES, because a blocklist and an allowlist are contradictory schemes and merging them yields neither. v2→v3 APPENDS, because v3 only adds specs/ to a scheme v2 already established — a replace there would have destroyed every hand-added rule. Both writers carry it; check_gitignore_parity pins both halves, and its live-target detection now counts reads (≥2): the additive arm's own grep satisfied the single-read pin, so deleting the destructive arm's detection shipped green.

Review rounds — what they found

Round 1, P0, reproduced before fixing: the v3 append FUSED with an unterminated last line. >> appends at the byte offset the file ends at, so a .gitignore whose last line carried no terminating newline came back as

!my-hand-added.md!specs/

— the user's rule destroyed, !specs/ inert, and the v3 marker still landing so the migration never retries. Both writers terminate the file first; five cases, and the mutation removing the guard from BOTH writers turns all five red.

Also round 1: ops-spec.sh took the lock it needed (LOCKDIR was undefined before the copied LOCK BLOCK — the first live --approve died on an unbound variable); check_lock_parity extended to three writers; the ops-init refusal path; three prose claims corrected.

Round 2, three vacuities in this PR's own gates:

  • check_lock_parity compared ops-verdict.sh against ops-adopt.sh only, so ops-spec.sh's copy was held by the content pin alone — and the content pin passes anything that still looks like a lock. Measured: LOCK_SPINS=100 in ops-spec.sh alone reported nothing. Parity now runs against one reference copy, so a fourth writer cannot drift unseen either.
  • The good-tree fixture omitted ops-spec.sh's lock block, so check_lock_parity returned early on the missing file and all four LockParityTest mutation cases went green against a check that never ran — the "Mutation-checked" does not record WHICH gate went red — a pin can be vacuous while its mutation is caught by a different suite #111 shape, inside the suite written to prevent it.
  • check_cr_strip_parity's if "_cr" not in code: continue skipped exactly what the realistic simplification produces. Measured on the real tree: ops-reverify.sh's loop replaced by the CR handling after #136: \r\r\n still fails the cap OPEN, check_cell admits a mid-cell CR, and the byte cap reads 1.2% loose #139 issue's own rejected ${row%%$'\r'*} with _cr dropped from its local line left validate_plugin: all contracts hold. (The narrower mutation the review reported — the loop deleted, the local line kept — fires even without this fix; the case is written against the one that actually escapes, and says so.)

Round 3 (Copilot), two findings — one real, one whose premise does not hold. Both verified by measurement before anything was changed; both threads answered with those measurements and resolved.

  • Real, and it made the pin vacuous. The dispatch no-override assertion read "model" in dFallCall — the test stub's RECORD, which always carries a model property because the stub writes one — so it collapsed to model === undefined, while the hasModelKey recorded for exactly this distinction went unused. Its own comment claimed it asked the options object. Measured: dispatch.js sending { model: undefined } on that rung kept the node suite at 430 passed, 0 failed. Now hasModelKey === false, control asserts === true; same mutation → 1 red.
  • Half real. The premise (the skeleton's || headers are miscounted) does not hold: the skeleton writes | Question | Resolution | Decided by | and |---|---|---|, both matched by the existing filters, and a fresh skeleton reports no open-questions problem. The other half does: grep -c '| *|' matches an empty cell ANYWHERE in the row, so | Can we X? | Yes | | — answered, nobody named — was refused as an "empty Resolution cell". A true refusal under a false name is worse than no message: it sends the operator to fix a cell that is already correct. Now cell-addressed, with Decided by given its own message and a sub-three-cell row failing CLOSED as unanswered.

Mutation evidence

Every pin driven red before it was believed, naming the gate:

Mutation Red in
restore model || TIERS.JUDGMENT 2 node cases + a control proving the absence check can fail
tolerate an unknown args.tier 1 node case (the message is pinned too)
re-add eager tier validation 1 node case (F07's unused-key property)
packet: dropped / extra field, validated-then-unsent, application in a comment, dropped status, charter losing a field or status, missing file, renamed locator 9 python cases in check_implement_packet
--show instead of --json, a dropped Workflow grant, a deleted command 3 bash cases
disable the HANDOFF rung, invert IMPLEMENT/HANDOFF precedence, drop the banner line 5 bash cases
the terminate-first guard removed from BOTH gitignore writers 5 bash cases
LOCK_SPINS=100 in ops-spec.sh alone test_third_writer_drift_fires
_cr gone from ops-reverify.sh entirely test_a_copy_that_drops_the_counter_ENTIRELY_fires
{ model: undefined } on dispatch's no-override rung 1 node case (was 0 before f025c04)
the | *| substring scan restored in ops-spec.sh 3 bash cases

Serialization is pinned by a source scan with a brainstorm.js control — the stub runs parallel() thunks sequentially, so a concurrency counter would be vacuous.

Gates — measured on head f025c04

python3 scripts/validate_plugin.py    -> all contracts hold
bash scripts/gate-suite.sh shell      -> 1209 cases (floor 1101 -> 1209)
bash scripts/gate-suite.sh python     ->  411 cases (floor  397 ->  411)
bash scripts/gate-suite.sh workflows  ->  430 cases (floor  384 ->  430)
bash scripts/gate-suite.sh compress   ->  161 cases (floor  161)
shellcheck 0.11.0, no exclusions      -> rc=0

CI on this head: validate run 341 success, base-gate run 31 success — so the tree is also clean at CI's pinned shellcheck 0.10.

Every floor raised in the same commit as its cases, with the measurement written into tests/floors.env. validate went red on f6d9b6f/8631e8d for a real lint of mine — stage.sh missing the SC2034 directive its three sibling libs carry — reproduced locally and fixed in 9e63e8f.

Scope

templates/OPERATOR.md changes by one word. It stayed untouched through the first nine commits; beb2fb9 adds implement to ORCHESTRATED MODE's primitives sentence, because the charter was listing the primitives while omitting the workflow that enforces its own one-implementer-at-a-time rule. 143 lines / 8884 bytes against the 150 / 9000 caps, re-measured on this head. No workflow gained a tool it cannot have.

CLAUDE.md is structurally full and this PR demonstrates it rather than solving it (#159): three new coupling rows and one amendment forced a second extraction to stay under the 38000 cap. Two rows were corrected while there — stage.sh's ladder never recorded the SPEC and PLAN rungs, and the provenance bullet called the spec stage unbuilt after #155 built it.

Follow-ups

#159 the CLAUDE.md headroom decision · #75 the artifact route and the tutorial (both explicitly deferred) · docs/CYCLE.md §4's plan gate stays unbuilt: the spec artifact is stamped, but nothing yet refuses a plan run whose spec is unapproved. #158, #160, #155, #156, #157 close with this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun

The cycle goes brainstorm -> ??? -> plan, and the only thing bridging the
gap is a source comment in brainstorm.js telling the operator to write the
approved design to docs/spec/ — a directory that emptied in 0.11.9. So the
one artifact carrying a design from divergence into planning is prose the
operator retypes: no file, no provenance, no source stamp, no ledger row,
and nothing surviving a compaction. The evidence gate's own premise, not
applied to the stage that needs it most.

docs/CYCLE.md specifies the three pieces that close it, design only:

- the spec artifact — .operator/specs/<slug>.md, tracked, fixed section
  order, a north star carrying `Missed if:` that BOTH the BAR block and
  plan.js's args.northStar read (today there are two north stars, written
  by hand, with nothing linking them);
- the approval stamp — ops-spec.sh --approve reuses ops-verdict.sh's
  source_stamp ladder, logs a SPEC-APPROVED record kind, and emits the BAR
  block the charter requires by hand today;
- the derived stage — ops-stage.sh computes where the engagement is from
  artifacts on disk, report-only, sharing lib/partition.sh. Derived, never
  stored: UNKNOWNS.md's rule about status in two places.

It prices what it proposes rather than leaving it to the fourth review
round: a tracked .operator/specs/ needs the gitignore allowlist bumped to
v3 in both writers, and the migration's backup path is hardcoded
two-state, so a v3 migration writes a .v1.bak that lies about its contents.

Also repairs three dangling docs/spec/ references (README, CONTRIBUTING,
docs/HANDOUT) — the F61 class, regressed since it was last fixed.

Gates: validate_plugin green; 397 python, 1085 bash (16 skipped, uid 0),
384 + 161 node — all passing, unchanged from baseline (docs-only diff).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
…CLE §5)

Measured: `grep -rn IMPLEMENT workflows/` returns nothing across all six.
IMPLEMENT is one of the four canonical tiers (ops-tiers.sh TIER_NAMES,
default claude-sonnet-5, documented in README and commands/tiers.md), and
ops-render.sh binds `seat_add mechanic IMPLEMENT default` — so the one seat
on the tier is the one seat no workflow can reach on it.

Two routes exist and both are wrong for an engagement in flight:

- a plain Agent call reads the agent file's frontmatter alias, resolved at
  session start, and the harness's Agent tool locks `model` to the enum
  sonnet|opus|haiku|fable — re-measured against this session's own tool
  schema on 2026-09-20, the re-check commands/tiers.md asks for. A cc-proxy
  id is refused before dispatch (#55, still true). The render route applies
  the binding globally and needs a session restart, mid-engagement, which is
  the event the RECOVERY PROTOCOL exists to survive.
- dispatch.js resolves `model || JUDGMENT` with only JUDGMENT in its
  DEFAULT_TIERS, so a mechanic dispatched without an explicit args.model —
  the IMPLEMENT-tier seat — runs on the judgment default. The log line says
  which happened, which makes it honest, not correct: a silent tier
  PROMOTION, and #153 already measures what a mis-bound tier costs.

The asymmetry names the defect: every stage of the cycle that only READS
runs as a workflow with its own tier map; the one stage that WRITES CODE is
a plain Agent call against a hardcoded alias.

§5 specifies workflows/implement.js — the first workflow to dispatch
IMPLEMENT, taking the dispatch packet as structured args and refusing an
incomplete one before spending a seat (#152 landing where it belongs),
serializing implementers so the charter's one-at-a-time rule becomes a
mechanism rather than prose, and stopping at the REPORT because the sandbox
has no filesystem and cannot touch the ledger. §5.4 carries the smaller
independently-shippable fix to dispatch.js's fallback, with the constraint
that the seat->tier binding must not gain a second declaration.

Gates: validate_plugin green; docs-only diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
@betmoar betmoar changed the title docs: specify the engagement cycle's missing spec stage (#75) docs: specify the two stages missing from the engagement cycle (#75) Sep 20, 2026
…YCLE §5.4)

Decision on the open question in §5.4. The ladder is args.model, then
args.tier resolved against the caller-supplied map (with IMPLEMENT and
MECHANICAL added to DEFAULT_TIERS so a bare invocation resolves), then —
when neither is named — NO `model` key in the agent() options at all.

The current `model || JUDGMENT` invents a choice: it promotes an
IMPLEMENT-tier seat to the judgment default and calls that a defensible
fallback. Omitting the key declines to choose, which is the only honest
move available to a workflow that cannot read tiers.env, and it leaves the
decision with the layers that already own it — the agent file's frontmatter,
a project-layer agent from ops-render.sh, $CLAUDE_CODE_SUBAGENT_MODEL, or a
CLAUDE.md instruction. Renderer and dispatcher stop competing: render sets
the standing default, dispatch overrides per call, and an absent override no
longer overwrites that default with a third answer.

Recorded as an assumption, not a measurement: what this repo has measured is
the converse — opts.model OVERRIDES the agent file's frontmatter
(2026-07-29, cited in review.js). That an OMITTED opts.model leaves the
frontmatter in effect is the expected complement and owes a live dispatch
against a seat with a distinguishable frontmatter model before it ships. If
it comes back otherwise, the rung becomes an explicit refusal.

Unchanged: the seat->tier binding gains no second declaration. It stays in
ops-render.sh's seat_add lines, and args.tier is named by the caller so the
workflow never holds that map.

Gates: validate_plugin green; docs-only diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
…158)

The fallback was `model || JUDGMENT` with only JUDGMENT in DEFAULT_TIERS, so
a seat dispatched without an explicit args.model ran on the judgment default
— including `mechanic`, which ops-render.sh binds to IMPLEMENT
(seat_add mechanic IMPLEMENT default). The log said which happened, which
made it honest, not correct: a silent tier PROMOTION, the cost class #153
measures, reached by a different door.

The ladder now:

1. args.model — an explicit id, same charset guard a tiers.env binding gets.
2. args.tier — a tier NAME (case-insensitive) resolved against the caller's
   args.tiers map. DEFAULT_TIERS gains IMPLEMENT/MECHANICAL/RECON so a bare
   invocation still resolves, to that tier's own harness alias and never to
   JUDGMENT's. An unknown name is REFUSED, never defaulted.
3. neither — no `model` key in the agent() options at all. The seat runs on
   its own configured default: agents/op-<seat>.md frontmatter, a
   project-layer agent from `/cc-operator:tiers render`, or
   $CLAUDE_CODE_SUBAGENT_MODEL. Renderer and dispatcher stop competing —
   render sets the standing default, dispatch overrides per call, and an
   absent override no longer overwrites that default with a third answer.

TIER VALUES ARE NOW VALIDATED LAZILY, and that is required rather than
incidental. Eager validation of every TIERS key was sound while JUDGMENT was
the only dispatchable tier: every key was reachable on every call. With four
tiers nameable and at most one reached per call, an eager loop resurrects
what PR #78 removed one level down — a malformed value on a tier THIS CALL
never touches failing the run, which makes forwarding the resolver's whole
map unsafe the moment any single tiers.env binding is malformed. The guard is
unchanged in strength; only its subject narrowed to the id that reaches
agent().

OWED MEASUREMENT, recorded in the code and in #158: what this repo has
measured is the converse — opts.model OVERRIDES the agent file's frontmatter
(2026-07-29, cited in review.js). That an OMITTED opts.model leaves the
frontmatter in effect is the expected complement, not something measured
here. The returned modelSource field is what a live run reads it off; a
dispatch that comes back "seat-default" and ran on something else refutes
rung 3, which then becomes an explicit refusal.

Three mutations, each red in tests/test_workflows.mjs before the fix:
- restoring `model || TIERS.JUDGMENT` -> 2 red (the absent-key assertion and
  the seat-default return), with a CONTROL proving the absence check can fail;
- tolerating an unknown args.tier -> 1 red (it throws, but with a misleading
  "is not a model id string", so the message is pinned too);
- re-adding eager validation -> 1 red (the F07 unused-key property).

Prose that documented the old fallback is corrected in the same commit:
commands/tiers.md's dispatch paragraph and the README workflow table.

Gates: validator all contracts hold · workflows 395@395 (floor 384 -> 395,
same commit) · compress 161 · python 397 · shell 1085 + 16 skipped.
shellcheck is not installed in this container; no shell file changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
`grep -rn IMPLEMENT workflows/` returned nothing across all six. IMPLEMENT is
one of the four canonical tiers (ops-tiers.sh TIER_NAMES) and the tier
ops-render.sh binds the implementer to (seat_add mechanic IMPLEMENT default),
and no workflow dispatched it — so every stage of the cycle that only READS
ran as a workflow with a tier map while the one stage that WRITES CODE was a
plain Agent call against a hardcoded frontmatter alias.

workflows/implement.js, per docs/CYCLE.md §5.3:

- FIRST workflow to dispatch IMPLEMENT. mechanic defaults to IMPLEMENT and
  author to JUDGMENT — what ops-render.sh's seat_add lines say — and args.tier
  overrides, so this is a default, not a second declaration of the seat->tier
  map. Same ladder as dispatch.js: args.model, args.tier, then no model key.
- TAKES THE DISPATCH PACKET as structured args and refuses an incomplete one
  with ZERO agents dispatched, naming EVERY defect at once rather than one per
  round (#152's point one level in; #84 measured what the other order costs —
  7 agents, 123,935 tokens, every seat answering it could not proceed).
- SERIAL. The charter's one-implementer-at-a-time rule [D:CHART-r6] stops
  being prose the operator must obey and becomes a property of the script.
- Hands the seat the four-status protocol as a SCHEMA ENUM and `changed` as an
  ARRAY — the operator routes on status, and a prose CHANGED line is what a
  diff cannot be checked against.
- STOPS on a dead seat rather than stepping over it: the next task may depend
  on this one's output. Completed work is kept and `dispatched/requested` says
  a short run was short.
- Returns the unioned CHANGED and stops there. The sandbox has no filesystem,
  so ops-claims.sh, the sentinel and the verdict row stay with the operator —
  and the return says out loud that CHANGED is the seats' CLAIM.

check_implement_packet is the new pin. PACKET_FIELDS is the FOURTH hand-copy
of the charter's dispatch packet and the first in code, so it is pinned rather
than trusted (F30): every field present, no field the charter lacks, the four
statuses complete, the charter still teaching both — and the APPLICATION,
because a field required by the refusal and then dropped before the prompt is
worse than one never required (the refusal implies it was used). An absent
implement.js is a FINDING, not a skip (#114).

Ten python cases, each its own mutation, all red in check_implement_packet:
dropped field, extra field, validated-then-unsent, the application moved into
a comment (F48/F57's shape), a dropped status, the charter losing a field,
the charter losing a status, a missing file, a renamed locator — plus the
independent-copy equality that fails loudly when the constant changes.
Serialization is pinned by a source scan (no `parallel(` in the file) WITH a
brainstorm.js CONTROL: the suite's stub runs parallel() thunks sequentially,
so a concurrency counter would pass for a parallel implementation — vacuous.

CLAUDE.md sits at 37985/38000 chars after this row. The narrative went to
docs/LANDMINES.md per the cap's own instruction; the next coupling will not
fit without another extraction.

Gates: validator all contracts hold · workflows 430@430 (floor 395 -> 430) ·
python 407@407 (floor 397 -> 407) · compress 161@161 · shell 1101@1101.
shellcheck is not installed in this container; no shell file changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
@betmoar betmoar changed the title docs: specify the two stages missing from the engagement cycle (#75) The implement stage runs as a workflow, and the cycle's two missing stages are specified (#158, #75) Sep 20, 2026
Six workflows shipped with no command surface: the operator hand-built
`Workflow({name, args})` calls and hand-pasted a model id resolved by a
separate script. That hand-paste IS #55 arriving at the call site — skip it
and every seat runs on a harness alias while tiers.env says otherwise, with
nothing warning. commands/tiers.md documents the two-step under its own
warning; this removes the step rather than documenting it again.

commands/{brainstorm,plan,implement,review,crawl,debate}.md. Each resolves
the bindings with `ops-tiers.sh --json` — the machine-readable form whose own
comment says it exists for Workflow({args:{tiers:...}}) — and passes the one
line through verbatim. dispatch.js deliberately gets no command: it is the
low-level escape hatch /cc-operator:tiers documents and takes a resolved id
by construction.

The bodies carry what the workflow cannot enforce from inside:

- brainstorm: pass the topic IN FULL (#92's 123,935-token fan-out against a
  placeholder is reproduced honestly by summarizing the brief), then
  interview one question at a time in the order the bundle already sorted.
- plan: the north star must be the SAME sentence as the BAR block's, because
  nothing keeps the two in step and a plan vetted against a goal the ledger
  never agreed to fails silently (docs/CYCLE.md §2).
- implement: every packet field, and then close the loop yourself —
  ops-claims.sh and ops-verdict.sh, because the workflow has no filesystem.
- review: doneMeans is not optional in practice; the spec and testability
  lenses ask about the task and without it review nothing.
- crawl: a dead crawler is lost coverage, not an empty shard.
- debate: use it only when the question cannot be settled by running
  something, and pick ids that DIFFER.

Pinned in the bash suite per command, not trusted to the prose: the --json
resolution, the grant covering the prescription (#104's rule — both the Bash
form and the Workflow tool in allowed-tools), and that each dispatches ITS
OWN workflow (check_commands reads frontmatter and paths and knows nothing
about which workflow a body invokes). Coverage, not equality: every workflow
except dispatch must have a command, so deleting one fires, while adding
commands/dispatch.md later is legal and automatically checked. The loop's own
vacuity is checked (>=5 commanded workflows) and start.md is the CONTROL that
the greps can come back negative.

Three mutations, each red in this case:
- review.md resolving --show instead of --json -> 1 red;
- debate.md losing the Workflow grant -> 1 red;
- crawl.md deleted -> 1 red (the coverage half).

No CLAUDE.md coupling row: the file sits 15 chars under its cap and the
extraction that would make room is #159.

Gates: validator all contracts hold · shell 1133@1133 (floor 1101 -> 1133,
same commit) · workflows 430 · python 407 · compress 161.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
Seven stages, and until #75 exactly one command. Every transition between
them was the operator remembering to make it, and after a compaction the
RECOVERY PROTOCOL's seven steps are the only path back — prose the operator
must CHOOSE to follow. This puts the answer in the one channel a session
reads before doing anything: the SessionStart banner.

scripts/lib/stage.sh is a PURE function. It opens no file: stage_derive takes
the counts scan_pending already computed plus scan_deviations' verdict, so it
adds no reader, no byte cap, no NUL probe and no second copy of the partition
rule. That is what lets the Stop hook (which runs every scan) and the
SessionStart hook (which runs only the pending one) share one derivation
without disagreeing — they differ in what they KNOW, and the unknown input
has its own answer rather than a guessed one: SessionStart passes "-" for the
deviation gate and the stage then never claims HANDOFF.

Rungs, in precedence order, each one making the next unreachable: BLOCKED (a
sentinel whose name no CLI can close) > IMPLEMENT (a task of mine open) >
HANDOFF (unpresented decisions) > CLEAR. A foreign task is REPORTED and is
never a stage — another session's open work changes nothing about what this
session should do next.

DERIVED, NEVER STORED, per docs/UNKNOWNS.md's rule about status in two
places: there is no engagement.json and there must not be one. REPORT-ONLY,
like the cap detector: nothing here exits, writes, or changes a gate's
verdict, and a case refuses an `exit` in any stage_derive branch.

The DIVERGE/SPEC/PLAN rows of docs/CYCLE.md §6 wait on #155 — with no spec
artifact there is nothing on disk to derive them from, and inventing a proxy
would be the guess this design refuses. §6 now says so.

Wiring is FAIL-SILENT and that polarity is load-bearing: the id injection is
the root of the whole ownership mechanism, so a missing lib costs the stage
line and nothing else. Its guard is `-f`, not `-r` — the validator refuses a
permission test here because uid 0 bypasses mode bits, so `-r` would read as
a guard while guarding nothing (caught by check_permission_guards on the
first run of this change, which is the gate working).

Three mutations, each red in the bash suite:
- disabling the HANDOFF rung -> 1 red;
- inverting IMPLEMENT/HANDOFF precedence -> 1 red;
- removing the banner's stage line -> 3 red (the end-to-end half, which is
  what catches a `&& \` continuation followed by a comment — a shape bash -n
  accepts and nobody can read; it was in the first cut of this wiring).
Plus a CONTROL that the banner reports CLEAR when nothing is open, and one
that a scanned-clean gate carries no "not scanned" caveat.

Gates: validator all contracts hold · shell 1151@1151 (floor 1133 -> 1151,
same commit) · workflows 430 · compress 161 · python 407 unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
dispatch.js and implement.js omit the `model` key entirely when the caller
names neither a model nor a tier, so the seat runs on its own configured
default. That rested on a complement this repo had never measured: what was
recorded is the CONVERSE — opts.model OVERRIDES the agent file's frontmatter
(2026-07-29, cited in review.js) — and it was carried in the code as an
admitted assumption.

MEASURED 2026-09-21. Two seats, same agentType (a project-layer agent pinning
`model: opus`), one dispatch with no `model` key and one with
`model: "haiku"`:

  seat "omitted"   per-agent metadata: no `model` key   served claude-opus-5
  seat "explicit"  per-agent metadata: "model":"haiku"  served claude-haiku-4-5-20251001

Omitting the key hands the decision to the agent definition, which is what
the rung claims. Not covered, and said so where it is written down: a
cc-proxy id rather than a harness alias, and a plugin-root agent rather than
a project-layer one — neither is the mechanism under test.

The note moves from "owed" to the measurement in all three places that
carried it: the comment at the call site in dispatch.js, docs/CYCLE.md §5.4,
and docs/LANDMINES.md. Writing it down as an assumption is what made it cheap
to close; phrasing it as a fact is the class LANDMINES exists for.

Gates: validator all contracts hold · workflows 430 · no behavior changed
(comments and prose only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
)

validate went red on f6d9b6f and 8631e8d:

  In scripts/lib/stage.sh line 64:
    STAGE="CLEAR"
    ^---^ SC2034 (warning): STAGE appears unused.

Correct, and the same shape partition.sh, caps.sh and autobar.sh each carry a
file-scoped directive for: a lib's globals are consumed by the SOURCING
script, which shellcheck cannot see. stage.sh shipped without the line the
three siblings have had since they were written.

This is the cost of "shellcheck is not installed in this container", repeated
across three commits. It is installed now (pip install shellcheck-py, 0.11.0)
and the failure was REPRODUCED before the fix and the same command shown
clean after:

  before: scripts/lib/stage.sh line 64 SC2034, rc=1
  after:  rc=0

One version caveat, stated rather than papered over: CI pins the
koalaman/shellcheck-alpine:v0.10.0 image and this container has no docker
daemon, so the local binary is 0.11.0. The difference that matters is SC2329
(added in 0.11), which fires on ops-verdict.sh's fallback_release — a
PRE-EXISTING info that 0.10 does not emit and CI therefore never saw. It is
not this PR's and is not touched here; `--exclude=SC2329` reproduces 0.10's
finding set exactly, and that run is clean.

Gates: validator all contracts hold · shell 1135 + 16 skipped · workflows 430
· shellcheck clean at 0.10's finding set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
THE CHARTER (#159 part 1). ORCHESTRATED MODE said "The review, brainstorm,
plan and debate workflows are the primitives" while workflows/implement.js
shipped in #158 — so the operator running under the charter was not told the
implement stage has a workflow, and the charter's own one-implementer-at-a-
time rule [D:CHART-r6] was enforced by a mechanism the charter did not name.
One word, +11 bytes: 143/150 lines, 8884/9000 bytes, caps re-run and green.

SC2329 (#160). shellcheck 0.11 reports "this function is never invoked" on
ops-verdict.sh's fallback_release. It is trap-reachable from NINE sites and
the file already carried `# shellcheck disable=SC2317` for the same fact —
0.10's code for it. Two codes, one fact; without the second, bumping the CI
pin goes red with no code change. check_lock_parity caught that the comment
lives inside the LOCK BLOCK and refused the edit until ops-adopt.sh's copy
matched byte-for-byte — the gate working. The tree is now clean under 0.11
with NO exclusions, so the pin can be bumped deliberately whenever the
maintainer wants; this commit does not bump it.

CLAUDE.md (#159 part 2, PARTIAL and said so). Four rows trimmed of restated
why, every rule token and every _"…"_ citation asserted to survive the edit:

  base-gate      1905 -> 1595   (why is LANDMINES 0.11.13/0.11.14)
  handoff         897 ->  716   (evidence is in the suite's own comments)
  CR-strip        863 ->  730   (why is LANDMINES 0.11.15)
  check_bare_name 755 ->  733

That freed ~825 chars and two new coupling rows spent ~810: lib/stage.sh
(#157) and a commands/<workflow>.md (#75), both previously unrecorded. Net
headroom 14 of 38000. The structural problem #159 names is therefore NOT
solved — it is demonstrated: this file cannot take a new coupling without an
extraction that costs as much as the coupling. docs/LANDMINES.md gains the
#157 narrative, which is where the stage row now points.

Gates: validator all contracts hold · shell 1135 + 16 skipped · python 407 ·
workflows 430 · shellcheck clean at 0.11 with no exclusions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
#156)

.operator/specs/ needs two allow lines, and the allowlist could not take them:
the migration fires only when the version marker is ABSENT, so adding a line
without bumping the marker leaves every already-initialized project silently
ignoring its own specs — the stale installed-copy class through a different
door.

MY OWN ISSUE PROPOSED THE WRONG FIX. It reasoned the blocker was the hardcoded
backup path (`.gitignore.v1.bak` would lie about holding v2 content) and asked
for a generalized backup name. That accepts the premise that a version bump
REPLACES. It does not have to: v1 -> v2 replaced because a blocklist and an
allowlist contradict, and v3 is v2 PLUS two lines. Replacing a v2 file would
silently delete every allow line the user added by hand — a destructive answer
to a non-destructive change. So the arm APPENDS, no backup is involved, and
the naming problem the issue was about does not arise.

Both writers, each with its own arm:

  v1 (no marker at all) -> REPLACE behind the verified backup. Unchanged.
  v2 (has the v2 marker) -> APPEND `!specs/`, `!specs/*.md`, then the v3
  marker LAST, so a die mid-append leaves the file unmarked and the next run
  retries rather than reading a half-upgraded file as done.

Measured on real projects rather than asserted, all four paths: a fresh init
emits v3; a v2 file carrying `!my-hand-added.md` keeps it in the LIVE file
with no .bak written; a re-run appends nothing; a v1 blocklist still replaces
and its content is recoverable at .v1.bak. Same four through the SessionStart
hook, whose notice says the upgrade was additive — the neighbouring v1 notice
tells the user their hand-written rules are GONE, which would be false here.

A VACUITY I INTRODUCED AND THEN CLOSED. The additive arm greps the current
marker against the live file, which satisfied #102's detection pin on its own
— so deleting the DESTRUCTIVE arm's detection grep, after which a v1 blocklist
is never replaced at all, shipped green the moment the arm landed (measured:
test_removing_only_the_DETECTION_grep_fires went red -> green with no change
to the pin). The pin now COUNTS live-target reads and requires two, because
both arms genuinely need their own. A pin whose subject gains a second
satisfier is vacuous for the first one.

Also derived from the marker rather than hardcoded: the temp suffix, which
appeared as a literal `.v2.tmp` in three regexes, so the bump unpinned the
atomic write in all three at once.

Mutations, each red: append -> rewrite (red in check_gitignore_parity AND in
the live-file case), the hook losing v2 recognition (3 bash cases), losing the
current marker, losing the previous marker.

Gates: validator all contracts hold · shell 1162@1162 (1151 -> 1162) · python
408@408 (407 -> 408) · workflows 430 · compress 161 · shellcheck clean at 0.11
with no exclusions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
The cycle went brainstorm -> ??? -> plan. The only thing carrying a design
across that gap was prose the operator retyped: no file, no provenance, no
source stamp, no ledger row — so a compaction lost it and nothing noticed.
And there were TWO north stars, the BAR block's and plan.js's, with nothing
keeping them one sentence.

scripts/ops-spec.sh, installed into .operator/bin/:

  --new <slug>     scaffold .operator/specs/<slug>.md, O_EXCL so a second
                   --new refuses rather than clobbering work in progress
  --check <slug>   writes nothing; refuses a missing section, a north star
                   with no `Missed if:` clause (plan.js reads it without a
                   fallback), a done-criteria table still holding the
                   placeholder, and any open question with an empty
                   Resolution — an unanswered one is the interview skipped
  --approve        stamps Status: APPROVED @<source-state> using
                   ops-verdict.sh's own ladder, logs SPEC-APPROVED, and
                   EMITS THE BAR BLOCK from the spec

The BAR block is the point. The charter requires it by hand before the first
implementation action, and its north star and plan.js's args.northStar were
two hand-written sentences that could drift silently. Emitting the block FROM
the spec makes them one sentence by construction — pinned by a case asserting
the block quotes the spec's north star and its Missed if: clause verbatim.

Write ORDER is ops-verdict.sh's (#14): DECISIONS line, then the BAR block,
then the spec's own Status LAST. A crash between them leaves a record that
approval was attempted with the spec still DRAFT, which re-runs cleanly; the
reverse leaves an APPROVED spec no ledger knows about and nothing retries.

SPEC-APPROVED is a RECORD kind, never gated (#9): a gated kind blocks Stop
until the handoff presents it, and an approved spec is not a deviation to
answer for.

THE NEW CASE FOUND A REAL GAP IN check_decisions_schema. It verified each
kind EXISTS in the header and that a gated/record split is visible — never
which SIDE a kind is on. SPEC-APPROVED written onto the `gated` line passed
every check while wedging every session that approved a spec. That is #9's
defect exactly, and the pin could not see it. The check is now side-aware,
keyed on each line's LEADING LABEL: the shipped marker line reads "marker
(clears the gated set)", so a substring scan files it under both and reports
HANDOFF-MARK as mis-sided.

FOURTH PROJECT ROOT BLOCK, byte-identical and added to check_root_parity's
list. The python RootParityTest refused the fixture the moment the list grew,
which is the check working: three uniform-mutation cases were mutating three
of four writers, so they measured drift rather than the uniform loss they are
named for.

Mutations, each red: the BAR block no longer quoting the spec's north star,
the Missed-if requirement dropped, the walk-up removed (red in BOTH
check_root_parity and the subdirectory case — the pairing that fourth copy
needed), SPEC-APPROVED on the gated line.

Gates: validator all contracts hold · shell 1187@1187 (1162 -> 1187) · python
409@409 (408 -> 409) · workflows 430 · compress 161 · shellcheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
#157)

docs/CYCLE.md §6 listed DIVERGE/SPEC/PLAN as waiting on #155: with no spec
artifact there was nothing on disk to derive them from, and inventing a proxy
would have been the guess the design refuses. #155 put the artifact on disk,
so two of those rows are now real.

stage_derive takes a sixth argument — the SPEC summary (none | draft |
approved | "-" for not-looked) — and the CALLER computes it, because the lib
still opens no file. That is the same division that lets the Stop hook and
SessionStart share one derivation while knowing different things.

THE OPT-OUT PROPERTY is the one that keeps this honest: an ABSENT
.operator/specs/ reports CLEAR, never a spec stage. A project that never used
the spec stage must not be told forever that it is in DIVERGE — the
derivation reports where the engagement IS, never where a ceremony says it
should be. Pinned, and red when `none` is folded into the draft arm.

The gate's own rungs still outrank the spec ones: an open task of mine, or an
unpresented decision, is what this session must deal with whatever the specs
say. Both pinned.

The hook's glob is bounded at 50 specs — this runs on every session start and
a directory someone filled is not a reason to stall one. Past the bound the
answer stays "draft", the conservative side: it prescribes finishing a spec
rather than planning from one that may not be approved.

Three mutations, each red: the spec rungs removed (4), the hook no longer
distinguishing APPROVED (1), an absent specs dir folded into the draft arm
(2, including the CLEAR control).

Gates: validator all contracts hold · shell 1194@1194 (1187 -> 1194) ·
python 409 · workflows 430 · compress 161 · shellcheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
…view)

The review panel's P0, reproduced here before the fix: `>>` appends at the
byte offset the file ends at, so a .gitignore whose last line carries no
terminating newline FUSED with the first appended rule —

  !my-hand-added.md!specs/

which destroys the user's rule and leaves `!specs/` inert, while the v3
marker still lands so the migration never retries. Both writers now
terminate the file first; five cases, and the mutation removing the guard
from BOTH writers turns all five red.

Also from the same review:
- ops-spec.sh takes the ledger lock it needs (LOCKDIR defined before the
  copied LOCK BLOCK — the first live --approve died on an unbound variable)
  and check_lock_parity holds THREE writers, compared against one reference
  copy: it compared ops-verdict vs ops-adopt only, so ops-spec.sh's copy was
  held by the content pin alone. Measured: LOCK_SPINS=100 in ops-spec.sh
  alone reported nothing. Red in test_third_writer_drift_fires.
- the good-tree fixture gains ops-spec.sh's lock block; without it
  check_lock_parity returned early on the missing file and the four
  LockParityTest mutation cases went green against a check that never ran.
- the commands case asserts tier-binding COVERAGE, not the literal
  ops-tiers.sh: it failed when implement.md's grant got WIDER (Bash(bash:*)
  per #104). Re-mutated — a command with no bash grant still fires.
- ops-init.sh's refusal path, stage.sh's and spec.md's claims, and the
  REPLAY-CHARTER/CLAUDE.md counts corrected.

Floors raised in the same commit, measured in a rootful container:
  bash    tests/test-scripts.sh          -> 1187 passed, 0 failed, 16 skipped
  python3 -m unittest discover -s tests  -> Ran 410 tests / OK
  node    tests/test_workflows.mjs       -> 430 passed, 0 failed
  node    tests/test_compress.mjs        -> 161 passed, 0 failed
  shellcheck 0.11.0 scripts/ lib/ tests/ -> rc=0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
…154 review)

THE VACUITY, MEASURED. The check's `if "_cr" not in code: continue` guard
skipped exactly what the realistic simplification produces. On the real
tree: ops-reverify.sh's loop replaced by the #139 issue's own rejected
`${row%%$'\r'*}` — which truncates the row at a mid-cell CR — with `_cr`
dropped from its `local` line left `validate_plugin: all contracts hold`.
The loop is now required unconditionally wherever lib/caps.sh declares
CAPS_MAX_CR, and the good-tree stub carries it (a stub that omits it IS the
deletion). Red in CrStripParityTest.

The review reported this as "deleting the whole strip from ops-reverify.sh
leaves all contracts hold". That narrower mutation — the loop gone, the
`local … _cr=0` line kept — FIRES even before this fix, twice. The case is
written against the mutation that actually escapes, and says so.

CHANGELOG + 0.12.0. The release gate refuses a non-empty [Unreleased] at the
tagged version, so this branch's work gets its own section: the spec stage
(#155), the implement workflow (#158), the command surface (#75), the stage
derivation (#157), the additive v3 gitignore (#156), the charter wording
(#159) and SC2329 (#160), plus this review round's four fixes.

Floors: FLOOR_python 410 -> 411 (the new case). CLAUDE.md back under its
38000 cap by trimming three rows of restated why; two rows corrected while
there — stage.sh's ladder is BLOCKED>IMPLEMENT>HANDOFF>SPEC>PLAN>CLEAR (the
SPEC and PLAN rungs #155 added were never recorded), and the CYCLE.md
provenance bullet called the spec stage unbuilt after #155 built it.

Gates, this tree:
  validator -> all contracts hold
  python    -> 411 cases (floor 411)
  shell     -> 1203 cases (floor 1203)
  workflows -> 430 cases (floor 430)
  compress  -> 161 cases (floor 161)
  shellcheck 0.11.0, no exclusions -> rc=0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
@betmoar betmoar changed the title The implement stage runs as a workflow, and the cycle's two missing stages are specified (#158, #75) The cycle's missing stages are built: a spec artifact, an implement workflow, a command per workflow (0.12.0) Sep 21, 2026
@betmoar
betmoar marked this pull request as ready for review September 21, 2026 08:33
@betmoar
betmoar requested a lite review from Copilot September 21, 2026 08:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

There are a couple of correctness issues in new/changed checks/tests (notably the open-questions validation in ops-spec.sh and a dispatch “no model key” assertion in tests/test_workflows.mjs) that should be fixed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
What changed in this PR

This PR ships v0.12.0 by completing the engagement cycle’s previously missing “spec” and “implement” stages, adding the operator-facing command surface to reach workflows, and strengthening validation/parity checks so the new artifacts and writers stay consistent with the existing evidence gate.

Changes:

  • Add a tracked spec artifact (ops-spec.sh) with approval stamping into both ledgers under the shared lock, plus .operator/.gitignore v3 additive migration to track .operator/specs/.
  • Add workflows/implement.js (strictly serial implement stage) and extend dispatch.js to resolve by args.modelargs.tier → “no model override”.
  • Add a command per workflow, stage derivation in SessionStart via scripts/lib/stage.sh, and the associated validator/tests/docs/version updates.
File Description
workflows/​implement.js New serial implement workflow with packet validation and schema-guided reporting.
workflows/​dispatch.js Tier-based model resolution and “no override” fallback (omit model key).
tests/​test_workflows.mjs New runtime capture fields + expanded tests for dispatch ladder and implement workflow.
tests/​floors.env Raised suite floors to match added/expanded test coverage.
templates/​OPERATOR.md Charter updated to include implement among orchestrated primitives.
templates/​DECISIONS-header.md Adds SPEC-APPROVED as a record kind in the DECISIONS schema.
scripts/​validate_plugin.py New/updated validator checks for implement packet, decisions kind “side”, gitignore v3 parity, lock/root parity extensions.
scripts/​ops-verdict.sh Adds ShellCheck SC2329 suppression for trap-reached fallback_release.
scripts/​ops-spec.sh New spec artifact CLI: scaffold/check/approve + lock-disciplined ledger writes.
scripts/​ops-sessionstart-hook.sh Adds v2→v3 gitignore append upgrade + derived stage line in the SessionStart banner.
scripts/​ops-install-set.sh Installs ops-spec.sh into .operator/bin/ set.
scripts/​ops-init.sh .operator/.gitignore v3 emission + v2→v3 additive append upgrade path.
scripts/​ops-adopt.sh Adds SC2329 suppression alongside existing unreachable-function suppression rationale.
scripts/​lib/​stage.sh New pure/report-only stage derivation helper for banner guidance.
README.md Updates docs to reflect spec artifacts, implement workflow, expanded commands, and gitignore semantics.
docs/​REPLAY-CHARTER.md Updates CLI list and replay guidance for the new installed CLI.
docs/​LANDMINES.md Adds rationale entries for implement-tier reachability, serialization pinning, and stage derivation.
docs/​HANDOUT.md Repairs outdated references from docs/spec/ to docs/TAGS.md.
docs/​CYCLE.md New design rationale/spec for cycle, now reflecting built stages.
CONTRIBUTING.md Updates rationale location references (docs/ vs removed docs/spec/).
commands/​tiers.md Documents dispatch tier ladder and the “no model override” behavior.
commands/​spec.md New /cc-operator:spec command guidance for writing/checking/approving specs.
commands/​review.md New per-workflow command wrapper resolving tiers automatically for review.
commands/​plan.md New per-workflow command wrapper with approved-spec gating guidance.
commands/​implement.md New per-workflow command wrapper for serial implement dispatch.
commands/​debate.md New per-workflow command wrapper resolving tiers automatically for debate.
commands/​crawl.md New per-workflow command wrapper resolving tiers automatically for crawl.
commands/​brainstorm.md New per-workflow command wrapper resolving tiers automatically for brainstorm.
CLAUDE.md Updates coupling table and invariants to include new stage/packet/stage lib rules.
CHANGELOG.md Adds 0.12.0 release notes summarizing cycle completion and related fixes.
.claude-plugin/​plugin.json Bumps plugin version to 0.12.0.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/ops-spec.sh Outdated
Comment thread tests/test_workflows.mjs Outdated
)

FINDING 2 — REAL, and it made the pin vacuous. The dispatch no-override
assertion read `"model" in dFallCall`, which asks the test stub's RECORD.
The stub writes `model: opts.model` unconditionally, so that property
ALWAYS exists and the check collapsed to `model === undefined` — while
`hasModelKey` (`"model" in opts`, taken at the call, added for exactly this
distinction) went unused. Its own comment claimed it asked the options
object. Measured: dispatch.js sending `{ model: undefined }` on that rung
kept the node suite at 430 passed, 0 failed. Now reads `hasModelKey ===
false`, the control asserts `=== true`, and the same mutation is 1 red.

FINDING 1 — HALF REAL. The premise is wrong: the skeleton's headers are
`| Question | Resolution | Decided by |` and `|---|---|---|`, both matched
by the existing filters, and a fresh skeleton reports no open-questions
problem (measured). The other half holds: `grep -c '| *|'` matches an empty
cell ANYWHERE in the row, so `| Can we X? | Yes |  |` — answered, nobody
named — was refused as an "empty Resolution cell". A true refusal under a
false name is worse than no message: it sends the operator to fix a cell
that is already correct.

Now cell-addressed. Resolution and 'Decided by' are separate conditions
with separate messages; a row with fewer than three cells is malformed and
counts as unanswered, failing CLOSED like every other refusal here. Six
cases (all five shapes plus a header/separator control); the mutation
restoring the substring scan is 3 red — the 'Decided by' message case and
both malformed-row cases. The rc-only unattributed case stays green under
that mutation ON PURPOSE: the old scan refused that row too, just under the
wrong name, so only a message assertion can see the defect.

Floors: FLOOR_shell 1203 -> 1209. Finding 2 needed none (an assertion
changed, no case added).

Gates, this tree:
  validator -> all contracts hold
  python    -> 411 cases (floor 411)
  shell     -> 1209 cases (floor 1209)
  workflows -> 430 cases (floor 430)
  compress  -> 161 cases (floor 161)
  shellcheck 0.11.0, no exclusions -> rc=0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPU5aDfukCkFX5rbLdJgun
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants