Skip to content

feat(method): the front door teaches a walk that runs, and the roster names agents that exist - #210

Merged
TinDang97 merged 9 commits into
mainfrom
feat/method-truth-sweep
Sep 2, 2026
Merged

feat(method): the front door teaches a walk that runs, and the roster names agents that exist#210
TinDang97 merged 9 commits into
mainfrom
feat/method-truth-sweep

Conversation

@TinDang97

Copy link
Copy Markdown
Collaborator

Eleven tasks across three milestones, every one gated on a receipt. Four critic passes over the skill corpus converged on one shape: the documentation described a method the engine did not implement, and nothing could see the gap — every existing guard checked nouns the engine EXPOSES rather than capabilities the prose PROMISES.

first-run-truth — 5/5

  • init refuses under an ancestor bundle without --nested; status names the ancestor it found instead of reporting no bundle at all.
  • status and brief report the derived beat through one _beat_of, so a frozen node never reads direction again — with a check proving there is only one derivation.
  • GETTING-STARTED.md is now executed by the suite: its anchored blocks are lifted, run, and their claims held. Three were false, including the claim it made about freeze itself.
  • The add run idiom is one canonical line in all three shipped spots. The path appears twice on purpose: run READS the report, the wrapped command WRITES it.
  • Both front doors dropped the cost claim this repo's own benchmark retracts, gained the kind×size ladder, and the surviving measured claim carries its provenance inline (n=1, ADD 2.0.0, pinned model).

live-persona-tier — 3/3

  • init seeds selectable personas, so the roster's tier-1 selector has something to find.
  • personas.md states all seven scaffolded keys and marks the two the selector reads, citing the roster file by path. It had called them "read by nothing" while add-worker.md selected on both.
  • seed.md's three named archetypes (backend-systems · security-reviewer · frontend-ux) resolved to no corpus file. The resolution guard now sweeps every shipped file and matches a bare slug, not only a full path.

roster-reachable — 3/3

  • The roster table's executor column named four agents this package does not ship, and the skill named neither of the two it does. Both are now bound to the installer's roster tree.
  • Each agent file carries an explicit MAY RUN / NEVER RUN verb split, named literally. The seam verbs (freeze · gate · done · milestone-done · check) stay forbidden and security stays HARD-STOP.
  • add-worker gained the explore mode that phases/explore.md shipped without.
  • The two installer twins declared different global tree sets (personas-index was in the JS and not the Python), and neither deployed the roster anywhere a host looks. A global install now lands it beside the skill through the shared per-file replace path — idempotent, tombstoning retired agents, never sweeping a user's own subagents.

Plus edges-documented

The direction guide called five sections four, documented a phantom After part that exists in no scaffold and no engine surface, and stated a covers: grammar missing two of the five forms the engine's REFERENT pattern admits.

Enforcement worked against me five times

Five gates refused PASS on an unbound probed assumption — a · probe: line makes an A<n> a first-class covers: referent, exactly like a Must. Each needed a genuine binding check written and the contract re-frozen. One probe was verified non-vacuous by mutating brief and confirming the check bites.

Two widenings, both declared: a recorded add replan before touching the repo-root README.md (same refuted claim, outside the frozen scope), then a re-freeze with it added; and test_shipped_docs.py now sweeps agents/ — nothing had ever linted the roster files.

Also fixed en route: the persona_routing_key check fired on untouched scaffold slots — splitting <from the closed taxonomy, comma-separated> on commas left interior words carrying no bracket. A guard firing on a missing thing. Committed separately with the four engine twins synced and ENGINE_MD5 re-aimed.

The benchmark report is held byte-identical to HEAD by a check: the repair for a refuted claim is to change the claim, never the measurement.

Verification

991 passed, 7 skipped across both test roots (add-method/tests/ and add-method/tooling/), the full CI shape.

…tracts

Four-lens critic review of the ADD skill, personas and roster on 2026-09-01
produced 4 root causes. This commit persists the frozen contracts only.

author: Tin Dang
…eads from stamps, and a routing key outside its taxonomy is a finding

Three frozen contracts from the method-truth sweep, all red-first.

nested-bundle-guard — `ancestor_bundle()` walks up from the candidate root; `status`
in a bundle-less subdirectory names the project and prints `next: cd <project> &&
add status` instead of `next: add init`; `init` refuses with R:RIVALBUNDLE before it
writes anything, unless `--nested` says a second bundle is deliberate. Reproduced
before the fix: following the engine's own `next:` line built a rival bundle.

beat-read-truth — `status` and `brief` derive the beat from the stamps rather than
the `status:` field, which `freeze` never advances. A frozen node read `[direction]`
beside `next: add brief`, while `todo` said build and `doctor` reported card_drift.
Fixed on the read side, so every existing bundle heals on the next read and reopen
is covered too. BEAT_TYPES keeps non-beat node types printing their own status.

persona-routing-keys-checked — PERSONA_FLOWS and PERSONA_TASK_KINDS become the single
source the scaffold writes and `doctor` checks; an out-of-taxonomy value is an `info`
finding naming the node, the key, the offending value and the allowed set. `explore`
joins the task kinds: a whole shipped lane whose selector predicate was unsatisfiable.
ADD's own two personas carried five of six values outside the taxonomy and now pass.

Twin sync and pin re-aim follow once the remaining engine edits land.

author: Tin Dang
… reaches the corpus

persona-tier-live, red-first. "Personas carry the expertise" was false on every
fresh install: 3.0 seeded no personas, so `.add/personas/` was empty and the roster's
selector had nothing to search; the 232-lens teacher corpus could not rescue it,
because not one of its files carries `flow:` or `task-kinds:`; and neither agent file
named the corpus or its routing index at all. The steady state was the generic
fallback, taken silently, with nothing in the receipt recording that no expert loaded.

`init` now seeds the four persona templates as real Persona nodes and names them in
its note. They are the project's from the moment they land — `put` never overwrites,
so an edited or deleted persona survives every re-init.

Both agents gain a three-tier selection ladder: the project's own roster, then the
teacher corpus routed through `personas-index/use-when.md` with a stated tie-break,
then the generic fallback — and each must name the tier it selected from in its
Return, because a fallback nobody can see is the failure this ladder exists to end.
Both corpus trees are optional installs, so a lean install skips that tier silently.

Recorded as a replan on the frozen node: A2 read the seed set as the three existing
planner templates, whose `flow:` values cover design, advisor and verify but not
build — so M2, every roster beat has a seeded match, was unsatisfiable with those
three. build-craftsman is the fourth, distilled from the corpus the same way.

The templates also lose their 2.x section numbers (§3 Scope, §4 suite), which resolve
to nothing in ABF-1 and would otherwise have shipped into every new bundle.

Verified on a real fresh bundle: 4 personas seeded, doctor clean, and the subdirectory
guard refusing a rival bundle with a runnable recovery.

author: Tin Dang
…e suite executes

getting-started-executed, red-first. BEYOND-CODE.md's non-code walk was executed by a
test; the PRIMARY code walk was executed by nothing — the shipped-doc suite checked it
only for phantom verbs. That asymmetry was the root cause, and these were its symptoms:

- The walk never showed `add brief`, so a reader following it literally hits R:UNBRIEFED
  at the gate. `brief` is now Beat 2's first command, with the reason stated.
- The freeze line omitted `--authority human`, so the documented ONE approval recorded
  as `authority: process` — a ledger indistinguishable from an agent approving itself.
  The paragraph claiming authority is "computed, never asserted" was half true and now
  says which half: the FLOOR is computed and unassertable; the stamp is not.
- The guide promised freeze refuses template placeholders and a node froze with five
  surviving: `placeholders_in` reads `- ` bullets in RULES/ASSUMPTIONS/CHECKS, and CARD's
  `goal:` is a keyed line in an unread section. The guard now covers CARD `goal:` at every
  depth. EVIDENCE and LESSONS stay exempt — they are filled by the run and the close.

Executing the walk also found a precondition the guide never stated: the declared `scope:`
paths must exist, or the gate cannot digest them and refuses the PASS. Now stated.

The runnable blocks are lifted from the shipped document through named anchors, so the
document cannot rot while the test passes against a private fork. The fixture writes the
reader's own tests using the ids the document's CHECKS name, which proves the binding and
not merely the sequence.

author: Tin Dang
…finished

The CARD `goal:` guard fed `_is_scaffold` through `placeholders_in`, so a node with
drafted RULES and CHECKS but no goal yet started reporting the `scaffold` beat — and
`todo` stopped counting its assumption sweep down mid-authoring, which is exactly when
that countdown is useful. `placeholders_in` now takes `card=`: freeze asks the full
question (is this finished), `_is_scaffold` asks the narrower one (was this ever
authored). Two questions, two answers, one function.

Fixture fallout, all one cause: eight suites reached a post-freeze state through nodes
that never stated a goal, which is the thing the guard exists to refuse. The shared
`draft_direction` helper now authors CARD — one edit, every suite that uses it follows —
and the six suites carrying their own inline authoring were patched the same way.

That includes `test_beyond_code_walkthrough`: the shipped non-code walkthrough had also
been approving a node whose stated goal was still `<one line>` — the same defect
GETTING-STARTED.md claimed freeze prevented, in the guide that proves its own commands.

author: Tin Dang
…CARD-guard fixture sweep

Twin sync across all four live trees (canonical tooling, the package _bundled twin, and
the two gitignored dogfood bundles) plus the three skill trees and the roster. ENGINE_MD5
and ENGINE_PKG_MD5 re-aimed for the sweep's engine changes.

Reverted the `placeholders_in` split from the previous commit: `test_authoring_beat`
drives the affordance layer and `freeze` against the same node and asserts they agree,
so there is exactly ONE notion of "authored" by design. Letting `_is_scaffold` ignore
CARD while `freeze` counted it recreated the defect that test exists to catch — a node
advised to freeze that freeze then refuses. The `card=` parameter stays for callers that
genuinely need only the three body sections, documented as not being a second notion.

The remaining fixtures follow the same one-line pattern: a node that reaches a
post-freeze state, or the direction beat, now states its goal. `test_assumption_sweep`
needed it most — its sweep refusals were being masked by a placeholder refusal, so those
checks had been probing the wrong guard.

author: Tin Dang
…red, not as wrong

`add new Persona` scaffolds `task-kinds: <from the closed taxonomy, comma-separated>`.
The routing-key check split that value on commas and then tested each token for a
leading `<`, so `the`, `closed`, `taxonomy` and `comma-separated>` all read as values
outside the closed vocabulary. Every freshly scaffolded persona reported two findings
naming words out of its own placeholder.

The guard fired on a MISSING thing — nobody had authored a value yet. It now skips the
key whenever the RAW value still carries a placeholder, which is the same notion of
"unauthored" `placeholders_in` already uses everywhere else, and drops the per-token
bracket test the comma split defeated.

Four engine twins synced and ENGINE_MD5 re-aimed.

author: Tin Dang
… names agents that exist

Eleven tasks across three milestones, all gated on receipts. Four critic passes over the
skill corpus converged on one shape: the documentation described a method the engine did
not implement, and nothing could see the gap because every guard checked nouns the engine
EXPOSES rather than capabilities the prose PROMISES.

first-run-truth (5/5)
  · `init` refuses under an ancestor bundle without `--nested`; `status` names the ancestor
    it found instead of reporting no bundle at all.
  · `status` and `brief` report the DERIVED beat through one function, so a frozen node
    never reads `direction` again — and a new check proves there is only one derivation.
  · GETTING-STARTED.md is EXECUTED by the suite: its anchored blocks are lifted, run, and
    their claims held. Three of them were false, including the freeze claim it made about
    itself.
  · The `add run` idiom is one canonical line in all three shipped spots, and the path
    appears twice on purpose — `run` READS the report, the wrapped command WRITES it.
  · Both front doors dropped the cost claim this repo's own benchmark RETRACTS, gained the
    kind×size ladder, and the measured claim now carries its provenance inline.

live-persona-tier (3/3)
  · `init` seeds selectable personas, so the roster's tier-1 selector has something to find.
  · `personas.md` states all seven scaffolded keys and marks the two the selector reads,
    citing the roster file by path — it had called them "read by nothing".
  · `seed.md`'s three named archetypes resolved to no corpus file; the resolution guard now
    sweeps every shipped file and matches a bare slug, not only a full path.

roster-reachable (3/3)
  · The roster table's executor column named four agents this package does not ship, and the
    skill named neither of the two it does. Both bound to the installer's roster tree now.
  · Each agent file carries an explicit MAY RUN / NEVER RUN verb split, named literally; the
    seam verbs stay forbidden and security stays HARD-STOP. `add-worker` gained the `explore`
    mode that `phases/explore.md` shipped without.
  · The two installer twins declared different global tree sets (`personas-index` was in the
    JS and not the Python), and NEITHER deployed the roster anywhere a host looks. A global
    install now lands it beside the skill through the shared per-file replace path.

Plus `edges-documented`: the direction guide called five sections four, documented a phantom
`After` part, and stated a `covers:` grammar missing two of the five forms the engine admits.

Every probed assumption is now bound to a check — five gates refused a PASS until they were,
which is the enforcement working. The benchmark report is held byte-identical to HEAD: the
repair for a refuted claim is to change the claim, never the measurement.

Suite: 991 passed, 7 skipped across both test roots.

author: Tin Dang

# And the gate still REFUSES on that receipt — the assumption's whole point is that the
# weakness stays visible rather than being papered over.
node = root / cid.lstrip("/")
import sys
from pathlib import Path

import pytest
import sys
from pathlib import Path

import pytest
…cannot fail is not a guard

Review of the method-truth sweep, by a second reader and by re-reading the prose the
sweep itself wrote. Seven defects, and the two that matter most are the same shape as
the ones the sweep was closing.

THE SHIPPED BUG
`ancestor_bundle` accepted any directory holding a file named `index.md` as an ADD
bundle root. `index.md` is the standard homepage filename for MkDocs — the tool this
repo publishes with — plus Docusaurus, Jekyll and Hugo. So in any docs tree, `add init`
refused a legitimate project with `R:RIVALBUNDLE` and `add status` announced "you are
inside the ADD project at ..." — naming a directory that holds no bundle, and handing
back a `cd` that leads nowhere. Three false statements to someone who has never run ADD.
`init` writes `abf_version:` into every bundle index and nothing else does, so both
branches now read the file's head and require that marker.

WHY IT SHIPPED GREEN
The test named "ignores a marker that is not a bundle" only ever wrote `graph.json` — a
file the function never reads. Delete every `index.md` check from the engine and that
test still passed. It discriminated nothing, and its `covers: A2` claimed it did. It now
writes the hostile inputs: a bare docs homepage, and an unmarked `index.md` inside a
`.add/`. Verified red against the previous engine.

THE SAME CLASS, TWICE MORE
- `test_the_parity_test_reads_both_twins` inspected a 1768-character slice of its own
  file, all of it ABOVE its own definition and starting mid-sentence in an unrelated
  docstring. It could not see the hard-coded tree set twelve lines below, and reordering
  the two functions flipped its verdict: it measured source layout, not the property it
  named. It now walks the module's AST for collection literals restating the declaration,
  and the restated union it was blind to is read from git instead.
- `_referent_forms` in test_edges_documented ran a regex over a regex PATTERN, matched
  nothing, and always fell through to a helper nothing else called. One reader now.

PROSE THE SWEEP GOT WRONG
- `direction.md` corrected the `covers:` grammar to six forms and called them five —
  the same off-by-one as the "four sections" bug it was fixing, one paragraph later.
  Bound to the engine's own count, and verified red at "five".
- `streams.md` renamed the executor column and two sentences kept calling it
  `agentType`, a column that no longer exists. A new check holds the prose to the
  table's header row; verified red by reintroducing the word.
- The root README carried the same measured claim as the package README with no
  provenance, because the M5 check read one front door while its M1 sibling read both.

INSTALLER
`_reconcile_global` gained a THIRD write target while the caller kept ONE `except
OSError` reporting `cannot write global home <home>`. On a machine where
`~/.claude/agents` is not writable — a namespace other tools own — the install aborted
naming a home that was fine, after the mirror and skill had landed. The roster write is
now non-fatal in both twins and names its own path when it degrades.

Also: the seeding loop no longer shadows `init`'s `title` parameter, and
`ancestor_bundle`'s docstring no longer claims a containment ceiling the code has no
check for — what actually holds is the symlink clause `resolve()` delivers.

Suite: 995 passed, 7 skipped. Four engine twins synced, ENGINE_MD5 re-aimed.

author: Tin Dang
@TinDang97
TinDang97 merged commit ee54390 into main Sep 2, 2026
8 checks passed
@TinDang97
TinDang97 deleted the feat/method-truth-sweep branch September 2, 2026 03:38
TinDang97 added a commit that referenced this pull request Sep 3, 2026
…amp is well-formed (#211)

* fix(engine): a guard asks whether what a stamp attests is TRUE, not whether the stamp is well-formed

Four critic passes over the merged 3.3.0 state converged on one shape, a level below the
one PR #210 closed: 3.3.0's guards ask whether a stamp is well-FORMED and never whether
the thing it attests is TRUE. `done` checked that a gate existed, not its verdict. The
freeze seal covered the contract text, not two of the three id classes the gate binds.

Measured on a fresh bundle, CLI only, no flags and no hand-editing:

    add freeze sec2 --by tin --authority human   (sensitivity: security)
    add run sec2 -- false                        -> receipt, exit 1
    add gate sec2 HARD-STOP --reason "SQL injection in login"
        HARD-STOP recorded; sec2 stays in `direction`   <- the engine's own words
    add done sec2
        /tasks/sec2.md is done                          <- and it left the board

HARD-STOP was the one verdict every integrity refusal deliberately lets through — a
security finding must always be recordable — and also the one that closed the node.

Three fixes, each red-first:

* `done` reads each entitling gate's `outcome` against CLOSING_VERDICTS = (PASS,
  RISK-ACCEPTED). `gate` is untouched: all three verdicts stay recordable. A resolving
  PASS after a stop still closes, and a stop before a `reopen` does not block the
  reopened node.

* `binding_digest()` seals the referent SET — real `E<n>` ids and probed `A<n>` ids —
  as a second field beside `direction:`. `referents_of` binds RULES + EDGES + probed
  assumptions; `direction_digest` sealed RULES + CHECKS + `gives:`, so the cheapest way
  past "these rules have no reported passing check: A1, E1" was to DELETE the obligation.
  A second digest rather than a wider `direction:`: widening would re-digest every
  already-frozen node, and sealing only the referent set keeps prose refinement free.
  An absent `binding:` means "cannot verify", so no pre-seal node is stranded.

* `new` refuses a slug held by ANY type, iterating TYPE_DIR rather than a hand list.
  Receipts are addressed by bare slug under `tasks/`, so two nodes sharing a slug shared
  one receipt stream — a red Task closed on a Milestone's green run, defeating R:GREENLIE
  with evidence that was never its own.

Deleted: `add.quick()` — new -> freeze(process) -> run -> gate PASS in ONE call, wired to
no CLI verb, named in no shipped doc, kept green by four assertions. A new census guard
replaces the name check: no public engine function may call both `new` and `done`/`gate`,
so re-adding the lane under any name goes red.

That census also found `checks_sync` unreachable — a real CHECKS-from-suite compiler with
9 tests whose own refusal names `add checks <slug> --verify`, a verb that does not exist.
Recorded as a strict xfail against /tasks/wire-or-delete-checks-sync.md rather than
accommodated by weakening the guard; wiring a 25th verb is outside this cut.

Suite 1006 passed / 7 skipped / 1 xfailed across BOTH test roots. Four engine twins synced,
ENGINE_MD5 re-aimed. `test_stamp_field_integrity`'s freeze-stamp field census gained
`binding` — an enumeration updated for a genuinely new field, not a relaxed assertion.

Two tasks stay UNFROZEN by design: `done-reads-the-verdict` and
`delete-the-unwired-quick-lane` sit at the security floor, whose interview is a human
seam. The code and its checks are green; the attestation is the human's to make.

Refs: critic passes A (engine truth), B (personas), C (cold start), D (promise vs artifact)
author: Tin Dang

* docs(add): the verdict-truth nodes, two gated on real receipts

Records the ADD nodes for the verdict-truth milestone. Both architecture-floored tasks
are frozen at `human`, briefed, run against their own suites and gated PASS on fresh
receipts:

  seal-covers-what-binds        PASS · receipt runs/2.md · freshness fresh
  slug-is-unique-across-types   PASS · receipt runs/2.md · freshness fresh

Both needed one re-cross: the gate refused the first PASS because each node's Reject
(R:SHEDBIND, R:CROSSRECEIPT) was declared but cited by no CHECKS line. The repair was to
bind the Reject to the check that actually proves it and refreeze — never to sign the gap
away with RISK-ACCEPTED. That is the fifth and sixth time this project's own gate has
refused a PASS over an unbound referent, and each time the refusal was right.

Two tasks stay UNFROZEN, deliberately. `done-reads-the-verdict` and
`delete-the-unwired-quick-lane` are security-floored, so `freeze` opens an interview —
seven readings a human must be shown before the ONE approval. Their code and checks are
green and committed; the attestation is not an agent's to make. Answering that interview
as the human, in the milestone whose entire subject is stamps that attest what never
happened, is the defect wearing a different hat.

`wire-or-delete-checks-sync` is scaffolded, not authored: the reachability census found
`checks_sync` unwired, and whether to wire a 25th verb or delete a working feature is a
direction call, not a build one.

Suite 1013 passed / 7 skipped / 1 xfailed across both test roots.

author: Tin Dang

* fix(engine): a refusal names a fix that runs, and a verb reads the flag it accepts

Tier 2 of the four-critic revalidation: the engine was steering users into the holes
Tier 1 had just closed. Every fix here is red-first.

THE NEXT: LINES

`BEAT_NEXT` is the one table `status`, `todo`, `brief`, `freeze` and every CARD line read
from, so a wrong idiom there is wrong in fourteen places.

* build carried `add run {slug} -- <cmd>` — the idiom GETTING-STARTED warns produces
  `ids: unknown`. A builder who obeyed it on genuinely green work then met "no reported
  passing check" with RISK-ACCEPTED named as the only exit: a permanent false waiver on
  correct work. Now the canonical double-`--junitxml` form (braces doubled — both
  consumers pass it through `.format(slug=…)`).
* verify carried `add gate {slug}`, an argparse error. A crash, not a refusal, with no
  `next:` to recover from. Now `add gate {slug} PASS --by "<name>"`.
* The non-git freshness refusal diagnosed git correctly and then said `add run` — which
  provably cannot fix it, so obeying the line looped forever. It now names `git init`.
* `add learn testing` said `next: add status`. The vocabulary is closed and five words
  long; the refusal now names it and shows the form.
* `add check <typo> --all` raised FileNotFoundError at the operator — the only traceback
  across ~35 malformed inputs. `cli.py` read the file before reaching `add.check`'s guard.

ACCEPTED-AND-DROPPED

* `run` was the only verb whose lookup was `scan(root).get(cid) or {}`. A typo'd slug got
  `receipt 1 recorded (exit 0)`, a green line and a `next:` pointing at nothing, while the
  real task still had no receipt — and `doctor` then reported the orphan the engine had
  just manufactured. It refuses like every other verb, keeping `run`'s dict return shape.

* `gate --authority` was declared, passed in, and overwritten before any read. Frozen M3
  (test_gate_verb.py:224) says a gate's authority is COMPUTED, never the caller's claim —
  and that rule is right, since a claim at the gate is the agent approving its own work.
  So the engine is unchanged and the CLI stops accepting an argument it discards.

* `freeze` had the opposite bug: it honoured ANY value, so `--authority process` silently
  downgraded a security freeze. This was the last item still open from 3.3.0. One new
  reader, `claimed_authority()`, serves both: a claim may rise above the computed floor,
  never sink below it -> R:FLOORDIVE.

THE LENS THAT NEVER ARRIVED

`brief` resolved a persona through the PATH grammar, but a lens is written as a bare slug
(`advise` validates exactly that against the Persona nodes). From `/tasks/x.md` the slug
`build-craftsman` normalised to `/tasks/build-craftsman` — neither a persona nor even a
`.md`. It missed every time: no seeded lens has ever reached a brief, by either key. The
brief now resolves the roster the way `advise` and `wave` do, and reads `advised_by:` as
well as `persona:` — the two the gate's R:NOCOVERAGE already treats as equals.

Three of four seeded personas opened with an ORIENT command that was silent or an error:
`add.py status --all` (a LIBRARY — exit 0, no output, which a planner reads as "clean
bundle"), `status --brief`, `graph`, `milestone-confirm`. Repointed at cli.py and real
verbs, under a census over every shipped template so a lens added later cannot skip it.

Suite 1027 passed / 7 skipped / 1 xfailed across both roots. Four engine twins and the
templates tree synced; ENGINE_MD5 and ENGINE_PKG_MD5 both re-aimed.

author: Tin Dang

* fix(docs): a claim on the front door is backed by the shipped artifact, or it is not made

Tier 4 of the four-critic revalidation, plus the one Tier 3 item that was losing data.
Each false claim is now bound by a guard that reads the ENGINE and fails if the prose
drifts back — the guard-class lesson from 3.2, applied to the front door.

THE TRUST CLAIM WAS THE BIGGEST ONE

Both READMEs promised that "gaming a test to get green is structurally impossible" /
"is treated as tampering". It is not. The freeze seal digests RULES · CHECKS · `gives:`
— the NAMES — and never the test FILES those names point at. Running the README's own
"See it yourself" demo (reopen --to build, gut both frozen tests to bare `pass`, re-run,
gate) records `gate PASS ... freshness: fresh` and closes the node. SKILL.md:123 and
GETTING-STARTED:374 both state this limit plainly; only the front door contradicted them.
The claim now says what the seal actually guarantees — the contract cannot move under a
build without the change appearing in the record — and a counter-guard pins the honest
limit so the claim is never "fixed" by deleting the disclosure.

BOUNDARIES A SECURITY REVIEWER READS

"Network: one optional advisory update check — a single HTTPS GET to the npm registry ...
disable with ADD_NO_UPDATE_CHECK=1". There is no network code in the shipped surface at
all — no https/urllib/socket/fetch in bin/cli.js, _installer.py, _cli.py, add.py or
cli.py — and nothing reads that variable. A reviewer setting it in CI believed they had
closed an egress. The true statement (zero network) is strictly stronger than the false
one. A guard now checks both directions, and a second refuses any documented ADD_* var
the code does not read.

THE REST

* `--stage mvp`, the Install section's only flag example, which the installer explicitly
  rejects — retired in 3.0, and there is no stage concept in 3.x.
* `cli.py deltas` sold as "the per-lane scoreboard: what got gated, passed, healed (GEPA)".
  It prints `[LENS] spec: text`. GEPA, per-lane, scoreboard and "fast lane" name nothing
  in the engine.
* `add upgrade` — a real verb that archives a 2.x bundle byte-for-byte, writes
  MIGRATION.md and inits a fresh 3.0 bundle — was named in no README, and the engine's own
  2.x refusal said `next: add init`, which writes nine files INTO the live 2.x directory
  and leaves a permanent doctor error. Both now name `upgrade`.
* The marketplace entry — the only text a user sees before installing — sold the retired
  2.x seven-phase vocabulary and claimed the book materializes into the project, which
  both READMEs and SKILL.md deny.
* `SOUL.md`, a 2.x bundle file 3.x never creates, was still the routing target for tone
  rules in persona-author.
* GETTING-STARTED said the scaffold lands at beat `direction`; the engine says `scaffold`,
  and the walkthrough test hardcoded the doc's version so it could not catch the drift.
  The guard now measures the beat off the engine.
* The README leads with "Most changes never create a node" and linked a walkthrough with
  ZERO occurrences of ladder, direct lane or explore. §2 now routes by the ladder.
* `--nested` (the only way past R:RIVALBUNDLE, while both READMEs promise monorepo support)
  and `--no-skill` (without it a plugin bootstrap installs a duplicate skill that shadows
  the plugin's) were documented nowhere. Both are now, at the point of need.
* SKILL.md's interview line omitted `correct` — the verdict for "the AI got this wrong".

DELTAS WERE LOSING LINES

A delta whose status was typo'd matched nothing in DELTA_LINE's status group and vanished
from `add deltas` — no warning, no doctor finding — while deltas.md documented three
reject codes for exactly this (`unknown_competency`, `no_evidence`, `unknown_status`),
none of which appeared anywhere in the engine. `deltas` now reports every line it cannot
place, under those three names, and refuses an unknown `--status` instead of reading it as
an empty result. Verified against this repo's own 40 deltas: zero false positives.

Suite 1046 passed / 7 skipped / 1 xfailed across both roots. Engine twins, all three skill
trees and both MD5 pins re-synced.

author: Tin Dang

* docs(add): the refusals-that-work nodes, five gated on real receipts

Records the ADD nodes for the Tier 2 milestone. All five frozen at `human`, briefed, run
against their own suites and gated PASS on fresh receipts — first try each, because the
`covers:` bindings were written at direction rather than discovered at the gate. That is
the fix for the two re-crosses Tier 1 cost.

  run-refuses-a-phantom-node             PASS
  gate-honours-or-refuses-authority      PASS
  next-lines-are-runnable                PASS
  the-persona-reaches-the-worker         PASS
  seeded-personas-orient-on-real-verbs   PASS

author: Tin Dang

* docs(add): record the human interview on done-reads-the-verdict

Four readings marked `correct` by the human, three deferred. The node stays UNFROZEN:
the engine's own note is that a `correct` is cleared by editing the item, and two of the
four corrections cannot be written yet because they conflict.

A1 (a human-authority HARD-STOP should close the node) collides with the fact that a gate's
authority is COMPUTED from the floor — so on a security node every stop is stamped `human`,
and taking A1 literally restores the exact measured walk this milestone closed. A1 also
pulls against A2, which says a stop blocks permanently.

Raised with the human; unanswered so far. No engine change made on a guess.

author: Tin Dang

* feat(engine): the human's interview answers, built — a force-close is an act, not a floor

Both security-floored tasks are now interviewed, frozen at `human`, and gated PASS on fresh
receipts. Four readings came back `correct`; one of them could not be built as written, and
the human resolved it.

A1 — A HUMAN MAY SHIP OVER A FINDING, BUT MUST SAY SO

The literal correction ("a human-authority HARD-STOP closes the node") would have restored the
exact measured walk this milestone closed: a gate's authority is COMPUTED from the node's floor,
so on a security task EVERY stop is stamped `human`. Raised, and resolved by the human: the
force-close is a DELIBERATE ACT. `add done <slug> --override "<why>"` closes a stopped node,
refuses without a reason, writes its own `act: done, override:` stamp, and never buys the ONE
approval — an unfrozen node is still refused. The ledger now distinguishes a person who chose
to ship over a finding from a floor that quietly permitted it.

A3 — A STOP IS A RECORD, AND A RECORD NEEDS A SEAL

`gate` now refuses a HARD-STOP on a node that was never frozen (R:UNSEALED) and refuses nothing
else, so writing a finding never gets harder. This NARROWS A13, an assumption frozen in 3.3.0
whose premise ("a HARD-STOP never closes a task") both halves of this branch disproved. Its test
is rewritten to the narrowed rule and a new counter-guard enumerates both refusal tiers to prove
every other refusal still lets a stop through.

A4 — AN UNREADABLE VERDICT FAILS OPEN

A gate stamp with no `outcome` closes, rather than stranding nodes an older engine wrote. The one
correction that loosens; recorded as such on the node.

A2, A5, A6, R:STOPSHIPS — confirmed as drafted.

TWO THINGS THE INTERVIEW ITSELF FOUND

* `interview` wrote its sidecar as bare markdown, so `doctor` filed `error missing_frontmatter`
  against a file the engine had just created — the `orphan_receipt` shape one verb over. It now
  carries frontmatter, and `Interview` joins ABF_TYPES. That list lives in TWO places by design
  (add.py compiles the graph, scripts/validate_bundle.py is the independent M0 oracle), and
  updating one made the two disagree about the FORMAT — the one disagreement the parity test
  exists to forbid. Both updated, and a new guard pins them to each other.
* `gate` refused the first security PASS with R:NOCOVERAGE: no seeded persona is a security
  lens (critic B3's gap, hit live). Added `personas/gate-security-reviewer.md`, distilled from
  the teacher corpus — the authorization lens for ADD's own gate. It reaches the brief, which
  is the B1 fix proven end to end on a real node.

CHECKS_SYNC DELETED

The human chose delete. One correction to what was asked: `checks_verify` is LIVE — `doctor`
calls it for `checks_citation` — so deleting both would have removed a shipped check. Only the
dead writer `checks_sync` is gone, with the nine tests that drove it; `checks_verify`,
`checks_of` and their coverage stay. The census now passes outright and the strict xfail is
removed, so `/tasks/wire-or-delete-checks-sync.md` is answered and its scaffold deleted.

Also fixed: `test_checks_of_keys_by_file_too` hardcoded `test_sync_is_idempotent` as a known
name collision and went red when one of the two was deleted for an unrelated reason — reporting
a keying defect that did not exist. It now DISCOVERS a collision instead of naming one.

Suite 1058 passed / 7 skipped across both roots. Twins and both pins re-synced.

author: Tin Dang

* docs(add): close verdict-truth (4/4) and refusals-that-work (5/5)

Both milestones carry their goal, scope, ground, risks and a per-task evidence row.
Nine tasks, every one gated PASS on a fresh receipt; the two security-floored ones at
authority `human` with a named lens.

`v3-final-collateral` stays open — a pre-existing template this branch did not touch.

author: Tin Dang

* fix(engine): an interview sidecar is a record about a node, not a node on the board

Registering `Interview` in ABF_TYPES stopped `doctor` filing a finding against a file the
engine had just written — and put the sidecars in `status` beside real tasks. The two rules
are independent: a type the graph knows, and a type the roster shows. Sidecars now sit with
`Run`, out of the roster.

author: Tin Dang

* fix(engine): a verb that reports success has read what it is reporting on

Three readers each answered "fine" over input they never examined, and the
interview routed all three to a refusal at the point of entry rather than a
finding somewhere downstream.

`new` judged exactly one slot — `sensitivity:`, because it computes the
authority floor — and recorded every other field verbatim. `kind:` looked
like a prose slot and is not: it is the Task side of a routing predicate
whose Persona side (`task-kinds:`) has been validated since 3.3.0. Two sides
of one match, held to different standards, so `--kind frontend` was accepted
and the task silently lost its lens for life. It now refuses with the closed
taxonomy enumerated, the same shape as the `--profile` and `sensitivity`
refusals already shipped.

`join` iterated `(d / "tasks").glob("*.md")` for each stream path, and glob
on a directory that does not exist yields nothing quietly — so `add join
/typo/.add` printed "joined 0 stream(s)" and exited 0. The first cut keyed
the refusal on the presence of `tasks/`, which was wrong: `init` never
writes that directory, `new` creates it lazily, so a real stream bundle that
has not authored a node yet has none. Corrected at the contract (A2
re-frozen) to `_is_bundle_index` — the `abf_version:` marker `init` always
writes — and the CLI now propagates the refusal to a non-zero exit.

`doctor` reported "no findings" over a bundle whose only task was 100%
scaffold. `placeholders_in` already detects exactly that, and had exactly
one caller: the gate. So the failure only ever surfaced at the END of the
loop, to someone who had already done the work, never to the newcomer
running `doctor` to ask whether their bundle was in good shape. The first
wiring could not fire at all — `scan()` nodes carry no body and
`placeholders_in` reads the body — which is the same asymmetry that made
`_open_decisions` return [] last session.

Three tasks, all gated PASS on their own receipts, 15 new checks, every one
red first. Both pins re-aimed; four engine twins synced.

Refs: milestone scaffold-truth (3 of 7)
author: Tin Dang

* fix(engine): a slot the scaffold offers is a slot a reader reads

Two guards that could not fire, for opposite reasons — one because the
value it judges was written where nothing looks, one because the text it
requires was never required.

`scope:` had exactly one slot in the Task scaffold, in `## PLAN`, and its
only reader is `fm.get("scope")`. Fill the slot the template offers and the
gate answers "the node declares no `scope:`" — so `phantom_scope`, the
refusal for a scope naming paths that do not exist, has never once been able
to fire on a scaffolded node. The slot consumed the author's attention and
returned nothing.

Moving it to frontmatter was the easy half. The first cut seeded a
placeholder, following the `gives:` precedent, and turned 29 green tests red:
`gives:` is DESCRIPTIVE, so an unfilled placeholder is merely unhelpful,
while `scope:` is ENFORCED — a placeholder makes every fresh node declare a
scope it cannot satisfy, degrading freshness and making any edit outside it
a violation. A3 was re-frozen against the measurement and the slot now seeds
EMPTY: the key's presence in frontmatter is the prompt, and its emptiness is
what every reader already means by "none declared".

`assumption_sweep` waived a dimension on the token alone, while the docstring
one line above promised "retired with `n/a` and a reason". Six bare `n/a`
lines were therefore a six-line off switch for the whole six-dimension
matrix, each cheaper to type than one honest assumption. Measured before
changing anything: 584 assumption lines across both bundles, 4 of which
actually waive, and all 4 already state a reason — so making the code mean
what its docstring says refuses nothing already written. The bar is any
non-empty reason, never a quality bar: a notary cannot judge whether a
reason is good, and a bar it cannot judge only teaches padding.

The sweep then caught this branch's own scope task with two unswept
(dimension, surface) pairs at freeze. Fixed by writing the two readings, not
by waiving them.

Two tasks, both gated PASS on their own receipts, 12 new checks, every one
red first. tests/ root: 1077 passed, 7 skipped. Pin re-aimed, four twins
synced.

Refs: milestone scaffold-truth (5 of 7)
author: Tin Dang

* feat(engine): a lane's scaffold produces a node that lane will accept

`--kind explore` is a whole shipped lane — a guide, a freeze refusal, a gate
path that reads `## FINDINGS` instead of a receipt, three refusal codes of
its own — and it had no front door. It emitted the identical build-lane
body, and `freeze` then refused it for a `budget:` line the body never
offered. The author was handed the wrong lane's node and then blamed for it.

The scaffold now branches: RULES ask for questions, `## PLAN` carries the
budget slot, `## FINDINGS` is present with the exacting
`F<n> (answers M<n>) · … · (evidence: <ref>)` shape and no finding in it.
`## CHECKS` STAYS — the first reading of this task had it dropped, and
explore.md contradicts that in as many words.

Shipping the budget slot then reproduced this milestone's own defect class
inside it: `^budget:\s*\S` matched `budget: <one hard number …>`, so a slot
satisfied the requirement it exists to prompt for, and a fully-drafted
explore froze with no budget. My own check for it was vacuous — it asserted
`freeze` refused without asserting WHICH refusal, and the node was being
refused for its other placeholders. `test_explore_gate` caught what my check
could not. Both fixed: the guard rejects a `<…>` value, and the check names
its refusal.

Five persona lenses seeded — docs-writer, interface-designer, data-steward,
security-reviewer, explore-investigator — each distilled from a named
teacher file, closing 5 of 11 task kinds that routed to nothing. `security`
mattered most: the gate REFUSES a security PASS without a named lens
(R:NOCOVERAGE), and this branch hit that refusal live, authored a lens into
THIS repo's bundle to clear it, and never seeded it. The fix did not ship.
The seeded template is the artifact.

The coverage guard enumerates PERSONA_TASK_KINDS from the constant, so the
twelfth kind fails it, and it names the unclaimed kinds rather than sending
a maintainer through nine files. Proven red by withholding the five
templates: "5 task kind(s) route to no seeded lens: data, docs, explore,
security, ui".

Milestone scaffold-truth CLOSED, 7/7 exit criteria, 7 tasks all gated PASS
on their own receipts. Suite 1060 -> 1099 passing, 7 skipped, both roots.
Four engine twins synced, both pins re-aimed.

Refs: milestone scaffold-truth (7 of 7)
author: Tin Dang

* fix(test): a check that asserts an empty list must assert it looked at something

CI failed on py3.10 and py3.12 while the tree was green on 3.14. The cause
was mine and it was worse than a crash.

`test_every_orient_command_runs_clean` bound its root with
`add.init(tmp_path, "code", "T") or tmp_path`. `add.init` returns a TUPLE,
which is truthy, so `--root` received the repr of the entire graph dict.
Every ORIENT command then ran against a bundle that does not exist: `status`
printed nothing and exited 0, and the check passed VACUOUSLY on every
command it was ever given.

It surfaced only on CI because `Path.exists()` on a name that long raises
ENAMETOOLONG on 3.10/3.12 and returns False on 3.14 — a version-dependent
crash standing in for a check that was never running. The green was the bug;
the red was the honest signal.

Fixed by binding the root, and guarded against recurrence: the check now
counts the commands it executed and fails if fewer than three ran. Verified
it bites by corrupting a real ORIENT line — `--nosuchflag` is now reported
with argparse's own error, where before it was silently accepted.

This is the milestone's own defect class, in my test rather than the engine:
a check that is perfectly well-formed and attests nothing.

Refs: PR #211
author: Tin Dang

* fix(method): every seeded lens orients on a command that runs, and the guide stops teaching a dead slot

Three of nine seeded personas carried an `ORIENT on load` line, so the guard
that executes those commands only ever inspected a third of the roster —
and its non-vacuity floor was a magic `>= 3` that happened to match. All
nine now carry one, and the floor is the roster itself: a new template with
no ORIENT line, or one whose command the check cannot execute, fails here
rather than quietly shrinking what the guard inspects. Verified by removing
one lens's line (red) and restoring it (green).

Each command is argument-free and chosen for the lens, not filled in:
security-reviewer takes `status --all` because a control is only as good as
the state it reads and the ledger IS that state; explore-investigator adds
`deltas` because the cheapest finding is one somebody already wrote down.
`check` was rejected as a candidate — it requires a ref, so it could never
run clean in this guard.

Docs, same branch, same thesis. GETTING-STARTED's worked `## PLAN` block
still carried `scope: src/, tests/` — the slot this branch deleted, and a
duplicate besides, since the walkthrough's own `add new --scope "src/,tests/"`
had already put it in frontmatter where readers look. A reader following the
guide would have hit "the node declares no `scope:`" at the gate. That is
precisely the false-front-door class the Tier 4 work closed, reopened by my
own engine change, and the walkthrough test executes these blocks so it
would have taught it faithfully.

SKILL.md said `## PLAN` holds "`scope:` tokens". Corrected, and the explore
lane's required `budget:` named there for the first time. The correction
initially pushed SKILL.md to 177 lines against a 176 budget; absorbed by
compressing the same bullet rather than re-pinning the budget. Back to
exactly 176/176, mirrored across all three skill trees.

Suite 1100 passed, 7 skipped, both roots.

Refs: PR #211
author: Tin Dang

* fix(test): a check that cannot fail is a claim, not a guard

A sweep of this branch's new checks for the shape that produced the CI
failure: a check that passes without exercising its subject. Ten confirmed,
seven more hardened. Two of the ten found real defects that had been sitting
behind dead branches.

REOPENED AND RE-GATED: scope-is-where-its-readers-look

Its M4 and S2 named a `phantom_scope` that does not exist. The engine keys
that refusal on the CARD claiming a scope the frontmatter lacks — not on
path existence, and not from `doctor`, which emits sixteen codes and not
that one. Both binding checks were vacuous: one filtered `doctor()` for a
code `doctor()` cannot emit, so the list was empty for every input; the
other hand-rolled a path-existence loop over a value the test itself had
just written, a tautology naming a predicate the engine does not have.

The finding the task was built on is still true and still fixed — the slot
moved to frontmatter where readers look. Only the second claim I attached to
it was false. Reopened to direction, M4 retired in place with a check that
PROVES the retirement by pinning the real predicate from source, S2 dropped,
A4/A6 and the contract line corrected, re-frozen at `human`, re-gated PASS.
The gate refused the first re-PASS for the retired M4 and for an unbound E3;
both were bound rather than waived.

TWO REAL DEFECTS BEHIND DEAD BRANCHES

`brief` said nothing when no lens resolved. Its check passed only because
the fixture's slug was the literal word `unlensed`, which the brief echoed
in `<task id=...>`; re-run with `zzz` it matched nothing at all. The Must
was good and unimplemented, so the engine now says the omission out loud —
a worker could not previously tell "no expert was loaded" from "the expert
had no note", and the receipt recorded neither.

The unbound-covers refusal offered ONLY `RISK-ACCEPTED` even when the
receipt carried no check ids at all. That is R:FALSEWAIVER exactly: a signed
waiver as the sole exit from possibly-correct work whose runner simply
emitted no JUnit report. The check meant to catch it sat under
`if "no reported passing check" in note:` and the fixture, being unauthored,
was refused for placeholders instead — the branch was never taken. Authored
the fixture, made the assertion unconditional, and split the refusal so an
unbound receipt names the re-run FIRST and the waiver second.

THE OTHER EIGHT

- A README flag check whose two filters were mutually exclusive, so its only
  assertion was `[]` for every possible input while the real finding list was
  computed and discarded.
- Three live-bundle reporters reduced to `assert isinstance(...)`. They scan
  `add-method/.add/`, which is GITIGNORED with zero tracked files — so on CI
  they measured a bundle that does not exist, and `isinstance` was the only
  thing left that could pass. Repointed at the root bundle (113 tracked
  files), with an explicit skip when neither is present.
- An orphan-count guard comparing `0 == 0`. Its docstring said the number
  "is expected to stay at eight" and that the test "exists to notice if it
  silently drops". It dropped to zero and stayed green, because both sides
  degrade together. It now injects an orphan, so it has a subject on any
  bundle in any state.
- A `git diff` over a gitignored path, guarding nine nodes that no longer
  exist in this repo at all. It now asserts its own premise, so restoring one
  goes red instead of inheriting a guard that was never running.
- A parity check satisfied by any shared unrelated code; it now requires the
  injected `edge_unresolved` on both sides.
- Seven more given non-vacuity floors tied to a real source rather than a
  magic number — the roster's own length, the stamp-writer census population,
  the reachability candidate pool.

Every fix demonstrated: each new or hardened check was shown red by
withholding its subject, then green when restored.

Suite 1100 passed, 7 skipped. Pin re-aimed, four twins synced.

Refs: PR #211
author: Tin Dang

* feat(engine): run reads the report path the command already names

`--junitxml` on `add run` told the engine where to READ a JUnit report; the
command itself wrote it. So the documented build line named one path twice:

    add run x --junitxml "$X" -- pytest ... --junitxml="$X"

The engine already held the command as a list, so the second mention carried no
information it did not have. It was a restatement the caller could get wrong,
and getting it wrong cost a receipt with `ids: unknown` — which the gate reads
as every rule unbound, and which (until this branch) offered a signed waiver as
the only way forward. A typo in a path was punished as a coverage gap.

`run` now sniffs the path out of the command when no `junit=` is given.
`_sniff_report` is deliberately narrow: two known flag spellings, `=path` and
two-token form, last occurrence wins — the same precedence the runners
themselves honour. Anything else returns None rather than guessing, because a
receipt bound to a file the command never wrote is worse than an unbound one:
the unbound receipt is visibly unbound, the wrongly-bound one is not.

The flag stays, as an override. A runner may write its report to a path the
command line never names — a config file, a fixed CI location — and it is
consulted first so a sniffed value can never beat a stated one. Everything
downstream is unchanged: staleness, extraction and `ids` see the same value
whichever route it arrived by. `BEAT_NEXT`'s build hint now names the path once.

Dogfooded: this task's own receipt was produced by a command carrying no
engine-side flag, and bound all 7 checks at `kind: test-ids`, `freshness:
content`.

Task: run-reads-the-path-it-was-handed (milestone scaffold-truth)
ENGINE_MD5 re-aimed 72b0bc5c -> 9a800083

author: Tin Dang
TinDang97 added a commit that referenced this pull request Sep 3, 2026
…true (#212)

Two merged-but-unreleased PRs ship here. 3.3.0 was tagged before #210 landed,
so this release carries both it and #211.

#210 — the documentation described a method the engine did not implement, and
nothing could see the gap: every guard checked nouns the engine EXPOSES rather
than capabilities the prose PROMISES. GETTING-STARTED.md is now executed by the
suite, the roster names agents that exist, and `init` seeds selectable personas.

#211 — every integrity guard asked whether a record was SHAPED right; none asked
whether the thing it recorded had happened. A `gate HARD-STOP` walked to `done`.
A red Task closed on another node's receipt through a slug collision. `brief`
resolved no lens at all and said nothing about it. And a scaffold `new` wrote
could not survive the guards `new` recommended next.

Version bumped across all eight declarations — five manifests, the ENGINE stamp,
and the three shipped skill trees — plus the dogfood bundle's own engine stamp.
ENGINE_MD5 re-aimed 9a800083 -> 7932ec06; cli.py untouched, so ENGINE_PKG_MD5
stands.

Also fixed here, self-inflicted last turn: `run` learned to sniff the report
path, which left the shipped skill teaching the doubled `--junitxml` in four
places, one with a sentence explaining that the repetition was deliberate.
`test_receipt_idiom_truth` could not catch it — it decides which examples to
check by whether the engine-side flag is present, so removing that flag makes an
example invisible rather than wrong. The new guard binds the taught line to
`BEAT_NEXT["build"]`, the string a user is actually shown, and was mutation
-tested: reintroducing the doubling turns it red.

Suite: 1111 passed, 7 skipped across both test roots.

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