Skip to content

feat: ceremony proportional to kind and size, and add check for the tally the gate reads - #206

Open
TinDang97 wants to merge 8 commits into
mainfrom
feat/add-ceremony-ladder-and-check-verb
Open

feat: ceremony proportional to kind and size, and add check for the tally the gate reads#206
TinDang97 wants to merge 8 commits into
mainfrom
feat/add-ceremony-ladder-and-check-verb

Conversation

@TinDang97

@TinDang97 TinDang97 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Two milestones, driven through ADD itself: right-sized-lane (3/3) and checkbox-verb (4/4).

1 — Ceremony proportional to kind and size

ADD funnelled every request toward the full specification bundle: a typo fix and a new subsystem read the same instructions. The one escape hatch, the Quick lane, admitted only mechanical edits and carried no discipline beyond "make the edit".

A size threshold alone tells an agent when to skip the node and nothing about what it still owes, so this ships a ladder keyed on kind × size. Each rung states three things in one row — the route, the effort and review it owes, and what persists:

the change (kind · size) route effort · review what persists
mechanical, or small behavior — ≤3 adjacent files, one-sitting diff, zero unknowns direct — no node inline card before the edit · red→green · invariants: hold · self-review the commit + one add learn line
one behavior worth a frozen contract Task, --depth quick or standard advisor pressure-test at direction · human freeze · receipt-backed verify the node · its frozen contract · a run receipt
an unanswered question — investigate · evaluate · research Task, --kind explore a hard budget · cited findings · sufficiency gate the node + its cited ## FINDINGS
a theme, or a slice spanning tasks Milestone persona-led plan · breadth-first task list · goal-gate at close the milestone + its task nodes

The floor is read first and outranks size: security · data · architecture, a consumed gives: surface, or frozen scope takes a node however small; security stays a HARD-STOP. The direct rung now admits small new behavior (previously mechanical-only) and in exchange owes a five-step checklist ending in a commit plus exactly one add learn line — its only bundle write.

Ships to every reader, not just Claude: intake.md + the SKILL.md bullet in all three skill trees, this repo's CLAUDE.md block, and one byte-identical sizing sentence in both installer pointer twins, so a Cursor/Codex user who never loads the skill still gets it in their own CLAUDE.md. Also repairs that block's retired verbs — add.py is the library and prints nothing (cli.py is the entrypoint), and guide no longer exists.

2 — add check: the engine writes the checklist it reads

milestone_done gates a milestone closed on a - [x]/- [ ] tally it parses out of ## EXIT, but no verb could write one. Every tick was a hand edit to markdown the engine reads — the first milestone in this PR was closed with a throwaway Python script, which is the same defect class as editing a frozen contract by hand: the state the gate turns on was reachable only from outside the engine.

add check REF N... [--all] [--off] [--section S] [--by WHO]. 1-based in document order; --section narrows and re-indexes; boxes inside fenced blocks are skipped so the number you count off the file is the number it writes to; all indices validated before any line is rewritten, one atomic replace, and an already-marked box reports unchanged and writes nothing.

On the goal-gate. Offered the narrower notary design — tick only boxes whose (← task) referent is done and gated PASS — the maintainer chose reach: any box, any node, any caller. So the gate is self-serve by choice, and attribution replaces the defence. Each invocation appends one verified: stamp naming who moved which boxes in which section; an unattributed call stamps the literal process:check; and the close line now reads:

(3/3 exit criteria met, checked by Ada Lovelace, process:check)

An agent's self-affirmation is visible in the sentence that closes the milestone. A milestone ticked before this verb existed closes checked by hand, inventing no name. check and milestone_done share one compiled BOX pattern, so the verb can never tick a box the gate cannot count.

The verb count moves 22 → 23, which five registries enumerate — the CLI's WIRED set, both README kernel counts, the book command reference, and the phantom fixture that used add check precisely because no such verb existed. The full suite found them; a grep would not have.

Verification

  • Red first: 15 of 16 guards failed against an engine with no check at all.
  • Mutation-tested: breaking index validation, the stamp, and bare-ref handling each tripped the specific guard meant to catch it.
  • Dogfooded: checkbox-verb was closed with the verb it ships.
  • Hand-driven on a real bundle, which caught two defects the 19 guards were green through — 2 box marked (no plural), and a listing clipped at each criterion's first physical line. Both fixed here on the direct rung of the ladder above, red-first, with their add learn lines. That listing is what an operator picks an index from, so a clipped row is a correctness problem for the choice.

19 engine guards. 747 passed / 7 skipped in add-method/, 8 passed in add-method/tooling/ (which owns the pins). Four engine twins synced; both ENGINE_MD5 and ENGINE_PKG_MD5 re-aimed.

Reviewer notes

  • The goal-gate is now self-serve. That was a deliberate call, and the stamp plus the close line are the only things standing where the refusal would have been — worth a hard look.
  • Atomicity is argued, not fault-injected: refusals are proven to write nothing, but no test kills the process mid-write. The guarantee rests on write()'s existing os.replace.
  • test_every_registry_learned_the_new_verb has never run red; the red evidence for that claim came from the five incumbent guards.

3 — The refusals the docs promised, made real

After the two milestones above, a four-lens adversarial review (method integrity · trust boundary · adoption cost · engine architecture) went at the whole method. It found ADD's mechanical enforcement sitting entirely inside freeze and gate — and both reachable around.

The unifying defect: every guard fired on the PRESENCE of a malformed thing and never on the ABSENCE of a required one. _section_of returns "" for a missing heading and every consumer reads empty as clean, so the way past each refusal was to delete rather than to forge.

The maintainer's call was to enforce more rather than soften the docs. Four refusals added, one coercion, one sanitiser — no verb removed, no message weakened:

what now refuses what it was
gate PASS on a node with no freeze/refreeze stamp → R:UNSEALED, all Task depths every post-freeze guard was if sealed: with no else, so skipping the ONE approval did not fail drift + R:UNBRIEFED — it switched them off, gating with less scrutiny than a node that went through the approval
run claims kind: test-ids only for a junit written during the run, and the receipt says when it downgrades to command-exit a hand-written XML beside /usr/bin/true, naming tests that do not exist, earned the strongest evidence rung
sensitive_paths: matches through a directory scope, both directions scope: src/ did not match src/auth/* — declaring a broader, honest scope lowered authority below one naming the file exactly
gate PASS when a changed file matching sensitive_paths: is covered by no scope entry → R:UNDECLARED_SENSITIVE nothing compared the declared scope to the real diff, so omitting the path from scope: defeated the one floor that does not rest on self-declared sensitivity
gate's --reason flattened, as replan already flattened its note an unbalanced { made the parser swallow the following stamp: two records written, one read back, from an append-only ledger whose ordering is the trust model
_scope_list coerces a string scope scope: src/ui.py parses as a STRING and every reader iterated it — one freshness entry per character, with / resolving to the filesystem root
check records via: tty | process beside the claimed name; milestone-done marks (unattended) --by is free text, so loop.md's "a box the AI ticked never reads as a human's" was false. The name stays a claim; the caller context is fact. Prose re-cut across all three skill trees

The scope-vs-diff refusal is deliberately narrow — an ordinary undeclared path stays freshness's business, because a scope diff that refuses everything is a scope diff everyone learns to widen past.

Fixture honesty, not test weakening. Eleven suites broke, and every one of them was cheating in a way the new rules named: hand-writing a junit beside the run, or gating a node that was never frozen. Each now drives the real path — freeze → brief → run (the command writes its own report) → gate. spike_cli's brief had to start recording the entry stamp; without it no drive can reach a PASS. Nothing was relaxed to make a test go green.

test_an_unsealed_node_is_exempt survives untouched: it strips the direction: digest but keeps the freeze stamp, which is exactly where the line falls — a missing digest is tolerated (pre-3.0 bundles, R:RETROBREAK), a missing stamp refuses.

Recorded as /tasks/sealed-gate-enforcement.md, gated PASS at authority plan — the engine computed that floor from sensitivity: architecture and ignored the --authority human claim on the command line, which is the behaviour under test. 15 checks bound, receipt runs/2.md. Two lessons filed to specs/method and specs/quality.

762 passed / 7 skipped across both roots. Both md5 pins re-aimed, four engine twins mirrored.

Reviewer notes on this section

  • The seal requirement is the one change that can strand an existing bundle. It reads the stamp, not the digest, so pre-3.0 freezes still gate — but a bundle whose nodes were never frozen at all will now be refused, by design.
  • _changed_paths returns [] wherever git cannot answer, so the new sensitive-path refusal can only ever add a refusal where git is present, never invent one where it is not (E2 pins this).
  • Three of the seven fixes are hardening with no adversary (_scope_list, the receipt numbering, the isinstance guard) — cheap, but they are not enforcement, and shouldn't be read as such.

…ver does

ADD funnelled every request toward the full specification bundle. A one-line
typo fix and a new subsystem read the same instructions, so the method paid
node-shaped ceremony for trivia — and the only escape hatch, the Quick lane,
admitted nothing but mechanical edits and carried no discipline at all beyond
"make the edit".

Replace the size threshold with a LADDER keyed on kind x size. Each rung states
three things in one row: the route, the effort and review it owes, and what
persists after the session. The closed floor is read FIRST and outranks size —
security, data, architecture, a consumed `gives:` surface or frozen scope takes
a node however small it is; security stays a HARD-STOP.

The direct rung now admits small NEW behavior, not just mechanical edits, and
in exchange carries a five-step checklist: route line, an inline card before
the first edit, red->green, `invariants:` under the bare runtime, then a receipt
of the commit plus exactly one `add learn` line. That learn line is its only
bundle write. Medium and large reuse today's vocabulary — `--depth quick`,
`standard|deep`, Milestone — no new lane, tier, verb or stamp.

The rule ships to every reader, not just to Claude: `intake.md` and the SKILL.md
bullet in all three live skill trees, this repo's CLAUDE.md block, and one
byte-identical sizing sentence in both installer pointer twins, so a Cursor or
Codex agent that never loads the skill still gets it in its own CLAUDE.md.

Also repairs the CLAUDE.md block's retired verbs: `add.py` is the library and
prints nothing — `cli.py` is the entrypoint — and `guide` no longer exists.

13 guards across two new files pin the text rather than the intent: the floor
must precede the size limits in reading order, no rung may carry an empty
review or persistence cell, and the SKILL.md line pin may not be raised to fund
the new bullet.

task: direct-lane-size-gate (gate PASS, authority plan, freshness fresh)
milestone: right-sized-lane closed 3/3
tests: add-method/ 726 passed 7 skipped; add-method/tooling/ 8 passed

author: Tin Dang
`milestone_done` gates a milestone closed on a `- [x]`/`- [ ]` tally it parses
out of `## EXIT`, but the engine shipped no verb that could write one. Every
tick was a hand edit to markdown the engine reads — the milestone closed
earlier in this session was ticked by a throwaway Python script, which is the
same defect class as editing a frozen contract by hand: the state the gate
turns on was reachable only from outside the engine.

Add `add check REF N... [--all] [--off] [--section S] [--by WHO]`. Indices are
1-based in document order; `--section` narrows to one heading and re-indexes
within it; boxes inside fenced code blocks are skipped, so the number a human
counts off the rendered file is the number the verb writes to. Every index is
validated before any line is rewritten and the write is one atomic replace, so
a refusal or a crash leaves the node fully old or fully new. A box already in
the requested state reports `unchanged` and writes nothing, making re-runs safe.

The verb deliberately does NOT defend the goal-gate. Offered the narrower
notary design — tick only boxes whose `(<- task)` referent is done and gated
PASS — the human chose reach: any box, any node, any section, any caller. What
replaces the defence is attribution. Each invocation appends exactly one
`verified:` stamp naming who moved which boxes in which section, an
unattributed call stamps the literal `process:check` rather than a blank, and
`milestone_done`'s close line now reads `(4/4 exit criteria met, checked by
<who>)` — or `checked by hand` for a milestone ticked before this verb existed,
inventing no name. A self-served goal-gate is now visible at the moment it
closes instead of being indistinguishable from a human affirmation.

`check` and `milestone_done` share ONE compiled box pattern, so the verb can
never tick a box the goal-gate cannot count.

The verb count moves 22 -> 23, which five registries enumerate: the CLI's WIRED
set, both README kernel counts, the book command reference, and the phantom
fixture in test_shipped_docs.py that used `add check` as its example of a verb
the engine lacks (now `add sharpen`). loop.md's milestone-close step no longer
tells the reader to hand-edit the tally.

task: box-check-verb (gate PASS, authority plan, freshness fresh)
milestone: checkbox-verb closed 4/4 — ticked with the verb it ships
tests: 17 new engine guards; add-method/ 745 passed 7 skipped; tooling/ 8 passed

author: Tin Dang
Hand-driving a real bundle surfaced `2 box marked` and `unchanged — box 2, 3`.
The tests all passed: every assertion checked the count and the box text, none
read the sentence as an operator would. Manual use is what caught it.

Direct lane (mechanical, one string, zero unknowns): the assertion was extended
and run red first, all four engine twins carry it, ENGINE_MD5 re-aimed.

tests: 18 engine guards; add-method/ 746 passed 7 skipped; tooling/ 8 passed

author: Tin Dang
Real exit criteria wrap across source lines, and the listing showed only the
first one — every row on the live `affordance-truth` milestone trailed off
mid-sentence. That listing is what an operator picks an index from, so a clipped
preview is a correctness problem for the choice, not a cosmetic one.

Join indented continuation lines into the preview text. Display only: the node
keeps its own wrapping byte for byte, and writes still go by line index, so
`check` remains read-only until it marks something. A continuation is an
indented non-empty line that opens no new box, heading or fence.

Direct lane (mechanical, zero unknowns): assertion written and run red first,
all four engine twins carry it, ENGINE_MD5 re-aimed.

tests: 19 engine guards; add-method/ 747 passed 7 skipped; tooling/ 8 passed

author: Tin Dang
…heck`

Both defects the manual drive found — `2 box marked`, and a criterion listing
clipped at its first physical line — were green under 17 guards. The lesson in
both directions is the same: assert the rendered line, and reassemble what the
file wraps, because the preview is what the operator acts on.

author: Tin Dang
A four-lens adversarial review (method integrity · trust boundary · adoption
cost · engine architecture) found ADD's mechanical enforcement sitting entirely
inside `freeze` and `gate`, and both reachable around. The unifying defect: every
guard fired on the PRESENCE of a malformed thing and never on the ABSENCE of a
required one, so the way past each refusal was to DELETE rather than to forge.

Four refusals added, one coercion, one sanitiser. No verb removed, no message
weakened:

  * `gate` refuses a PASS on a node carrying no freeze/refreeze stamp, at every
    Task depth (R:UNSEALED). Every post-freeze guard — drift, brief entry,
    R:UNBRIEFED — was keyed off `if sealed:` with no else, so a node that skipped
    the ONE approval did not FAIL those checks, it switched them off. The
    pre-seal tolerance is unchanged: a missing DIGEST is still tolerated, only a
    missing STAMP refuses.
  * `run` claims `kind: test-ids` only for a junit report written during the run,
    and the receipt SAYS when it downgrades to `command-exit`. A hand-written XML
    beside `/usr/bin/true` earned the strongest evidence rung.
  * `authority_for` matches `sensitive_paths:` through a directory scope, both
    directions — `scope: src/` did not match `src/auth/*`, so declaring a
    BROADER, honest scope LOWERED authority below one naming the file exactly.
  * `gate` refuses a PASS when a changed file matching `sensitive_paths:` is
    covered by no declared scope entry (R:UNDECLARED_SENSITIVE). Deliberately
    narrow — an ordinary undeclared path stays freshness's business.
  * `gate`'s `--reason` is flattened the way `replan` already flattened its note:
    an unbalanced brace made the parser swallow the FOLLOWING stamp, so two
    records were written and one read back from an append-only ledger whose
    ordering IS the trust model.
  * `_scope_list` coerces a single-entry `scope: src/ui.py` — it parses as a
    STRING, and every reader iterated it per character, resolving `/` to the
    filesystem root.
  * `check` records the caller context (`via: tty | process`) beside the claimed
    `--by` name, and `milestone-done` marks unattended credit. `--by` is free
    text, so `loop.md`'s "a box the AI ticked never reads as a human's" was
    false; the name stays a claim, the context is fact. loop.md re-cut across
    all three skill trees.

Fixture honesty, not test weakening: eleven suites hand-wrote a junit beside the
run or gated a node that was never frozen. Each now drives the real path —
freeze → brief → run (the command writes its own report) → gate. `spike_cli`'s
`brief` records the entry stamp, without which no drive can reach a PASS.

Recorded as /tasks/sealed-gate-enforcement.md (gated PASS at authority `plan`,
receipt runs/2.md, 15 checks bound); two lessons filed to specs/method and
specs/quality. Both md5 pins re-aimed, four engine twins mirrored.

Suite: 762 passed, 7 skipped (both roots).

author: Tin Dang
…thon

`add check` wrote its file back with a PEP 701 f-string — `f"...{"".join(lines)}"`,
double quotes nested inside double quotes — which is a SyntaxError on every
Python before 3.12. `add.py` therefore failed to IMPORT on 3.10, so CI's py 3.10
job died at `cli.py init` before a single test ran, and the whole suite was
skipped rather than red. Local runs on 3.14 never saw it.

Split the join out of the f-string. Every tracked .py now parses under 3.10, and
the full suite is green on both 3.10 and 3.14 (762 passed, 7 skipped).

Also removes an `engine_pin.py` that a mirror loop leaked into
`src/add_method/_bundled/tooling/` — the pin is dev-only and the bundle parity
guards name it as such.

ENGINE_MD5 re-aimed; four engine twins mirrored.

author: Tin Dang
`test_four_engine_twins_and_both_pins` hard-required all four twins, including
the two `.add/tooling/` copies. Both are gitignored — they are materialised by
`add init`, not tracked — so a fresh checkout has neither and the guard turned a
clean clone red. It passed locally only because this working tree dogfoods both.

The SHIPPED twin (`src/add_method/_bundled/tooling/`) stays hard-required: it is
tracked, and a consumer installs what it holds. The dogfood twins exists-skip
individually, matching `test_tree_parity.py`'s established convention — but any
twin that IS present is still byte-compared, and the guard asserts at least one
twin was checked, so the skip can never be what makes a drifted twin pass.

Verified the way CI runs it: fresh clone -> `cli.py init` -> `pytest -q` from
add-method/ on Python 3.10. 762 passed, 7 skipped.

author: Tin Dang
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.

1 participant