From 70d46ff078301438da726062909fc03f476a3b23 Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 00:04:18 +0700 Subject: [PATCH 1/9] chore(method): draft six-phase-loop milestone (await-confirm) 8->6 phase merge (specify absorbs scenarios; verify absorbs observe) + phase-guide disclosure into the bundle-owning roster agents so the orchestrator loads only SKILL.md + per-phase persona presets + the build-entry spec echo. Human decisions from the 2026-07-14 interview recorded in Ground; tasks gated until milestone-confirm. refs: milestone six-phase-loop (await-confirm) author: Tin Dang --- .add/milestones/six-phase-loop/MILESTONE.md | 56 +++++++++++++++++++++ .add/state.json | 21 ++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 .add/milestones/six-phase-loop/MILESTONE.md diff --git a/.add/milestones/six-phase-loop/MILESTONE.md b/.add/milestones/six-phase-loop/MILESTONE.md new file mode 100644 index 00000000..ff4a6cbc --- /dev/null +++ b/.add/milestones/six-phase-loop/MILESTONE.md @@ -0,0 +1,56 @@ +# MILESTONE: Six-phase loop with bundle-owned subagents + +goal: Merge specify+scenarios and verify+observe (8->6 phases), disclose phase guides into the lean roster's bundle agents so the orchestrator loads only SKILL.md, add per-phase persona presets + a build-entry spec echo +rationale: +stage: mvp · status: active · created: 2026-07-13T17:03:37+00:00 +release: pending +relations: + +> SDD living doc for this milestone. Keep it THIN: breadth, shared decisions, and +> exit criteria only — per-task detail lives in each `.add/tasks//TASK.md`, +> written just-in-time. Update this doc whenever a task reveals a milestone gap. + +## Scope +In: +Out: + +> UI/UX in scope? Name it precisely, not "make it nice" — information architecture · +> interaction pattern · visual hierarchy · design tokens · component states · +> accessibility floor (WCAG AA) · responsive breakpoints · user journey +> (`.add/personas-teacher/design/`). Precise ≠ distinctive: skip generic AI-design +> defaults (cream+serif+terracotta · near-black+neon · broadsheet-hairline) and name ONE +> deliberate signature element instead (Claude Code's `frontend-design` skill). A UI +> feature also triggers DESIGN.md via the `add` skill's design.md. + +## Ground (current assets this builds on) +- 8-phase lifecycle (expectations-first, PR #145): setup · specify · scenarios · plan · tests · build · verify · observe; ONE freeze at plan->tests; compound ticks (call-floor 4/5) already collapse freeze->tests and build->verify->gate. +- Phase bundles exist in prose (SKILL.md table Bundle column: DIRECTION/BUILD/VERIFY, three-phase-flow milestone) but the ORCHESTRATOR still loads every phase guide inline. +- Lean 5-agent roster (add-design/add-build/add-verify/add-persona/add-advisor) spans bundles; dynamic teacher-grade personas infra shipped (PR #137); agents live in THREE trees incl _bundled. +- Read path after call-floor: SKILL.md 9498B + per-phase guide each phase (~33KB/task fast lane). +- Evidence for the spec-echo task: WM1 transcripts show drift starts at build entry (contract frozen, then built from memory). +- Human decisions recorded 2026-07-14 (AskUserQuestion): TRUE phase merge 8->6 · personas-per-phase on the lean roster (NOT a per-step agent rebuild) · build-entry spec echo YES · phase guides disclose INTO the bundle subagents, orchestrator loads SKILL.md only. + +## Scope +IN: engine phase-list 8->6 (specify absorbs scenarios; verify absorbs observe) + section renumber + template updates + in-flight task migration; phase-guide re-cut to 6 files + pool re-anchor; bundle-guide disclosure into roster agents (orchestrator reads SKILL.md only); per-phase persona presets; build-entry spec echo; recipe/kickoff updates. +OUT: gate SEMANTICS (one recorded outcome, security HARD-STOP, tamper/scope guards — all unchanged) · the freeze position (plan->tests) · roster membership changes (stays 5) · stdout terseness · benchmark harness changes. + +## Tasks (breadth-first; engine merges FIRST — guides/agents re-cut against the new truth) +- [ ] phase-merge-specify depends-on: none — scenarios fold INTO specify (§2 Given/When/Then becomes the §1 rules' scenario block; one drafting phase); engine PHASES list, section indices, advance/recipe paths, TASK.md.tmpl + fast twin; migration maps in-flight tasks (scenarios->specify) — the phase-index-vs-section bug class from expectations-first is the named trap +- [ ] phase-merge-verify depends-on: phase-merge-specify — observe folds INTO verify (§7 spec-delta capture becomes an optional post-gate block of §6; deltas/fold lifecycles unchanged, they read the file not the phase); gate stays the phase's one outcome; 6-phase list lands: setup · specify · plan · tests · build · verify +- [ ] guide-recut depends-on: phase-merge-verify — phases/*.md re-cut to 6 guides (1-specify absorbs 2-scenarios; 6-verify absorbs 7-observe); pools re-anchored (human-signed restructure, POOLS comments cite this milestone); orchestrator fast-lane read path <= 30KB finally lands +- [ ] bundle-disclosure depends-on: guide-recut — the orchestrator loads ONLY SKILL.md; each roster agent's prompt names + loads its bundle's guides (add-design <- specify+plan; add-build <- tests+build; add-verify <- verify); SKILL.md flow table routes to AGENTS for bundle work, guides stay readable inline for the no-spawn lane (inline remains first-class per the user's inline-over-heavy-spawns feedback) +- [ ] persona-presets depends-on: bundle-disclosure — per-phase teacher-grade persona preset lines in the three roster agent files (dynamic-personas infra); advisory, never lowers a gate +- [ ] build-entry-spec-echo depends-on: phase-merge-verify — the tick INTO build (advance tests->build or freeze --cross chain) prints the §1 Must/Reject lines + frozen §3 contract summary (spec-in-context where drift starts; propose-not-impose, fail-open like the scope echo) + +## Exit criteria (observable) +- [ ] the 6-phase lifecycle is test-pinned end-to-end: new-task -> specify -> plan(freeze) -> tests -> build -> verify(gate) with compound ticks intact (<- phase-merge-*) +- [ ] an in-flight 8-phase task migrates loud-and-safe (scenarios->specify, observe->verify) — test-pinned (<- phase-merge-*) +- [ ] 6 phase guides, pools green at re-anchored baselines, orchestrator fast-lane read path <= 30KB (<- guide-recut) +- [ ] roster agent files name + load their bundle guides; SKILL.md carries no per-phase guide bodies (<- bundle-disclosure) +- [ ] each of the 3 bundle agents carries per-phase persona preset lines (<- persona-presets) +- [ ] the build-entry echo renders Must/Reject + contract summary; bare non-build ticks byte-identical — test-pinned (<- build-entry-spec-echo) +- [ ] floors untouched: full fence green, ENGINE_MD5 re-pinned, 3-tree parity, gate semantics byte-identical +- [ ] (paid, human-gated) WM1 re-measure after merge: fidelity >= 0.97 held, calls <= 12 + +## Risks +- The expectations-first migration class: phase-index vs §-section off-by-one (named trap in every merge task) · ~3500 fixtures pin phase sequences — merges are the two biggest test-ripple tasks this repo has attempted since flow-reorder · gate tamper tripwire fires if migration touches frozen §3s (recover via re-cross --by, precedent recorded). diff --git a/.add/state.json b/.add/state.json index 4aed1447..813e334e 100644 --- a/.add/state.json +++ b/.add/state.json @@ -1,7 +1,7 @@ { "project": "AIDD / ADD Methodology", "stage": "mvp", - "active_task": "skill-orient-split", + "active_task": null, "tasks": { "add-check": { "title": "add.py check: validate .add project integrity", @@ -9040,7 +9040,7 @@ } }, "created": "2026-05-28T15:39:04+00:00", - "updated": "2026-07-13T17:01:22+00:00", + "updated": "2026-07-13T17:03:37+00:00", "milestones": { "v1-1": { "title": "v1.1 \u2014 adoption & ergonomics", @@ -9429,9 +9429,21 @@ "status": "active", "created": "2026-07-13T15:52:21+00:00", "updated": "2026-07-13T15:52:21+00:00" + }, + "six-phase-loop": { + "title": "Six-phase loop with bundle-owned subagents", + "goal": "Merge specify+scenarios and verify+observe (8->6 phases), disclose phase guides into the lean roster's bundle agents so the orchestrator loads only SKILL.md, add per-phase persona presets + a build-entry spec echo", + "stage": "mvp", + "status": "active", + "created": "2026-07-13T17:03:37+00:00", + "updated": "2026-07-13T17:03:37+00:00", + "confirmed": false, + "confirmed_at": null, + "confirmed_by": null, + "await_confirm": true } }, - "active_milestone": "call-floor", + "active_milestone": "six-phase-loop", "archived": [ { "slug": "v5", @@ -10269,7 +10281,8 @@ "expectations-first", "plan-legibility", "ceremony-to-effort", - "call-floor" + "call-floor", + "six-phase-loop" ], "active_tasks": { "flow-simplification": "phase-review", From e6e3ccb95aba3c56990f5c501b25123aecc21432 Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 00:59:09 +0700 Subject: [PATCH 2/9] =?UTF-8?q?feat(method):=20phase-merge-specify=20?= =?UTF-8?q?=E2=80=94=20scenarios=20merged=20into=20specify=20(six-phase-lo?= =?UTF-8?q?op=201/6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TRUE phase merge, first of two (human-decided over recipe-ticks): the scenarios PHASE folds into specify — one drafting phase now produces §1 rules AND §2 Given/When/Then. The TASK.md §-section shape is UNTOUCHED (sections are the stable API; only the lifecycle list shrinks). Engine (synced x3 — tooling · .add/tooling · _bundled): - constants.py: PHASES=(specify,plan,tests,build,verify,observe,done); _SKIPPABLE_PHASES=(observe,); PHASE_GUIDE specify action gains the Given/When/Then duty; PHASE_OWNER/PHASE_AGENT drop scenarios; PHASE_GROUPS DIRECTION=(specify,plan,tests) - add.py: _normalize_phase_tokens legacy map gains scenarios->specify (silent, TOTAL, idempotent, normalize-on-read — expectations-first precedent); _FRONT_PHASES/(spawn-hints/guide-file map/freeze early-phase check/skip-rationale regex follow); _RETIRED_SKIP_TOKENS: a pre-merge `skips: scenarios` header is tolerated LOUD (note at the observe crossing, filtered from the skip set) while non-skippable tokens keep the die — the frozen M13 pin (zero skip logic on non-skippable crossings) holds; task_phases: explicit _PHASE_SECTIONS table replaces the index-derived n->section mapping (the named off-by-one trap, hit live) - installer showcase twins (bin/cli.js BRAND_LOOP · _installer.py _LOOP): 6-step loop, grounded by test_onboarding_brand Tests: NEW test_phase_merge_specify.py (8 red->green: phase list + maps, advance lands plan, scenarios refused, legacy state normalizes, old skip declaration tolerated loud, bad token still dies). ~30 fixture suites repaired as the declared sequence-shift ripple (test_min_pillar walk now 4 advances to verify; test_add advance pin; onboarding showcase slice; report dots; phase-detail blocks; fast-lane skips rewritten to observe) — zero tests weakened. Full fence 3517/3517 OK. Pins: ENGINE_MD5 -> b27ce845 · ENGINE_PKG_MD5 -> 870a4ce0 · SEAMS _declared_scope anchor -> add.py:5631. Gate: PASS (compound tick from build; scope widened +installer twins via sanctioned re-cross). refs: six-phase-loop milestone (37d5a80), stacked on feat/call-floor (#147) author: Tin Dang --- .add/SEAMS.md | 2 +- .add/state.json | 80 +++++++++- .add/tasks/phase-merge-specify/TASK.md | 105 +++++++++++++ add-method/bin/cli.js | 2 +- .../src/add_method/_bundled/tooling/add.py | 73 ++++++--- .../_bundled/tooling/add_engine/constants.py | 24 +-- .../add_method/_bundled/tooling/engine_pin.py | 4 +- add-method/src/add_method/_installer.py | 2 +- add-method/tooling/add.py | 73 ++++++--- add-method/tooling/add_engine/constants.py | 24 +-- add-method/tooling/engine_pin.py | 4 +- add-method/tooling/test_add.py | 6 +- add-method/tooling/test_agent_portability.py | 2 +- .../tooling/test_build_expectations_gate.py | 3 +- add-method/tooling/test_bundle_advance.py | 4 +- .../tooling/test_cross_component_contract.py | 8 +- .../tooling/test_cross_component_milestone.py | 44 +++--- add-method/tooling/test_engine_batch_ops.py | 7 +- add-method/tooling/test_fast_lane_skips.py | 107 +++++++------ add-method/tooling/test_fast_lane_template.py | 9 +- add-method/tooling/test_fast_new_task_flag.py | 2 +- .../tooling/test_freeze_before_build_gate.py | 3 +- .../tooling/test_freeze_gate_universal.py | 3 +- add-method/tooling/test_gate_owner_marker.py | 6 +- add-method/tooling/test_ground_phase.py | 25 ++- add-method/tooling/test_hint_batch_ops.py | 6 +- add-method/tooling/test_machine_state.py | 2 +- add-method/tooling/test_min_pillar.py | 6 +- add-method/tooling/test_next_footer_engine.py | 33 ++-- add-method/tooling/test_onboarding_brand.py | 14 +- .../tooling/test_per_component_verify.py | 2 +- add-method/tooling/test_phase_build_guard.py | 7 +- add-method/tooling/test_phase_bundles.py | 9 +- add-method/tooling/test_phase_detail.py | 47 +++--- .../tooling/test_phase_merge_specify.py | 142 ++++++++++++++++++ add-method/tooling/test_plan_phase_flow.py | 9 +- add-method/tooling/test_proof_harness.py | 2 +- add-method/tooling/test_reopen_transition.py | 8 +- add-method/tooling/test_report.py | 4 +- add-method/tooling/test_setup_lock.py | 3 +- .../tooling/test_skip_error_ergonomics.py | 6 +- add-method/tooling/test_step_spawn_hint.py | 7 +- add-method/tooling/test_streams.py | 4 +- add-method/tooling/test_template_form_tags.py | 4 +- add-method/tooling/test_unflagged_freeze.py | 6 +- 45 files changed, 650 insertions(+), 293 deletions(-) create mode 100644 .add/tasks/phase-merge-specify/TASK.md create mode 100644 add-method/tooling/test_phase_merge_specify.py diff --git a/.add/SEAMS.md b/.add/SEAMS.md index 71dd8913..a6a0c679 100644 --- a/.add/SEAMS.md +++ b/.add/SEAMS.md @@ -54,7 +54,7 @@ Citations: 232 files reference "byte-identical" in `.add/tasks/` — method: ## scope-token-grammar Name: §5 "Scope (may touch):" token-resolution grammar -Anchor: `add-method/tooling/add.py:5619` (`_declared_scope`) +Anchor: `add-method/tooling/add.py:5631` (`_declared_scope`) Contract: `_declared_scope` reads ONLY the first physical line after the §5 header — a wrapped multi-line list silently truncates. Each backticked token then resolves independently: `./...` = this task's dir, any token containing `/` = project-root-relative, diff --git a/.add/state.json b/.add/state.json index 813e334e..85202d4e 100644 --- a/.add/state.json +++ b/.add/state.json @@ -1,7 +1,7 @@ { "project": "AIDD / ADD Methodology", "stage": "mvp", - "active_task": null, + "active_task": "phase-merge-specify", "tasks": { "add-check": { "title": "add.py check: validate .add project integrity", @@ -9037,10 +9037,68 @@ "email": "tindang.ht97@gmail.com", "source": "git" } + }, + "phase-merge-specify": { + "title": "Merge scenarios into specify: 8->7 phase list", + "phase": "done", + "gate": "PASS", + "milestone": "six-phase-loop", + "depends_on": [], + "created": "2026-07-13T17:12:47+00:00", + "updated": "2026-07-13T17:57:58+00:00", + "fast": true, + "freeze": { + "version": "v1", + "frozen_at": "2026-07-13T17:12:47+00:00", + "approved_by": "Tin Dang", + "actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } + }, + "flag_verified": true, + "tripwire": { + "contract_md5": "897c5770316a68758bda603d91689732", + "tests": { + "add-method/tooling/test_phase_merge_specify.py": "676a663952ad6fee509f7f358600ef07" + } + }, + "scope": { + "declared": [ + "add-method/tooling/", + "add-method/src/add_method/_bundled/tooling/", + ".add/tooling/", + "add-method/.add/", + "add-method/bin/", + "add-method/src/add_method/_installer.py" + ], + "snapshot_md5": "31ff7c2501425f61e601e0b2eb10db01" + }, + "heal": { + "attempts": 1, + "history": [ + { + "at": "2026-07-13T17:57:37+00:00", + "reason": "scope_violation: task 'phase-merge-specify' touched outside its declared \u00a75 Scope \u2014 add-method/bin/cli.js \u00b7 add-method/src/add_method/_installer.py (2 total)", + "source": "scope" + } + ] + }, + "recross": { + "by": "Tin Dang", + "at": "2026-07-13T17:57:57+00:00", + "from_phase": "build" + }, + "gate_actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } } }, "created": "2026-05-28T15:39:04+00:00", - "updated": "2026-07-13T17:03:37+00:00", + "updated": "2026-07-13T17:57:58+00:00", "milestones": { "v1-1": { "title": "v1.1 \u2014 adoption & ergonomics", @@ -9436,11 +9494,16 @@ "stage": "mvp", "status": "active", "created": "2026-07-13T17:03:37+00:00", - "updated": "2026-07-13T17:03:37+00:00", - "confirmed": false, - "confirmed_at": null, - "confirmed_by": null, - "await_confirm": true + "updated": "2026-07-13T17:10:12+00:00", + "confirmed": true, + "confirmed_at": "2026-07-13T17:10:12+00:00", + "confirmed_by": "tindang", + "await_confirm": true, + "actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } } }, "active_milestone": "six-phase-loop", @@ -10296,7 +10359,8 @@ "expectations-first": "book-plan-align", "plan-legibility": "relations-surface", "ceremony-to-effort": "fold-draft-at-close", - "call-floor": "skill-orient-split" + "call-floor": "skill-orient-split", + "six-phase-loop": "phase-merge-specify" }, "todos": [ { diff --git a/.add/tasks/phase-merge-specify/TASK.md b/.add/tasks/phase-merge-specify/TASK.md new file mode 100644 index 00000000..e4c83d84 --- /dev/null +++ b/.add/tasks/phase-merge-specify/TASK.md @@ -0,0 +1,105 @@ +# TASK: Merge scenarios into specify: 8->7 phase list + +slug: phase-merge-specify · created: 2026-07-14 · stage: mvp +milestone: six-phase-loop +autonomy: auto +phase: done +fast: true + +> Fast lane — one small task, minimal sections, filled top-to-bottom. The trust floor still +> holds: a FROZEN §3 contract · ≥1 red test before build · a recorded §6 gate (security = HARD-STOP). +> The acceptance scenario collapses into §1 `Accept:`; OBSERVE is one optional line at the gate. + +--- + +## 1 · SPECIFY — the rules + +> Project the expectations from the milestone Ground + this request — light, not re-invented. +Feature: phase-merge-specify — the scenarios PHASE folds into specify (six-phase-loop 1/6, human-decided TRUE merge): one drafting phase produces §1 rules AND §2 Given/When/Then; the §-section shape of TASK.md is UNTOUCHED (sections are the stable API; only the lifecycle list shrinks) +Must: + - PHASES drops "scenarios" (7 tokens incl done); a bare `advance` from specify lands at plan; every phase map follows (PHASE_GUIDE action for specify gains the Given/When/Then clause · PHASE_OWNER · PHASE_GROUPS DIRECTION=(specify,plan,tests) · PHASE_AGENT · _FRONT_PHASES · the status blurb map · the phases/*.md guide-file map) + - legacy state migrates on read: phase token "scenarios" -> "specify" via _normalize_phase_tokens's _legacy map (ground/contract precedent — silent, TOTAL, idempotent, normalize-on-read only) + - _SKIPPABLE_PHASES shrinks to ("observe",); a pre-merge task HEADER still declaring `skip: scenarios` is tolerated loud (an advisory note, never a die) — the declaration is simply ignored + - freeze's early-phase check (add.py ~6516 `phase in ("specify","scenarios")`) reads specify only; the skip-rationale regex drops the scenarios alternative +Reject: + - `advance --to scenarios` / `phase scenarios ` -> the existing unknown/invalid-phase refusal (scenarios is no longer a phase token) + - a NEW skip declaration naming scenarios -> the advisory note (not the old bad-token die, not a silent accept) +Accept: Given a fresh task at specify with §1+§2 drafted, When `advance` runs bare, Then the task is at plan (one hop, no scenarios stop); and Given a pre-merge state.json with a task at phase "scenarios", When any command loads state, Then the task reads as phase "specify" byte-safe elsewhere. +Boundary: two legacy dialects — the state token ("scenarios" in state.json) and the header skip declaration (`skip: scenarios — reason`); both must land tolerated, differently (silent normalize vs loud ignore) +Assumptions: ⚠ ~19 fixture files step through the scenarios phase and shift by one advance — why: measured by grep, but sequence-shift breakage is only visible at the fence; if wrong (more suites pin the 8-list): each is a declared doc-truth ripple, fixed within scope (cost: fence reruns) + +--- + +## 3 · PLAN — the change plan: ground · contract · build-strategy + +### Grounding +Touches (files · symbols): add-method/tooling/add_engine/constants.py:PHASES/_SKIPPABLE_PHASES/PHASE_GUIDE/PHASE_OWNER/PHASE_GROUPS/PHASE_AGENT · add-method/tooling/add.py:_normalize_phase_tokens/_FRONT_PHASES/_SKIP_RATIONALE_CLAUSE_RE/freeze-check-6516/status-blurb-2025/guide-file-map-3059/skip-reader-1805 · fixture files stepping the scenarios phase (fence-named, declared ripples) +Context (working folder): ENGINE_MD5 + ENGINE_PKG_MD5 both re-aim (constants.py is add_engine/); SEAMS pin drifts on add.py shifts; sync x3 +Honors (patterns / conventions): expectations-first migration precedent (_legacy map, normalize-on-read) · sections never renumber (§3 stays §3 everywhere) · defaults loud, never silent-skip · guides/SKILL.md prose re-cut deferred to guide-recut (doc-truth transitional state accepted within the milestone) +Anchors the contract cites: PHASES · _normalize_phase_tokens · _SKIPPABLE_PHASES · PHASE_GROUPS +Ground SHA: 37d5a80 — stamped by freeze + +### Contract + +``` +constants.py: + PHASES = ("specify", "plan", "tests", "build", "verify", "observe", "done") + _SKIPPABLE_PHASES = ("observe",) + PHASE_GUIDE["specify"] action += "; write one Given/When/Then per Must AND per Reject (§2)" + PHASE_GROUPS["DIRECTION"] = ("specify", "plan", "tests") + PHASE_OWNER/PHASE_AGENT/PHASE_GUIDE: scenarios keys removed +add.py: + _normalize_phase_tokens: _legacy = {"ground": "specify", "contract": "plan", + "scenarios": "specify"} + _FRONT_PHASES = ("specify", "plan", "tests") + freeze check: phase in ("specify",) [or == "specify"] + _SKIP_RATIONALE_CLAUSE_RE: (observe) only + skip reader: token "scenarios" -> print note "scenarios merged into specify — skip + declaration ignored", never die; other bad tokens keep the existing die +guide-file map: scenarios entry removed (2-scenarios.md file untouched until guide-recut) +``` + +`Least-sure flag surfaced at freeze:` [test] the tolerated-skip note path — why: the skip reader dies on bad tokens today and its callers may assume a validated set; threading a tolerated-but-ignored token through _task_skip_set's return shape risks a downstream KeyError; if wrong: filter the token OUT of the returned set and note at the READ site only (cost: one refactor round) +Status: FROZEN @ v1 — approved by Tin Dang +### Build-strategy +Scope (may touch): `add-method/tooling/` `add-method/src/add_method/_bundled/tooling/` `.add/tooling/` `add-method/.add/` `add-method/bin/` `add-method/src/add_method/_installer.py` +Strategy & known-problem fixes: red tests -> constants maps -> add.py sites -> skip tolerance -> migration token -> fence -> fixture ripple repair rounds. Traps: phase-index-vs-§-section off-by-one (sections DO NOT move) · test_phase_bundles hardcoded expected_bundle dict · fast-lane-skips suite pins scenarios as a skippable example (rewrite to observe) · sync engine x3 never tests · ENGINE_PKG_MD5 re-aims too (constants.py). +Approach (domain strategy): lifecycle shrinks, section API frozen; migrate-on-read; loud tolerance for retired declarations + +### AI-verify record (required when gate_mode: ai-plan-verify) +- [x] §3 PLAN grounding anchors resolve in the current tree (constants.py:59/347/72-124, add.py:125-141/1603/1805/2025/3059/6181/6516 all read this session) +- [x] §1 every Must + every Reject present, each paired with its outcome +- [x] §3 Contract shape is concrete +- [x] Lowest-confidence flag surfaced and substantive (the tolerated-skip return-shape risk) +Verified by: claude-opus-4-8 (orchestrator, inline) · at: 2026-07-14T01:20:00Z + +--- + +## 4 · TESTS — failing-first (red) + +Plan: test_phases_has_no_scenarios · test_advance_specify_lands_plan · test_legacy_state_token_normalizes · test_old_skip_declaration_tolerated_loud · test_new_phase_cmd_scenarios_refused · test_maps_carry_no_scenarios_key · test_specify_guide_action_names_gwt. +Tests live in: `add-method/tooling/test_phase_merge_specify.py` · MUST run red before Build. + +--- + +## 5 · BUILD — AI writes the code (execution) + +> The change plan was frozen in §3 PLAN. Build to it: honor the §3 Build-strategy Scope; improve on the strategy if the code teaches you better. +Strategy actually used: as planned, with two corrections the code taught: (1) skip validation + the retired-`scenarios` note stay INSIDE the observe-crossing branch — the frozen M13 pin (non-skippable crossings run ZERO skip logic) refuted an always-validate restructure; the tolerated token is filtered OUT of _task_skip_set's return exactly as the freeze flag's fallback predicted. (2) task_phases' index-derived n→§(n+1) mapping broke when PHASES shrank — replaced with the explicit _PHASE_SECTIONS table (the milestone's named off-by-one trap, hit live). Fixture ripple was ~30 suites (vs the §1 ~19 estimate), repaired in fence rounds. +Code lives in: `./src/` · Constraints: change no test, no frozen contract; stay inside the §3 Build-strategy Scope; allow-list packages only. + +--- + +## 6 · VERIFY — evidence + gate + +- [x] all tests pass · coverage held · no test or contract altered during build +- [x] green was EARNED — no overfit / vacuous asserts / stubbed-away logic +- [x] input dialect held — tests speak the spec's example formats (spec-dialect floor) +- [x] no exposed secrets, injection openings, or unexpected dependencies (security = HARD-STOP) + +Build expectations (from §1 Accept + §3 CONTRACT): PHASES carries no `scenarios`; a bare advance from specify lands at plan; a legacy `scenarios` state token normalizes on read; a pre-merge `skips: scenarios` header is tolerated loud (note at the observe crossing) while a non-skippable token still dies — confirmed by test_phase_merge_specify 8/8 (red first, then green) + the FULL fence 3517 tests OK / REAL_EXIT=0 (fence-pms-final2.log; ~30 legacy-sequence fixture suites repaired as the §1-declared ripple, zero tests weakened); engine twins ×3 byte-identical; ENGINE_MD5 b27ce845 + ENGINE_PKG_MD5 870a4ce0 re-pinned; SEAMS `_declared_scope` anchor verified at add.py:5631. + +### GATE RECORD +Outcome: PASS +Reviewed by: Tin Dang · date: 2026-07-14 + diff --git a/add-method/bin/cli.js b/add-method/bin/cli.js index 6075b5ce..82d923c2 100755 --- a/add-method/bin/cli.js +++ b/add-method/bin/cli.js @@ -414,7 +414,7 @@ async function loadClack() { // itself is folded into step 3, Plan, not a separate phase). Fail-soft: any draw error is // swallowed so a banner can never abort the install. No color is emitted (default accent: // none); the glyphs / tagline / accent are a SWAPPABLE content slot. -const BRAND_LOOP = ["Specify", "Scenarios", "Plan", "Tests", "Build", "Verify", "Observe"]; +const BRAND_LOOP = ["Specify", "Plan", "Tests", "Build", "Verify", "Observe"]; function terminalCaps(env, stream) { const width = Number(env.COLUMNS) || (stream && stream.columns) || 80; diff --git a/add-method/src/add_method/_bundled/tooling/add.py b/add-method/src/add_method/_bundled/tooling/add.py index 4c5793c4..baf21a47 100644 --- a/add-method/src/add_method/_bundled/tooling/add.py +++ b/add-method/src/add_method/_bundled/tooling/add.py @@ -123,9 +123,9 @@ def _phase_index(name: str) -> int: # --- state load/save (KEPT in add.py: write-path pinned by add._atomic_write tests) - def _normalize_phase_tokens(state: dict) -> dict: - """Rewrite the two legacy phase tokens on task records (expectations-first): `ground` - -> `specify`, `contract` -> `plan`. TOTAL · idempotent · never raises. ONLY the two - legacy tokens are touched — every other phase (including an already-migrated specify/plan + """Rewrite the legacy phase tokens on task records: `ground` -> `specify`, `contract` + -> `plan` (expectations-first) · `scenarios` -> `specify` (phase-merge-specify). TOTAL · + idempotent · never raises. ONLY the legacy tokens are touched — every other phase (including an already-migrated specify/plan and unrelated tasks) is left byte-identical, so a second pass changes nothing. Normalizes on READ only; state is persisted (migrated) solely when a command legitimately saves — never an auto-write (the over-eager persist was the prior attempt's corruption).""" @@ -134,7 +134,7 @@ def _normalize_phase_tokens(state: dict) -> dict: tasks = state.get("tasks") if not isinstance(tasks, dict): return state - _legacy = {"ground": "specify", "contract": "plan"} + _legacy = {"ground": "specify", "contract": "plan", "scenarios": "specify"} for rec in tasks.values(): if isinstance(rec, dict) and rec.get("phase") in _legacy: rec["phase"] = _legacy[rec["phase"]] @@ -1594,9 +1594,8 @@ def cmd_advance(args: argparse.Namespace) -> None: if PHASES.index(_to) <= idx: _die(f"advance_to_not_forward: task '{slug}' is already at {cur}") nxt = PHASES[idx + 1] - # fast-lane-skips: the ONLY new code the other 6 crossings ever reach is this membership - # test itself — a False result is a no-op, zero further bytes executed. scenarios/observe - # may be jumped as a SINGLE crossing (never entered) when the task declares them, is + # fast-lane-skips: observe + # may be jumped as a SINGLE crossing (never entered) when the task declares it, is # lane-eligible (fast:true | oneshot:true | project benchmark_mode:true), and states a # reason BEFORE the jump — no silent skip. Every _die below fires BEFORE the phase write # further down (validate-then-write, matching every other guard in this function). @@ -1604,9 +1603,15 @@ def cmd_advance(args: argparse.Namespace) -> None: hdr = _task_header(root, slug) tokens, skip_err = _task_skip_set(hdr) if skip_err: - _die(skip_err) # "skip_not_allowed" — fires the first time either - # skippable phase is reached, regardless of which token - # in the malformed declaration is actually bad + _die(skip_err) # "skip_not_allowed" — fires the first time a skippable + # phase is reached (the observe crossing), BEFORE any + # skip could be honored — fail-closed placement unchanged + # phase-merge-specify: a pre-merge declaration naming the retired scenarios token is + # tolerated-and-ignored, noted loud at the same crossing the machinery already reads it. + _m_sk = _SKIPS_LINE_RE.search(hdr) + if _m_sk and any(t.strip() in _RETIRED_SKIP_TOKENS for t in _m_sk.group(1).split(",")): + print("note: `skips: scenarios` — scenarios merged into specify " + "(phase-merge-specify); the declaration is ignored") if nxt in tokens: eligible = _skip_lane_eligible( state["tasks"][slug].get("fast") is True, @@ -1624,8 +1629,8 @@ def cmd_advance(args: argparse.Namespace) -> None: state["tasks"][slug].setdefault("skips", []).append({ "phase": nxt, "reason": reason, "by": identity._actor_stamp(state)["name"], "at": _now()}) - nxt = PHASES[idx + 2] # hop the skipped phase; always in-bounds (neither - # "scenarios" nor "observe" is PHASES[-1]) + nxt = PHASES[idx + 2] # hop the skipped phase; always in-bounds ("observe" + # is never PHASES[-1]) # else: nxt not in tokens -> fall through unchanged, phase entered normally (no-op) # build-boundary gate: pre-lock the front (specify..tests) is allowed, but crossing # into build/verify/observe/done is refused until `add.py lock`. @@ -1786,6 +1791,9 @@ def _task_gate_mode(hdr: str) -> str | None: # fast-lane-skips: the AI-declared skip-set — same anchored declaration grammar as # gate_mode:/sensitivity:/autonomy: (line-start or `·`, value stops at whitespace/`<`/`#`/`|`). _SKIPS_LINE_RE = re.compile(r"(?:^|·)[ \t]*skips:[ \t]*([^\s<#|]+)", re.MULTILINE) +# phase-merge-specify: header tokens for phases that were MERGED away — a pre-merge +# declaration naming one is ignored with a note, never a die (old boards keep working). +_RETIRED_SKIP_TOKENS = frozenset({"scenarios"}) def _task_skip_set(hdr: str) -> tuple[frozenset[str], str | None]: @@ -1797,11 +1805,16 @@ def _task_skip_set(hdr: str) -> tuple[frozenset[str], str | None]: comma) -> (frozenset(), "skip_not_allowed") — the WHOLE declaration is discarded on any single bad element, never partially honored (mirrors _ai_freeze_allowed's "?" fail-closed philosophy, not _task_sensitivity's "None means absent, default safely" philosophy — a - malformed CSV element is garbled, not absent). PURE.""" + malformed CSV element is garbled, not absent). A RETIRED token (`scenarios`, merged into + specify at phase-merge-specify) is filtered out, never bad — the pre-merge declaration is + tolerated-and-ignored (cmd_advance prints the advisory note; this stays PURE). PURE.""" m = _SKIPS_LINE_RE.search(hdr) if not m: return frozenset(), None toks = [t.strip() for t in m.group(1).split(",")] + toks = [t for t in toks if t not in _RETIRED_SKIP_TOKENS] + if not toks: + return frozenset(), None bad = [t for t in toks if t not in _SKIPPABLE_PHASES] if bad: # skip-error-ergonomics M1: the error carries its own repair — the raw @@ -1823,7 +1836,7 @@ def _task_skip_set(hdr: str) -> tuple[frozenset[str], str | None]: # than _flag_well_formed's [part]-tag grammar (no "none material" escape hatch): an irreversible # phase jump always needs a stated reason, or it never happens. _SKIP_RATIONALE_LINE_RE = re.compile(r"^[ \t]*Skip rationale:[ \t]*(.*)$", re.MULTILINE) -_SKIP_RATIONALE_CLAUSE_RE = re.compile(r"^\s*(scenarios|observe)\s*[-—:]\s*(.+)$") +_SKIP_RATIONALE_CLAUSE_RE = re.compile(r"^\s*(observe)\s*[-—:]\s*(.+)$") def _skip_rationale(raw0: str, phase: str) -> str | None: @@ -2021,8 +2034,7 @@ def _ai_verify_checklist_complete(raw3: str) -> bool: # one human decision. Advisory ONLY — the engine never spawns; the line just NAMES the agent # shape a parallel run would use at this step. _SPAWN_HINTS = { - "specify": "domain researcher", - "scenarios": "wide scenario sweep", + "specify": "domain researcher + wide scenario sweep", "plan": "change-plan design sweep", "tests": "red-suite test-author", "build": "independent well-scoped batch", @@ -3056,7 +3068,7 @@ def cmd_status(args: argparse.Namespace) -> None: # routed there through the CLI alone. Never a dead pointer: the path is printed # only if the file exists; a missing tree gets an install hint instead. _PHASE_GUIDE_FILES = { - "specify": "1-specify.md", "scenarios": "2-scenarios.md", + "specify": "1-specify.md", "plan": "3-plan.md", "tests": "4-tests.md", "build": "5-build.md", "verify": "6-verify.md", "observe": "7-observe.md", } @@ -5943,6 +5955,13 @@ def _clean_phase_body(body: str) -> str: return "\n".join(lines) if meaningful else "(empty)" +# phase-merge-specify: the TASK.md §-sections each work phase OWNS. Sections never +# renumber (§3 is the frozen contract everywhere); the phase list shrank instead, so +# specify owns two sections. Explicit — never derive a section from a phase index. +_PHASE_SECTIONS = {"specify": (1, 2), "plan": (3,), "tests": (4,), + "build": (5,), "verify": (6,), "observe": (7,)} + + def task_phases(root: Path, slug: str) -> list[dict]: """The frozen per-task PHASE-DETAIL shape (v9-1): parse the task section blocks into the non-terminal phases specify→observe. PURE — NO writes. Each entry is @@ -5958,12 +5977,16 @@ def task_phases(root: Path, slug: str) -> list[dict]: except OSError: # missing OR unreadable -> every phase fail-closed to "(empty)" return [{"phase": names[n], "n": n, "body": "(empty)"} for n in range(len(names))] spans = _phase_spans(text) - # spans is keyed by the TASK.md SECTION number (§1 SPECIFY .. §7 OBSERVE); the phase - # index n (0..len-1) maps to section n+1 now that §0 GROUND is gone (specify=§1). The - # old model had a §0, so n and the section number were aligned — they diverge by one now. - return [{"phase": names[n], "n": n, - "body": _clean_phase_body(spans[n + 1]) if (n + 1) in spans else "(empty)"} - for n in range(len(names))] + # spans is keyed by the TASK.md SECTION number (§1 SPECIFY .. §7 OBSERVE). The §-sections + # are the stable API and KEEP their numbers; the phase list no longer aligns 1:1 with + # them (phase-merge-specify: the specify phase owns §1 AND §2) — so the mapping is an + # explicit table, never derived from the phase index (the off-by-one bug class). + out = [] + for n, name in enumerate(names): + parts = [_clean_phase_body(spans[s]) for s in _PHASE_SECTIONS[name] if s in spans] + parts = [b for b in parts if b != "(empty)"] + out.append({"phase": name, "n": n, "body": "\n\n".join(parts) if parts else "(empty)"}) + return out def _task_title(root: Path, slug: str) -> str: @@ -6178,7 +6201,7 @@ def render_report(root: Path, state: dict, mslug: str, *, # never interprets, scores, or filters — add.py stays judgment-free, the human # signature is the gate. _MARKER_PREFIXES = (("⚠", "⚠"), ("- [~]", "[~]"), ("- [ ]", "[ ]")) -_FRONT_PHASES = ("specify", "scenarios", "plan", "tests") +_FRONT_PHASES = ("specify", "plan", "tests") def _decision_markers(body: str, section: int) -> list[dict]: @@ -6513,7 +6536,7 @@ def _next_command(phase: str, *, contract_frozen: bool = False) -> str: tells the very agent that just ran it to run it again.""" if phase == "verify": return "add.py gate PASS | RISK-ACCEPTED | HARD-STOP" - if phase in ("specify", "scenarios"): + if phase == "specify": return "add.py advance --to plan (or step-by-step: add.py advance --fill )" if phase == "plan": return "add.py advance" if contract_frozen else "add.py freeze --by " diff --git a/add-method/src/add_method/_bundled/tooling/add_engine/constants.py b/add-method/src/add_method/_bundled/tooling/add_engine/constants.py index 5b7a8e2a..0da5d5d3 100644 --- a/add-method/src/add_method/_bundled/tooling/add_engine/constants.py +++ b/add-method/src/add_method/_bundled/tooling/add_engine/constants.py @@ -56,7 +56,7 @@ # a sibling of CHANGELOG.md — NOT inside .add/. The ledger IS the attribution source: # a milestone is "released" iff its slug appears on a `milestones:` row. RELEASES_FILE = "RELEASES.md" -PHASES = ("specify", "scenarios", "plan", "tests", "build", "verify", "observe", "done") +PHASES = ("specify", "plan", "tests", "build", "verify", "observe", "done") GATES = ("none", "PASS", "RISK-ACCEPTED", "HARD-STOP") # heal-then-escalate (verify-integrity): the bounded self-heal loop cap. A CONFIRMED cheat # (mechanical tripwire divergence, or an agent-reported semantic refute-read finding) returns @@ -70,10 +70,8 @@ # `add.py guide` copy: per-phase (concrete next action, book chapter to read). # Keep the action wording aligned with each phase's EXIT line in the TASK template. PHASE_GUIDE = { - "specify": ("state every rule — Must / Reject (+ named code) / After, projected from the milestone Ground + the request; rank assumptions lowest-confidence first and flag the biggest risk", + "specify": ("state every rule — Must / Reject (+ named code) / After, projected from the milestone Ground + the request; rank assumptions lowest-confidence first and flag the biggest risk; then write one Given/When/Then per Must AND per Reject (§2) — every result observable", "03-step-1-specify.md"), - "scenarios": ("write one Given/When/Then per Must AND per Reject; every result observable", - "04-step-2-scenarios.md"), "plan": ("build the change plan — ground the real code the contract will cite, freeze the contract shape (names match the glossary), and set the build strategy; this is the one human approval", "05-step-3-plan.md"), "tests": ("write one failing test per scenario; run them RED for the right reason", @@ -92,10 +90,10 @@ # follows the book's who-does-what table (Verify is "human only"); `tests`/`build`/`observe` # are AI-led. A phase missing here is `unmapped_phase` (fail closed) — never defaulted. PHASE_OWNER = { - "specify": "human", "scenarios": "human", "plan": "seam", + "specify": "human", "plan": "seam", "tests": "ai", "build": "ai", "verify": "human", "observe": "ai", "done": "human", } -# phase-bundles: the 7 work phases (PHASES minus the terminal "done") group into 3 +# phase-bundles: the work phases (PHASES minus the terminal "done") group into 3 # agent-owned bundles surfaced at `status`/`guide` — DIRECTION fixes the shape (through # the frozen change plan — grounding + contract + build-strategy — AND the red suite; the # method thesis is "fix spec/scenarios/plan/failing-tests BEFORE the build"), BUILD makes @@ -103,18 +101,18 @@ # reorder; "done" (terminal, human-led) deliberately has no bundle — see PHASE_AGENT/ # _phase_bundle below. Union == set(PHASES) - {"done"}, pairwise disjoint (test_phase_bundles.py). PHASE_GROUPS = { - "DIRECTION": ("specify", "scenarios", "plan", "tests"), + "DIRECTION": ("specify", "plan", "tests"), "BUILD": ("build",), "VERIFY": ("verify", "observe"), } # phase-bundles: the roster agent PREFERRED for each phase (per-PHASE, not per-bundle — # `tests` bundles into DIRECTION above yet its preferred agent is still add-build, the -# shipped roster's own boundary: add-design owns specify/scenarios/plan, add-build owns +# shipped roster's own boundary: add-design owns specify/plan, add-build owns # tests/build, add-verify owns verify/observe). A phase missing here is a bug (PHASE_GROUPS' # own union covers every key); `_phase_bundle` is the fail-closed resolver for an # unmapped/corrupted phase token, not this map directly. PHASE_AGENT = { - "specify": "add-design", "scenarios": "add-design", "plan": "add-design", + "specify": "add-design", "plan": "add-design", "tests": "add-build", "build": "add-build", "verify": "add-verify", "observe": "add-verify", } @@ -339,12 +337,14 @@ _GATE_MODES = ("human", "ai-plan-verify") # --- skippable phases (shared: _task_skip_set reader + cmd_advance's skip pre-pass) — the -# fast-lane-skips closed 2-tuple: the ONLY set cmd_advance's skip pre-pass ever tests `nxt` -# against. specify/plan/tests/build/verify can NEVER be skipped — a structural +# fast-lane-skips closed tuple: the ONLY set cmd_advance's skip pre-pass ever tests `nxt` +# against (scenarios left it at phase-merge-specify: merged into specify, the retired +# header token is tolerated-and-ignored). specify/plan/tests/build/verify can NEVER be +# skipped — a structural # exclusion (this tuple never names them), not a runtime-checked policy. Same relative order # as PHASES. Listed in __all__ (mirrors _GATE_MODES): a new trust-loosening capability is # deliberately surfaced via `from add_engine.constants import *`, not tucked away. --- -_SKIPPABLE_PHASES = ("scenarios", "observe") +_SKIPPABLE_PHASES = ("observe",) # --- format-dialect registry (shared: _dialect_gaps + the tests->build crossing warning + # cmd_check's dialect_gap lint) — quality-floors floor 1. Closed (name, regex) pairs: a diff --git a/add-method/src/add_method/_bundled/tooling/engine_pin.py b/add-method/src/add_method/_bundled/tooling/engine_pin.py index 84c86fde..50db2fb8 100644 --- a/add-method/src/add_method/_bundled/tooling/engine_pin.py +++ b/add-method/src/add_method/_bundled/tooling/engine_pin.py @@ -17,5 +17,5 @@ this file only ever holds the newest pointer. """ -ENGINE_MD5 = "26f78f0480771939d4604d21f62546f6" # re-aimed @ compound-ticks (call-floor 4/5: opt-in freeze --cross lands a plan freeze in tests; a completing gate at build auto-crosses to verify; the new-task recipe advertises the compressed lane). prior: 6f4e791f… @ scope-coverage-hint -ENGINE_PKG_MD5 = "d83fc67fc7979adde37358a29d5e3f46" # re-aimed @ kickoff-truth v2 (io_state.py: _die gains the dup-failure short-circuit — _register_invocation/_clear_last_fail/_dup_fail_hint; the sig sidecar lives in the OS tmp dir keyed by md5(root), NEVER in the .add tree — the reject-writes-nothing floor; constants.py untouched at v2). prior: 28212a55… @ guides-and-skill +ENGINE_MD5 = "b27ce845f42aa832b1338c4881f9cb27" # re-aimed @ phase-merge-specify (six-phase-loop 1/6: scenarios PHASE merged into specify — legacy state token + retired skips: token both tolerated; sections untouched). prior: 26f78f04… @ compound-ticks +ENGINE_PKG_MD5 = "870a4ce0603599ca359302910df01614" # re-aimed @ phase-merge-specify (constants.py: PHASES drops scenarios; _SKIPPABLE_PHASES=(observe,); PHASE_GUIDE/OWNER/GROUPS/AGENT follow). prior: d83fc67f… @ kickoff-truth v2 diff --git a/add-method/src/add_method/_installer.py b/add-method/src/add_method/_installer.py index 1b15fa50..1159ea58 100644 --- a/add-method/src/add_method/_installer.py +++ b/add-method/src/add_method/_installer.py @@ -220,7 +220,7 @@ def _write_gemini_settings(target) -> str: # the engine's phase flow), never invented marketing. Grounding itself is folded into # step 3, Plan, not a separate phase. The wordmark glyphs / tagline / accent are a # SWAPPABLE content slot, not part of the frozen boundary. -_LOOP = ("Specify", "Scenarios", "Plan", "Tests", "Build", "Verify", "Observe") +_LOOP = ("Specify", "Plan", "Tests", "Build", "Verify", "Observe") def _terminal_caps(env): diff --git a/add-method/tooling/add.py b/add-method/tooling/add.py index 4c5793c4..baf21a47 100644 --- a/add-method/tooling/add.py +++ b/add-method/tooling/add.py @@ -123,9 +123,9 @@ def _phase_index(name: str) -> int: # --- state load/save (KEPT in add.py: write-path pinned by add._atomic_write tests) - def _normalize_phase_tokens(state: dict) -> dict: - """Rewrite the two legacy phase tokens on task records (expectations-first): `ground` - -> `specify`, `contract` -> `plan`. TOTAL · idempotent · never raises. ONLY the two - legacy tokens are touched — every other phase (including an already-migrated specify/plan + """Rewrite the legacy phase tokens on task records: `ground` -> `specify`, `contract` + -> `plan` (expectations-first) · `scenarios` -> `specify` (phase-merge-specify). TOTAL · + idempotent · never raises. ONLY the legacy tokens are touched — every other phase (including an already-migrated specify/plan and unrelated tasks) is left byte-identical, so a second pass changes nothing. Normalizes on READ only; state is persisted (migrated) solely when a command legitimately saves — never an auto-write (the over-eager persist was the prior attempt's corruption).""" @@ -134,7 +134,7 @@ def _normalize_phase_tokens(state: dict) -> dict: tasks = state.get("tasks") if not isinstance(tasks, dict): return state - _legacy = {"ground": "specify", "contract": "plan"} + _legacy = {"ground": "specify", "contract": "plan", "scenarios": "specify"} for rec in tasks.values(): if isinstance(rec, dict) and rec.get("phase") in _legacy: rec["phase"] = _legacy[rec["phase"]] @@ -1594,9 +1594,8 @@ def cmd_advance(args: argparse.Namespace) -> None: if PHASES.index(_to) <= idx: _die(f"advance_to_not_forward: task '{slug}' is already at {cur}") nxt = PHASES[idx + 1] - # fast-lane-skips: the ONLY new code the other 6 crossings ever reach is this membership - # test itself — a False result is a no-op, zero further bytes executed. scenarios/observe - # may be jumped as a SINGLE crossing (never entered) when the task declares them, is + # fast-lane-skips: observe + # may be jumped as a SINGLE crossing (never entered) when the task declares it, is # lane-eligible (fast:true | oneshot:true | project benchmark_mode:true), and states a # reason BEFORE the jump — no silent skip. Every _die below fires BEFORE the phase write # further down (validate-then-write, matching every other guard in this function). @@ -1604,9 +1603,15 @@ def cmd_advance(args: argparse.Namespace) -> None: hdr = _task_header(root, slug) tokens, skip_err = _task_skip_set(hdr) if skip_err: - _die(skip_err) # "skip_not_allowed" — fires the first time either - # skippable phase is reached, regardless of which token - # in the malformed declaration is actually bad + _die(skip_err) # "skip_not_allowed" — fires the first time a skippable + # phase is reached (the observe crossing), BEFORE any + # skip could be honored — fail-closed placement unchanged + # phase-merge-specify: a pre-merge declaration naming the retired scenarios token is + # tolerated-and-ignored, noted loud at the same crossing the machinery already reads it. + _m_sk = _SKIPS_LINE_RE.search(hdr) + if _m_sk and any(t.strip() in _RETIRED_SKIP_TOKENS for t in _m_sk.group(1).split(",")): + print("note: `skips: scenarios` — scenarios merged into specify " + "(phase-merge-specify); the declaration is ignored") if nxt in tokens: eligible = _skip_lane_eligible( state["tasks"][slug].get("fast") is True, @@ -1624,8 +1629,8 @@ def cmd_advance(args: argparse.Namespace) -> None: state["tasks"][slug].setdefault("skips", []).append({ "phase": nxt, "reason": reason, "by": identity._actor_stamp(state)["name"], "at": _now()}) - nxt = PHASES[idx + 2] # hop the skipped phase; always in-bounds (neither - # "scenarios" nor "observe" is PHASES[-1]) + nxt = PHASES[idx + 2] # hop the skipped phase; always in-bounds ("observe" + # is never PHASES[-1]) # else: nxt not in tokens -> fall through unchanged, phase entered normally (no-op) # build-boundary gate: pre-lock the front (specify..tests) is allowed, but crossing # into build/verify/observe/done is refused until `add.py lock`. @@ -1786,6 +1791,9 @@ def _task_gate_mode(hdr: str) -> str | None: # fast-lane-skips: the AI-declared skip-set — same anchored declaration grammar as # gate_mode:/sensitivity:/autonomy: (line-start or `·`, value stops at whitespace/`<`/`#`/`|`). _SKIPS_LINE_RE = re.compile(r"(?:^|·)[ \t]*skips:[ \t]*([^\s<#|]+)", re.MULTILINE) +# phase-merge-specify: header tokens for phases that were MERGED away — a pre-merge +# declaration naming one is ignored with a note, never a die (old boards keep working). +_RETIRED_SKIP_TOKENS = frozenset({"scenarios"}) def _task_skip_set(hdr: str) -> tuple[frozenset[str], str | None]: @@ -1797,11 +1805,16 @@ def _task_skip_set(hdr: str) -> tuple[frozenset[str], str | None]: comma) -> (frozenset(), "skip_not_allowed") — the WHOLE declaration is discarded on any single bad element, never partially honored (mirrors _ai_freeze_allowed's "?" fail-closed philosophy, not _task_sensitivity's "None means absent, default safely" philosophy — a - malformed CSV element is garbled, not absent). PURE.""" + malformed CSV element is garbled, not absent). A RETIRED token (`scenarios`, merged into + specify at phase-merge-specify) is filtered out, never bad — the pre-merge declaration is + tolerated-and-ignored (cmd_advance prints the advisory note; this stays PURE). PURE.""" m = _SKIPS_LINE_RE.search(hdr) if not m: return frozenset(), None toks = [t.strip() for t in m.group(1).split(",")] + toks = [t for t in toks if t not in _RETIRED_SKIP_TOKENS] + if not toks: + return frozenset(), None bad = [t for t in toks if t not in _SKIPPABLE_PHASES] if bad: # skip-error-ergonomics M1: the error carries its own repair — the raw @@ -1823,7 +1836,7 @@ def _task_skip_set(hdr: str) -> tuple[frozenset[str], str | None]: # than _flag_well_formed's [part]-tag grammar (no "none material" escape hatch): an irreversible # phase jump always needs a stated reason, or it never happens. _SKIP_RATIONALE_LINE_RE = re.compile(r"^[ \t]*Skip rationale:[ \t]*(.*)$", re.MULTILINE) -_SKIP_RATIONALE_CLAUSE_RE = re.compile(r"^\s*(scenarios|observe)\s*[-—:]\s*(.+)$") +_SKIP_RATIONALE_CLAUSE_RE = re.compile(r"^\s*(observe)\s*[-—:]\s*(.+)$") def _skip_rationale(raw0: str, phase: str) -> str | None: @@ -2021,8 +2034,7 @@ def _ai_verify_checklist_complete(raw3: str) -> bool: # one human decision. Advisory ONLY — the engine never spawns; the line just NAMES the agent # shape a parallel run would use at this step. _SPAWN_HINTS = { - "specify": "domain researcher", - "scenarios": "wide scenario sweep", + "specify": "domain researcher + wide scenario sweep", "plan": "change-plan design sweep", "tests": "red-suite test-author", "build": "independent well-scoped batch", @@ -3056,7 +3068,7 @@ def cmd_status(args: argparse.Namespace) -> None: # routed there through the CLI alone. Never a dead pointer: the path is printed # only if the file exists; a missing tree gets an install hint instead. _PHASE_GUIDE_FILES = { - "specify": "1-specify.md", "scenarios": "2-scenarios.md", + "specify": "1-specify.md", "plan": "3-plan.md", "tests": "4-tests.md", "build": "5-build.md", "verify": "6-verify.md", "observe": "7-observe.md", } @@ -5943,6 +5955,13 @@ def _clean_phase_body(body: str) -> str: return "\n".join(lines) if meaningful else "(empty)" +# phase-merge-specify: the TASK.md §-sections each work phase OWNS. Sections never +# renumber (§3 is the frozen contract everywhere); the phase list shrank instead, so +# specify owns two sections. Explicit — never derive a section from a phase index. +_PHASE_SECTIONS = {"specify": (1, 2), "plan": (3,), "tests": (4,), + "build": (5,), "verify": (6,), "observe": (7,)} + + def task_phases(root: Path, slug: str) -> list[dict]: """The frozen per-task PHASE-DETAIL shape (v9-1): parse the task section blocks into the non-terminal phases specify→observe. PURE — NO writes. Each entry is @@ -5958,12 +5977,16 @@ def task_phases(root: Path, slug: str) -> list[dict]: except OSError: # missing OR unreadable -> every phase fail-closed to "(empty)" return [{"phase": names[n], "n": n, "body": "(empty)"} for n in range(len(names))] spans = _phase_spans(text) - # spans is keyed by the TASK.md SECTION number (§1 SPECIFY .. §7 OBSERVE); the phase - # index n (0..len-1) maps to section n+1 now that §0 GROUND is gone (specify=§1). The - # old model had a §0, so n and the section number were aligned — they diverge by one now. - return [{"phase": names[n], "n": n, - "body": _clean_phase_body(spans[n + 1]) if (n + 1) in spans else "(empty)"} - for n in range(len(names))] + # spans is keyed by the TASK.md SECTION number (§1 SPECIFY .. §7 OBSERVE). The §-sections + # are the stable API and KEEP their numbers; the phase list no longer aligns 1:1 with + # them (phase-merge-specify: the specify phase owns §1 AND §2) — so the mapping is an + # explicit table, never derived from the phase index (the off-by-one bug class). + out = [] + for n, name in enumerate(names): + parts = [_clean_phase_body(spans[s]) for s in _PHASE_SECTIONS[name] if s in spans] + parts = [b for b in parts if b != "(empty)"] + out.append({"phase": name, "n": n, "body": "\n\n".join(parts) if parts else "(empty)"}) + return out def _task_title(root: Path, slug: str) -> str: @@ -6178,7 +6201,7 @@ def render_report(root: Path, state: dict, mslug: str, *, # never interprets, scores, or filters — add.py stays judgment-free, the human # signature is the gate. _MARKER_PREFIXES = (("⚠", "⚠"), ("- [~]", "[~]"), ("- [ ]", "[ ]")) -_FRONT_PHASES = ("specify", "scenarios", "plan", "tests") +_FRONT_PHASES = ("specify", "plan", "tests") def _decision_markers(body: str, section: int) -> list[dict]: @@ -6513,7 +6536,7 @@ def _next_command(phase: str, *, contract_frozen: bool = False) -> str: tells the very agent that just ran it to run it again.""" if phase == "verify": return "add.py gate PASS | RISK-ACCEPTED | HARD-STOP" - if phase in ("specify", "scenarios"): + if phase == "specify": return "add.py advance --to plan (or step-by-step: add.py advance --fill )" if phase == "plan": return "add.py advance" if contract_frozen else "add.py freeze --by " diff --git a/add-method/tooling/add_engine/constants.py b/add-method/tooling/add_engine/constants.py index 5b7a8e2a..0da5d5d3 100644 --- a/add-method/tooling/add_engine/constants.py +++ b/add-method/tooling/add_engine/constants.py @@ -56,7 +56,7 @@ # a sibling of CHANGELOG.md — NOT inside .add/. The ledger IS the attribution source: # a milestone is "released" iff its slug appears on a `milestones:` row. RELEASES_FILE = "RELEASES.md" -PHASES = ("specify", "scenarios", "plan", "tests", "build", "verify", "observe", "done") +PHASES = ("specify", "plan", "tests", "build", "verify", "observe", "done") GATES = ("none", "PASS", "RISK-ACCEPTED", "HARD-STOP") # heal-then-escalate (verify-integrity): the bounded self-heal loop cap. A CONFIRMED cheat # (mechanical tripwire divergence, or an agent-reported semantic refute-read finding) returns @@ -70,10 +70,8 @@ # `add.py guide` copy: per-phase (concrete next action, book chapter to read). # Keep the action wording aligned with each phase's EXIT line in the TASK template. PHASE_GUIDE = { - "specify": ("state every rule — Must / Reject (+ named code) / After, projected from the milestone Ground + the request; rank assumptions lowest-confidence first and flag the biggest risk", + "specify": ("state every rule — Must / Reject (+ named code) / After, projected from the milestone Ground + the request; rank assumptions lowest-confidence first and flag the biggest risk; then write one Given/When/Then per Must AND per Reject (§2) — every result observable", "03-step-1-specify.md"), - "scenarios": ("write one Given/When/Then per Must AND per Reject; every result observable", - "04-step-2-scenarios.md"), "plan": ("build the change plan — ground the real code the contract will cite, freeze the contract shape (names match the glossary), and set the build strategy; this is the one human approval", "05-step-3-plan.md"), "tests": ("write one failing test per scenario; run them RED for the right reason", @@ -92,10 +90,10 @@ # follows the book's who-does-what table (Verify is "human only"); `tests`/`build`/`observe` # are AI-led. A phase missing here is `unmapped_phase` (fail closed) — never defaulted. PHASE_OWNER = { - "specify": "human", "scenarios": "human", "plan": "seam", + "specify": "human", "plan": "seam", "tests": "ai", "build": "ai", "verify": "human", "observe": "ai", "done": "human", } -# phase-bundles: the 7 work phases (PHASES minus the terminal "done") group into 3 +# phase-bundles: the work phases (PHASES minus the terminal "done") group into 3 # agent-owned bundles surfaced at `status`/`guide` — DIRECTION fixes the shape (through # the frozen change plan — grounding + contract + build-strategy — AND the red suite; the # method thesis is "fix spec/scenarios/plan/failing-tests BEFORE the build"), BUILD makes @@ -103,18 +101,18 @@ # reorder; "done" (terminal, human-led) deliberately has no bundle — see PHASE_AGENT/ # _phase_bundle below. Union == set(PHASES) - {"done"}, pairwise disjoint (test_phase_bundles.py). PHASE_GROUPS = { - "DIRECTION": ("specify", "scenarios", "plan", "tests"), + "DIRECTION": ("specify", "plan", "tests"), "BUILD": ("build",), "VERIFY": ("verify", "observe"), } # phase-bundles: the roster agent PREFERRED for each phase (per-PHASE, not per-bundle — # `tests` bundles into DIRECTION above yet its preferred agent is still add-build, the -# shipped roster's own boundary: add-design owns specify/scenarios/plan, add-build owns +# shipped roster's own boundary: add-design owns specify/plan, add-build owns # tests/build, add-verify owns verify/observe). A phase missing here is a bug (PHASE_GROUPS' # own union covers every key); `_phase_bundle` is the fail-closed resolver for an # unmapped/corrupted phase token, not this map directly. PHASE_AGENT = { - "specify": "add-design", "scenarios": "add-design", "plan": "add-design", + "specify": "add-design", "plan": "add-design", "tests": "add-build", "build": "add-build", "verify": "add-verify", "observe": "add-verify", } @@ -339,12 +337,14 @@ _GATE_MODES = ("human", "ai-plan-verify") # --- skippable phases (shared: _task_skip_set reader + cmd_advance's skip pre-pass) — the -# fast-lane-skips closed 2-tuple: the ONLY set cmd_advance's skip pre-pass ever tests `nxt` -# against. specify/plan/tests/build/verify can NEVER be skipped — a structural +# fast-lane-skips closed tuple: the ONLY set cmd_advance's skip pre-pass ever tests `nxt` +# against (scenarios left it at phase-merge-specify: merged into specify, the retired +# header token is tolerated-and-ignored). specify/plan/tests/build/verify can NEVER be +# skipped — a structural # exclusion (this tuple never names them), not a runtime-checked policy. Same relative order # as PHASES. Listed in __all__ (mirrors _GATE_MODES): a new trust-loosening capability is # deliberately surfaced via `from add_engine.constants import *`, not tucked away. --- -_SKIPPABLE_PHASES = ("scenarios", "observe") +_SKIPPABLE_PHASES = ("observe",) # --- format-dialect registry (shared: _dialect_gaps + the tests->build crossing warning + # cmd_check's dialect_gap lint) — quality-floors floor 1. Closed (name, regex) pairs: a diff --git a/add-method/tooling/engine_pin.py b/add-method/tooling/engine_pin.py index 84c86fde..50db2fb8 100644 --- a/add-method/tooling/engine_pin.py +++ b/add-method/tooling/engine_pin.py @@ -17,5 +17,5 @@ this file only ever holds the newest pointer. """ -ENGINE_MD5 = "26f78f0480771939d4604d21f62546f6" # re-aimed @ compound-ticks (call-floor 4/5: opt-in freeze --cross lands a plan freeze in tests; a completing gate at build auto-crosses to verify; the new-task recipe advertises the compressed lane). prior: 6f4e791f… @ scope-coverage-hint -ENGINE_PKG_MD5 = "d83fc67fc7979adde37358a29d5e3f46" # re-aimed @ kickoff-truth v2 (io_state.py: _die gains the dup-failure short-circuit — _register_invocation/_clear_last_fail/_dup_fail_hint; the sig sidecar lives in the OS tmp dir keyed by md5(root), NEVER in the .add tree — the reject-writes-nothing floor; constants.py untouched at v2). prior: 28212a55… @ guides-and-skill +ENGINE_MD5 = "b27ce845f42aa832b1338c4881f9cb27" # re-aimed @ phase-merge-specify (six-phase-loop 1/6: scenarios PHASE merged into specify — legacy state token + retired skips: token both tolerated; sections untouched). prior: 26f78f04… @ compound-ticks +ENGINE_PKG_MD5 = "870a4ce0603599ca359302910df01614" # re-aimed @ phase-merge-specify (constants.py: PHASES drops scenarios; _SKIPPABLE_PHASES=(observe,); PHASE_GUIDE/OWNER/GROUPS/AGENT follow). prior: d83fc67f… @ kickoff-truth v2 diff --git a/add-method/tooling/test_add.py b/add-method/tooling/test_add.py index c8c1e4c6..faaba88b 100644 --- a/add-method/tooling/test_add.py +++ b/add-method/tooling/test_add.py @@ -132,12 +132,12 @@ def test_new_task_requires_init(self): def test_advance_moves_phase_and_syncs_marker(self): self._run("init") self._run("new-task", "t") # phase=specify (plan-phase-core seed) - self._run("advance") # specify -> scenarios + self._run("advance") # specify -> plan (phase-merge-specify: scenarios folded in) st = self._state() - self.assertEqual(st["tasks"]["t"]["phase"], "scenarios") + self.assertEqual(st["tasks"]["t"]["phase"], "plan") marker = [l for l in (Path(self.tmp) / ".add" / "tasks" / "t" / "TASK.md" ).read_text().splitlines() if l.startswith("phase:")][0] - self.assertIn("scenarios", marker) + self.assertIn("plan", marker) def test_phase_explicit_set(self): self._run("init") diff --git a/add-method/tooling/test_agent_portability.py b/add-method/tooling/test_agent_portability.py index 33bc0361..bbf607c7 100644 --- a/add-method/tooling/test_agent_portability.py +++ b/add-method/tooling/test_agent_portability.py @@ -21,7 +21,7 @@ import add PHASE_FILES = { - "specify": "1-specify.md", "scenarios": "2-scenarios.md", + "specify": "1-specify.md", "plan": "3-plan.md", "tests": "4-tests.md", # plan-phase-core: "contract" -> "plan" "build": "5-build.md", "verify": "6-verify.md", "observe": "7-observe.md", } diff --git a/add-method/tooling/test_build_expectations_gate.py b/add-method/tooling/test_build_expectations_gate.py index 6d25f595..fed2b86a 100644 --- a/add-method/tooling/test_build_expectations_gate.py +++ b/add-method/tooling/test_build_expectations_gate.py @@ -83,8 +83,7 @@ def _fill_build_expectations(self, slug="t"): p.write_text(t, encoding="utf-8") def _to_plan(self, slug="t"): - for _ in range(2): # specify -> scenarios -> plan - self._quiet(["advance", slug]) + self._quiet(["advance", slug]) # specify -> plan def _optedin_task_at_tests(self, slug="t", ms="mvp"): self._quiet(["new-milestone", ms, "--goal", "g", "--stage", "mvp", "--await-confirm"]) diff --git a/add-method/tooling/test_bundle_advance.py b/add-method/tooling/test_bundle_advance.py index ab0a88da..d5c95be3 100644 --- a/add-method/tooling/test_bundle_advance.py +++ b/add-method/tooling/test_bundle_advance.py @@ -85,7 +85,7 @@ def _to_build(self, slug): (tests_dir / "test_red.py").write_text("def test_x():\n assert False\n", encoding="utf-8") self._freeze(slug) - for _ in range(4): # specify→scenarios→plan→tests→build + for _ in range(3): # specify→plan→tests→build self._silent("advance", slug) self.assertEqual(self._phase(slug), "build") @@ -125,7 +125,7 @@ def test_to_invalid_refused(self): # scenario 4 def test_plain_advance_unchanged(self): # scenario 5 (additive) self._silent("new-task", "t", "--title", "F") self._silent("advance", "t") - self.assertEqual(self._phase("t"), "scenarios") + self.assertEqual(self._phase("t"), "plan") class RecrossTest(_Harness): diff --git a/add-method/tooling/test_cross_component_contract.py b/add-method/tooling/test_cross_component_contract.py index 9205933a..1d1d2abf 100644 --- a/add-method/tooling/test_cross_component_contract.py +++ b/add-method/tooling/test_cross_component_contract.py @@ -84,8 +84,7 @@ def _new_at_contract(self, slug, role_line, *, frozen="v1", fence="ENDPOINT shap t = re.sub(r"(## 3 · PLAN.*?)```.*?```", rf"\1```\n{fence}\n```", t, count=1, flags=re.DOTALL) assert f"\n{fence}\n" in t, "fence did not land in §3" p.write_text(t, encoding="utf-8") - for _ in range(2): # specify -> scenarios -> plan - self._quiet(["advance", slug]) + self._quiet(["advance", slug]) # specify -> plan def _freeze(self, slug: str) -> None: """Stamp §3 FROZEN + a well-formed flag so the universal freeze gate passes at @@ -280,8 +279,7 @@ def test_zero_contract_no_role_byte_identical(self): # the universal freeze gate sits at this crossing regardless of the contract system, # unrelated to the cross-component behavior under test here) self._quiet(["new-task", "t"]) - for _ in range(2): # specify -> scenarios -> plan - self._quiet(["advance", "t"]) + self._quiet(["advance", "t"]) # specify -> plan self._freeze("t") out, err = self._advance("t") self.assertIsNone(err) @@ -369,7 +367,7 @@ def test_plain_task_unaffected(self): # a task with no consumes: carries no contract_pin -> the guard returns early (byte-identical) self._quiet(["new-task", "t"]) self._freeze("t") # freeze-gate-universal sweep - for _ in range(5): # specify -> ... -> verify (5 hops) + for _ in range(4): # specify -> ... -> verify (4 hops) self._quiet(["advance", "t"]) self.assertEqual(self._phase("t"), "verify") out, err = self._gate("PASS", "t") diff --git a/add-method/tooling/test_cross_component_milestone.py b/add-method/tooling/test_cross_component_milestone.py index dc13f367..b0b5f366 100644 --- a/add-method/tooling/test_cross_component_milestone.py +++ b/add-method/tooling/test_cross_component_milestone.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Red/green tests for the intra-milestone cross-component HOLD (component-aware-add, task 4). -A `consumes: ` task is HELD from advancing scenarios->contract while its producer's contract +A `consumes: ` task is HELD from advancing specify->plan while its producer's contract snapshot (`.add/contracts/.json`, written by task 3 on the producer's freeze) does not exist — so a BE producer and an FE consumer ship in ONE milestone, the FE ordered downstream of the frozen endpoint. Undeclared / no-role tasks cross byte-identically. @@ -66,13 +66,13 @@ def _registry(self): def _task_path(self, slug): return self.addp / "tasks" / slug / "TASK.md" - def _at_scenarios(self, slug, role_line=None): + def _at_specify(self, slug, role_line=None): self._quiet(["new-task", slug]) if role_line: p = self._task_path(slug) p.write_text(p.read_text().replace("phase: specify", f"{role_line}\nphase: specify", 1), encoding="utf-8") - self._quiet(["advance", slug]) # specify -> scenarios + # phase-merge-specify: the hold now binds the specify -> plan crossing directly def _advance(self, slug): out, errbuf = io.StringIO(), io.StringIO() @@ -95,15 +95,15 @@ def _write_snapshot(self, cid="gateway-api"): class Hold(_Board): def test_consumer_held_until_producer_freezes(self): - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out, err = self._advance("fe") self.assertIsNotNone(err) self.assertIn("producer_contract_unfrozen", err or "") - self.assertEqual(self._phase("fe"), "scenarios") + self.assertEqual(self._phase("fe"), "specify") def test_consumer_proceeds_once_snapshot_exists(self): self._write_snapshot() - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out, err = self._advance("fe") self.assertIsNone(err, f"should proceed once the producer froze, got {err!r}") self.assertEqual(self._phase("fe"), "plan") @@ -117,23 +117,23 @@ def test_full_stack_slice_one_milestone(self): t = t.replace("Status: DRAFT", "Status: FROZEN @ v1 — approved by T") t = re.sub(r"(## 3 · PLAN.*?)```.*?```", r"\1```\nBE SHAPE\n```", t, count=1, flags=re.DOTALL) bp.write_text(t, encoding="utf-8") - for _ in range(3): # specify -> scenarios -> plan -> tests (writes snapshot) + for _ in range(2): # specify -> plan -> tests (writes snapshot) self._quiet(["advance", "be"]) self.assertTrue((self.addp / "contracts" / "gateway-api.json").exists()) # now the FE can enter §3 - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out, err = self._advance("fe") self.assertIsNone(err) self.assertEqual(self._phase("fe"), "plan") def test_undeclared_contract_does_not_hold(self): - self._at_scenarios("fe", "consumes: nope") + self._at_specify("fe", "consumes: nope") out, err = self._advance("fe") self.assertIsNone(err, "an undeclared contract id must not hold") self.assertEqual(self._phase("fe"), "plan") def test_no_role_byte_identical(self): - self._at_scenarios("plain") + self._at_specify("plain") out, err = self._advance("plain") self.assertIsNone(err) self.assertEqual(self._phase("plain"), "plan") @@ -158,7 +158,7 @@ def _make_producer(self, slug="be", cid="gateway-api", shape="BE SHAPE v1"): t = self._re.sub(r"(## 3 · PLAN.*?)```.*?```", rf"\1```\n{shape}\n```", t, count=1, flags=self._re.DOTALL) bp.write_text(t, encoding="utf-8") - for _ in range(3): # specify -> scenarios -> plan -> tests (writes snapshot) + for _ in range(2): # specify -> plan -> tests (writes snapshot) self._quiet(["advance", slug]) return bp @@ -192,15 +192,15 @@ def _check(self): def test_stale_leftover_blocks_at_advance(self): bp = self._make_producer() self._drift_producer(bp) # live §3 hash now != snapshot hash - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out, err = self._advance("fe") self.assertIsNotNone(err) self.assertIn("producer_contract_stale", err or "") - self.assertEqual(self._phase("fe"), "scenarios") # held; nothing pinned + self.assertEqual(self._phase("fe"), "specify") # held; nothing pinned def test_current_snapshot_admits_at_advance(self): self._make_producer() # snapshot hash == live producer §3 hash - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out, err = self._advance("fe") self.assertIsNone(err, f"a current snapshot must admit, got {err!r}") self.assertEqual(self._phase("fe"), "plan") @@ -209,7 +209,7 @@ def test_no_live_producer_is_existence_only(self): # a hand-rolled leftover snapshot with NO backing producer task -> existence-only (a # cross-milestone / federation consume of an earlier-frozen contract is not blocked). self._write_snapshot() # {id,producer,hash:"h1"} — no `task`, no producer - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out, err = self._advance("fe") self.assertIsNone(err, f"no live producer must be existence-only, got {err!r}") self.assertEqual(self._phase("fe"), "plan") @@ -218,22 +218,22 @@ def test_no_live_producer_is_existence_only(self): def test_phase_contract_runs_stale_hold(self): bp = self._make_producer() self._drift_producer(bp) - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out, err = self._phase_to("fe", "plan") self.assertIsNotNone(err, "phase plan must not bypass the recency hold") self.assertIn("producer_contract_stale", err or "") - self.assertEqual(self._phase("fe"), "scenarios") + self.assertEqual(self._phase("fe"), "specify") def test_phase_contract_enforces_absent_hold(self): # no snapshot at all -> the override still enforces the existence hold - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out, err = self._phase_to("fe", "plan") self.assertIsNotNone(err) self.assertIn("producer_contract_unfrozen", err or "") - self.assertEqual(self._phase("fe"), "scenarios") + self.assertEqual(self._phase("fe"), "specify") def test_phase_contract_no_role_byte_identical(self): - self._at_scenarios("plain") + self._at_specify("plain") out, err = self._phase_to("plain", "plan") self.assertIsNone(err, f"a no-role task must not hold at phase plan, got {err!r}") self.assertEqual(self._phase("plain"), "plan") @@ -242,7 +242,7 @@ def test_phase_contract_no_role_byte_identical(self): def test_check_warns_drifted_consumer_never_red(self): bp = self._make_producer() self._drift_producer(bp) - self._at_scenarios("fe", "consumes: gateway-api") # fe held at scenarios, snapshot stale + self._at_specify("fe", "consumes: gateway-api") # fe held at scenarios, snapshot stale out = self._check() self.assertIn("contract_producer_stale", out) code = 0 @@ -259,7 +259,7 @@ def test_check_warns_hashless_snapshot_never_red(self): (self.addp / "contracts").mkdir(parents=True, exist_ok=True) (self.addp / "contracts" / "gateway-api.json").write_text( json.dumps({"id": "gateway-api", "producer": "gateway"}), encoding="utf-8") # no hash - self._at_scenarios("fe", "consumes: gateway-api") + self._at_specify("fe", "consumes: gateway-api") out = self._check() self.assertIn("contract_snapshot_hashless", out) code = 0 diff --git a/add-method/tooling/test_engine_batch_ops.py b/add-method/tooling/test_engine_batch_ops.py index d86c6402..b49a2804 100644 --- a/add-method/tooling/test_engine_batch_ops.py +++ b/add-method/tooling/test_engine_batch_ops.py @@ -54,7 +54,7 @@ def test_fill_writes_section_and_advances(self): # M1 self.assertEqual(r.returncode, 0, r.stderr + r.stdout) body = self.task_md.read_text() self.assertIn("Feature: widget rules", body) - self.assertIn("phase: scenarios", body) + self.assertIn("phase: plan", body) def test_fill_from_stdin(self): # M1 (stdin form) r = _run(self.root, "advance", "--fill", "-", stdin_text="Feature: stdin rules\n") @@ -65,9 +65,8 @@ def test_guard_refusal_rolls_back_byte_identical(self): # M2 # walk to plan with plain advances (sections untouched), then attempt the # plan->tests crossing with --fill on an unfrozen §3 — plan-phase-core moved # the freeze gate from tests->build to THIS crossing (one step earlier). - for _ in range(2): - r = _run(self.root, "advance") # specify -> scenarios -> plan - self.assertEqual(r.returncode, 0, r.stderr + r.stdout) + r = _run(self.root, "advance") # specify -> plan (merged flow) + self.assertEqual(r.returncode, 0, r.stderr + r.stdout) before = self.task_md.read_bytes() draft = self.root / "d4.md" draft.write_text("Plan: test_widget — red first\n") diff --git a/add-method/tooling/test_fast_lane_skips.py b/add-method/tooling/test_fast_lane_skips.py index 93f809d0..1a77111e 100644 --- a/add-method/tooling/test_fast_lane_skips.py +++ b/add-method/tooling/test_fast_lane_skips.py @@ -181,7 +181,8 @@ def _new_fast_task(self, slug="t", fast=True, oneshot=False, benchmark=False): class SkippablePhasesConstantTest(unittest.TestCase): def test_value_and_all(self): - self.assertEqual(engine_constants._SKIPPABLE_PHASES, ("scenarios", "observe")) + self.assertEqual(engine_constants._SKIPPABLE_PHASES, ("observe",)) + # phase-merge-specify: scenarios left the tuple (merged into specify) self.assertIn("_SKIPPABLE_PHASES", engine_constants.__all__) def test_importable_via_star_import(self): @@ -198,13 +199,18 @@ def test_absent_line_returns_empty_set_no_error(self): self.assertEqual(add._task_skip_set("slug: t · created: x\nautonomy: auto\n"), (frozenset(), None)) - def test_valid_two_token_csv(self): + def test_retired_token_filtered_from_csv(self): + # phase-merge-specify: a pre-merge two-token declaration keeps its live half self.assertEqual(add._task_skip_set("skips: scenarios,observe\n"), - (frozenset({"scenarios", "observe"}), None)) + (frozenset({"observe"}), None)) def test_valid_single_token(self): + self.assertEqual(add._task_skip_set("skips: observe\n"), + (frozenset({"observe"}), None)) + + def test_retired_only_declaration_resolves_empty(self): self.assertEqual(add._task_skip_set("skips: scenarios\n"), - (frozenset({"scenarios"}), None)) + (frozenset(), None)) def test_one_bad_token_refuses_whole_declaration(self): # skip-error-ergonomics: the error string now carries its own repair — @@ -264,10 +270,10 @@ class SkipRationaleTest(unittest.TestCase): "observe — no rollout to watch\nGround SHA: abc\n") def test_extracts_each_matching_clause(self): - self.assertEqual(add._skip_rationale(self.RAW0, "scenarios"), - "the Accept line covers it") self.assertEqual(add._skip_rationale(self.RAW0, "observe"), "no rollout to watch") + # phase-merge-specify: a retired scenarios clause no longer parses as a rationale + self.assertIsNone(add._skip_rationale(self.RAW0, "scenarios")) def test_no_line_at_all_returns_none(self): self.assertIsNone(add._skip_rationale("Touches: x\nGround SHA: abc\n", "observe")) @@ -277,8 +283,8 @@ def test_line_present_but_no_clause_for_phase_returns_none(self): self.assertIsNone(add._skip_rationale(raw0, "observe")) def test_empty_reason_text_returns_none(self): - raw0 = "Skip rationale: scenarios — \n" - self.assertIsNone(add._skip_rationale(raw0, "scenarios")) + raw0 = "Skip rationale: observe — \n" + self.assertIsNone(add._skip_rationale(raw0, "observe")) # --------------------------------------------------------------------------- @@ -286,20 +292,21 @@ def test_empty_reason_text_returns_none(self): # --------------------------------------------------------------------------- class CmdAdvanceSkipMechanicTest(_Harness): - def test_jumps_specify_to_plan_when_scenarios_declared_eligible_reasoned(self): + def test_retired_scenarios_declaration_ignored_with_note(self): + # phase-merge-specify: specify -> plan is the NATURAL crossing now; a pre-merge + # scenarios declaration is tolerated-and-ignored (noted at the observe crossing), + # and no skip is ever recorded for the retired phase. self._new_fast_task("t", fast=True) self._silent("phase", "specify", "t") self._set_header("t", skips="scenarios") - self._set_skip_rationale("t", "scenarios — collapsed into Accept") out = self._silent("advance", "t") self.assertEqual(self._state()["tasks"]["t"]["phase"], "plan") - self.assertIn("phase specify -> plan", out) - skips = self._state()["tasks"]["t"]["skips"] - self.assertEqual(len(skips), 1) - self.assertEqual(skips[0]["phase"], "scenarios") - self.assertEqual(skips[0]["reason"], "collapsed into Accept") - self.assertIn("by", skips[0]) - self.assertIn("at", skips[0]) + self.assertNotIn("skips", self._state()["tasks"]["t"]) + self._silent("phase", "verify", "t") + out = self._silent("advance", "t") # the observe crossing + self.assertIn("merged into specify", out) + self.assertEqual(self._state()["tasks"]["t"]["phase"], "observe") + self.assertNotIn("skips", self._state()["tasks"]["t"]) def test_jumps_verify_to_done_when_observe_declared_eligible_reasoned(self): self._new_fast_task("t", fast=True) @@ -319,7 +326,7 @@ def test_ordinary_path_when_phase_not_in_declared_skip_set(self): self._set_header("t", skips="observe") self._set_skip_rationale("t", "observe — no rollout to watch") self._silent("advance", "t") - self.assertEqual(self._state()["tasks"]["t"]["phase"], "scenarios") + self.assertEqual(self._state()["tasks"]["t"]["phase"], "plan") self.assertNotIn("skips", self._state()["tasks"]["t"]) @@ -348,14 +355,11 @@ def _set_section3_and_freeze(self, slug): self._silent("freeze", "--by", "Human") def test_five_non_skippable_crossings_never_invoke_task_skip_set(self): - # plan-phase-core: ground+contract collapsed into plan, so the flow is now - # specify -> scenarios -> plan -> tests -> build -> verify -> observe -> done — - # 7 crossings total, 2 "by design" (specify->scenarios, verify->observe) and - # FIVE non-skippable (was six: the old ground->specify leg no longer exists — - # new-task now seeds specify directly). + # phase-merge-specify: scenarios merged into specify, so the flow is now + # specify -> plan -> tests -> build -> verify -> observe -> done — + # 6 crossings total, 1 "by design" (verify->observe) and FIVE non-skippable. self._new_fast_task("t", fast=True) # no skips: declared anywhere - self.assertGreater(self._advance_spy_count(), 0, "specify->scenarios (by design)") - self.assertEqual(self._advance_spy_count(), 0, "scenarios->plan") + self.assertEqual(self._advance_spy_count(), 0, "specify->plan") self._set_section3_and_freeze("t") self.assertEqual(self._advance_spy_count(), 0, "plan->tests") self.assertEqual(self._advance_spy_count(), 0, "tests->build") @@ -427,15 +431,15 @@ def test_malformed_token_is_false(self): class StatusGuideSurfaceTest(_Harness): def test_status_and_guide_show_declared_csv_and_progress(self): self._new_fast_task("t", fast=True) - self._silent("phase", "specify", "t") - self._set_header("t", skips="scenarios,observe") - self._set_skip_rationale("t", "scenarios — collapsed into Accept") - self._silent("advance", "t") # consumes the "scenarios" skip -> 1 recorded + self._silent("phase", "verify", "t") + self._set_header("t", skips="observe") + self._set_skip_rationale("t", "observe — no rollout to watch") + self._silent("advance", "t") # consumes the "observe" skip -> 1 recorded out_status = self._silent("status") out_guide = self._silent("guide") for out in (out_status, out_guide): - self.assertIn("declared scenarios,observe", out) - self.assertIn("skipped so far 1/2", out) + self.assertIn("declared observe", out) + self.assertIn("skipped so far 1/1", out) def test_no_declaration_no_recorded_skip_prints_no_line(self): self._new_fast_task("t", fast=True) @@ -451,7 +455,7 @@ class GateExplainSkipSetTest(_Harness): def test_blocked_outcome_for_ineligible_declared_task(self): self._silent("lock", "--force") self._silent("new-task", "t", "--title", "F") # NOT fast/oneshot - self._set_header("t", skips="scenarios") + self._set_header("t", skips="observe") out = self._silent("gate", "--explain", "t") self.assertIn("skip-set: blocked (skip_lane_required)", out) # the existing gate_mode line (task2, absent here) never crashes / interferes @@ -544,13 +548,13 @@ def test_fast_template_trees_byte_identical(self): class RejectPathsTest(_Harness): def test_malformed_token_refused_as_whole_not_partially_honored(self): self._new_fast_task("t", fast=True) - self._silent("phase", "specify", "t") - self._set_header("t", skips="scenarios,build") + self._silent("phase", "verify", "t") + self._set_header("t", skips="observe,build") before = self._state() out, code = self._run("advance", "t") self.assertNotEqual(code, 0) self.assertIn("skip_not_allowed", out) - self.assertEqual(self._state()["tasks"]["t"]["phase"], "specify") + self.assertEqual(self._state()["tasks"]["t"]["phase"], "verify") self.assertNotIn("skips", self._state()["tasks"]["t"]) self.assertEqual(self._state(), before) @@ -558,27 +562,28 @@ def test_lane_ineligible_declaration_refused(self): self._silent("lock", "--force") self._silent("new-milestone", "m", "--goal", "g", "--stage", "mvp") self._silent("new-task", "t", "--title", "F") # NOT fast/oneshot - self._silent("phase", "specify", "t") - self._set_header("t", skips="scenarios") + self._silent("phase", "verify", "t") + self._set_header("t", skips="observe") out, code = self._run("advance", "t") self.assertNotEqual(code, 0) self.assertIn("skip_lane_required", out) - self.assertEqual(self._state()["tasks"]["t"]["phase"], "specify") + self.assertEqual(self._state()["tasks"]["t"]["phase"], "verify") def test_missing_rationale_refused(self): self._new_fast_task("t", fast=True) - self._silent("phase", "specify", "t") - self._set_header("t", skips="scenarios") + self._silent("phase", "verify", "t") + self._set_header("t", skips="observe") out, code = self._run("advance", "t") self.assertNotEqual(code, 0) self.assertIn("skip_reason_missing", out) - self.assertEqual(self._state()["tasks"]["t"]["phase"], "specify") + self.assertEqual(self._state()["tasks"]["t"]["phase"], "verify") self.assertNotIn("skips", self._state()["tasks"]["t"]) def test_unskippable_phase_name_refused_via_same_code(self): self._new_fast_task("t", fast=True) self._silent("phase", "specify", "t") self._set_header("t", skips="build") + self._silent("phase", "verify", "t") out, code = self._run("advance", "t") self.assertNotEqual(code, 0) self.assertIn("skip_not_allowed", out) @@ -594,16 +599,15 @@ def test_oneshot_security_task_skip_fires_but_freeze_stays_human(self): self._silent("lock", "--force") self._silent("new-milestone", "m", "--goal", "g", "--stage", "mvp") self._silent("new-task", "risky", "--title", "F", "--oneshot") - self._set_header("risky", skips="scenarios,observe") + self._set_header("risky", skips="observe") p = self._task_md("risky") t = p.read_text(encoding="utf-8") t = re.sub(r"(?m)^(autonomy:[^\n]*)$", r"\1\nsensitivity: security", t, count=1) p.write_text(t, encoding="utf-8") - self._set_skip_rationale("risky", "scenarios — a; observe — b") + self._set_skip_rationale("risky", "observe — b") self._silent("phase", "specify", "risky") - self._silent("advance", "risky") # specify -> plan (scenarios skip fires, recorded) + self._silent("advance", "risky") # specify -> plan (the natural crossing now) self.assertEqual(self._state()["tasks"]["risky"]["phase"], "plan") - self.assertEqual(len(self._state()["tasks"]["risky"]["skips"]), 1) # AI freeze attempt is blocked (task2's unchanged floor) body = ("\n```\nGET /x\n 200 -> {ok:true}\n```\n\n" "Least-sure flag surfaced at freeze:\n ⚠ [contract] x — cost: y.\nStatus: DRAFT\n" @@ -623,10 +627,14 @@ def test_oneshot_security_task_skip_fires_but_freeze_stays_human(self): self.assertIn("ai_freeze_blocked_sensitivity", out) # the human path still works, and the earlier skip record is untouched self._silent("freeze", "--by", "A Human") - st = self._state()["tasks"]["risky"] self.assertRegex(self._silent("status", "--section", "3"), r"Status:\s*FROZEN") + # the skip axis composes downstream: verify -> done consumes the observe skip + self._silent("phase", "verify", "risky") + self._silent("advance", "risky") + st = self._state()["tasks"]["risky"] + self.assertEqual(st["phase"], "done") self.assertEqual(len(st["skips"]), 1) - self.assertEqual(st["skips"][0]["phase"], "scenarios") + self.assertEqual(st["skips"][0]["phase"], "observe") # --------------------------------------------------------------------------- @@ -638,10 +646,9 @@ def test_plain_task_visits_scenarios_and_observe_every_crossing(self): self._silent("lock", "--force") self._silent("new-milestone", "m", "--goal", "g", "--stage", "mvp") self._silent("new-task", "t", "--title", "F") # full-lane, no fast/oneshot; seeds specify - self._silent("advance", "t") # specify -> scenarios - self.assertEqual(self._state()["tasks"]["t"]["phase"], "scenarios") + self._silent("advance", "t") # specify -> plan (scenarios merged into specify) + self.assertEqual(self._state()["tasks"]["t"]["phase"], "plan") self.assertNotIn("skips", self._state()["tasks"]["t"]) - self._silent("advance", "t") # scenarios -> plan # freeze the contract to cross plan -> tests p = self._task_md("t") text = p.read_text(encoding="utf-8") diff --git a/add-method/tooling/test_fast_lane_template.py b/add-method/tooling/test_fast_lane_template.py index 1adac145..64cbf5d8 100644 --- a/add-method/tooling/test_fast_lane_template.py +++ b/add-method/tooling/test_fast_lane_template.py @@ -34,8 +34,9 @@ KEPT = {1, 3, 4, 5, 6} # the frozen kept-section set (§0 folded into §3 PLAN) DROPPED = {2, 7} # §2 SCENARIOS, §7 OBSERVE (TASK.md SECTION numbers) -DROPPED_IDX = {1, 6} # the same phases as task_phases()["n"] PHASE INDICES - # (scenarios=1, observe=6; n is the ordinal, not the §-number) +DROPPED_IDX = {5} # observe (n=5); §2 SCENARIOS now folds into specify's + # body (phase-merge-specify) so its absence is not empty + # (plan=1, observe=5; n is the ordinal, not the §-number) def _sections(text: str) -> set[int]: @@ -138,10 +139,12 @@ def test_task_phases_renders_without_error(self): (root / "tasks" / "s").mkdir(parents=True) (root / "tasks" / "s" / "TASK.md").write_text(self.rendered, encoding="utf-8") phases = add.task_phases(root, "s") - self.assertEqual(len(phases), 7) # specify..observe always 7 entries now + self.assertEqual(len(phases), 6) # specify..observe always 6 entries now bynum = {p["n"]: p["body"] for p in phases} # keyed by PHASE INDEX (n), not §-number for n in DROPPED_IDX: self.assertEqual(bynum[n], "(empty)") # dropped phases render empty, no crash + self.assertNotEqual(bynum[0], "(empty)", + "specify renders §1 even with §2 absent (fast template)") class BuildExpectationsWording(unittest.TestCase): diff --git a/add-method/tooling/test_fast_new_task_flag.py b/add-method/tooling/test_fast_new_task_flag.py index 457b19d4..501be6fb 100644 --- a/add-method/tooling/test_fast_new_task_flag.py +++ b/add-method/tooling/test_fast_new_task_flag.py @@ -78,7 +78,7 @@ def _plain_milestone(self, ms="mvp"): self._quiet(["new-milestone", ms, "--goal", "g", "--stage", "mvp"]) # no --await-confirm def _to_plan(self, slug): - for _ in range(2): # specify -> scenarios -> plan + for _ in range(1): # specify -> plan self._quiet(["advance", slug]) def _freeze(self, slug): diff --git a/add-method/tooling/test_freeze_before_build_gate.py b/add-method/tooling/test_freeze_before_build_gate.py index 8fb19d17..4c14acda 100644 --- a/add-method/tooling/test_freeze_before_build_gate.py +++ b/add-method/tooling/test_freeze_before_build_gate.py @@ -92,8 +92,7 @@ def _fill_build_expectations(self, slug="t"): p.write_text(t, encoding="utf-8") def _to_plan(self, slug="t"): - for _ in range(2): # specify -> scenarios -> plan - self._quiet(["advance", slug]) + self._quiet(["advance", slug]) # specify -> plan def _optedin_task_at_plan(self, slug="t", ms="mvp"): self._quiet(["new-milestone", ms, "--goal", "g", "--stage", "mvp", "--await-confirm"]) diff --git a/add-method/tooling/test_freeze_gate_universal.py b/add-method/tooling/test_freeze_gate_universal.py index ac6c088d..ec8b442b 100644 --- a/add-method/tooling/test_freeze_gate_universal.py +++ b/add-method/tooling/test_freeze_gate_universal.py @@ -108,8 +108,7 @@ def _fill_build_expectations(self, slug="t"): p.write_text(t, encoding="utf-8") def _to_plan(self, slug="t"): - for _ in range(2): # specify -> scenarios -> plan - self._quiet(["advance", slug]) + self._quiet(["advance", slug]) # specify -> plan def _plain_task_at_plan(self, slug="t", ms="plain"): self._quiet(["new-milestone", ms, "--goal", "g", "--stage", "mvp"]) # NO --await-confirm diff --git a/add-method/tooling/test_gate_owner_marker.py b/add-method/tooling/test_gate_owner_marker.py index 155f1c32..f4ccde28 100644 --- a/add-method/tooling/test_gate_owner_marker.py +++ b/add-method/tooling/test_gate_owner_marker.py @@ -13,7 +13,7 @@ The phase × autonomy table (only the verify row moves with the dial): tests/build/observe -> [you drive] (always) - specify/scenarios/plan/done -> [human gate] (always; the plan freeze stays human, run.md:21) + specify/plan/done -> [human gate] (always; the plan freeze stays human, run.md:21) verify -> [you drive] under auto, [human gate] under conservative/manual <- exit criterion Option F: the FROZEN machine-state-json JSON (`owner`/`stop`) is NOT touched — the marker rides @@ -40,12 +40,12 @@ # the table the frozen §3 contract encodes — only verify moves with the dial _TABLE = { "auto": { - "specify": " [human gate]", "scenarios": " [human gate]", + "specify": " [human gate]", "plan": " [human gate]", "tests": " [you drive]", "build": " [you drive]", "verify": " [you drive]", "observe": " [you drive]", "done": " [human gate]", }, "conservative": { - "specify": " [human gate]", "scenarios": " [human gate]", + "specify": " [human gate]", "plan": " [human gate]", "tests": " [you drive]", "build": " [you drive]", "verify": " [human gate]", "observe": " [you drive]", "done": " [human gate]", }, diff --git a/add-method/tooling/test_ground_phase.py b/add-method/tooling/test_ground_phase.py index 4bd8eea5..35ac61c6 100644 --- a/add-method/tooling/test_ground_phase.py +++ b/add-method/tooling/test_ground_phase.py @@ -11,10 +11,10 @@ invariants; this file keeps its own CLI-arranged regression net). Behavior pinned, not words: - new-task seeds phase "specify"; grounding now renders inside "## 3 · PLAN"; - - advance specify -> scenarios (index-derived); PHASES len 8, specify at index 0; + - advance specify -> plan (index-derived); PHASES len 7, specify at index 0; - PHASE_OWNER["plan"] == "seam" (the one human approval); - the decision digest gives a fresh task the FRONT seam (never "approve the contract" - prematurely — specify/scenarios/plan/tests all share the front seam now); + prematurely — specify/plan/tests all share the front seam now); - render_decide does not crash on the plan seam; - reopen targets are specify..observe; done is refused; - the phase-detail drill-down renders specify..observe (7 sections) — SEE the @@ -110,7 +110,7 @@ def _freeze(self, slug: str): def _mk_done(self, slug: str): """Drive a fresh task specify -> observe -> done (PASS). SIX advances now - (specify -> scenarios -> plan -> tests -> build -> verify -> observe), then + (specify -> plan -> tests -> build -> verify -> observe), then `gate PASS` finalizes to done. (plan-phase-core: ground+contract collapsed into plan, so the transition count is unchanged from before the rename — verified empirically against the live engine, not assumed.)""" @@ -122,11 +122,11 @@ def _mk_done(self, slug: str): assert self._task(slug)["phase"] == "done", "fixture: task did not reach done" # ---- the ladder shape ------------------------------------------------- - def test_phases_has_specify_first_len_8(self): + def test_phases_has_specify_first_len_7(self): # new truth (plan-phase-core): ground+contract collapsed into plan; PHASES is 8. self.assertEqual(add.PHASES[0], "specify", "specify must be the first phase") self.assertEqual(add.PHASES[-1], "done", "done stays the terminal phase") - self.assertEqual(len(add.PHASES), 8, "ground+contract folded into plan: PHASES is now 8") + self.assertEqual(len(add.PHASES), 7, "scenarios merged into specify: PHASES is now 7") def test_every_phase_is_owned_plan_is_seam(self): # PHASE_OWNER is fail-closed (unmapped_phase) -> every phase MUST be mapped; @@ -160,18 +160,17 @@ def test_first_task_of_project_starts_at_specify(self): self.assertEqual(code, 0, f"first task should create; err={err!r}") self.assertEqual(self._task("first")["phase"], "specify") - # ---- advance specify -> scenarios ---------------------------------------- - def test_advance_specify_to_scenarios(self): + # ---- advance specify -> plan ---------------------------------------- + def test_advance_specify_to_plan(self): self._run("new-task", "feat", "--title", "Feat") self._run("advance", "feat") - self.assertEqual(self._task("feat")["phase"], "scenarios", - "advancing from specify lands at scenarios") + self.assertEqual(self._task("feat")["phase"], "plan", + "advancing from specify lands at plan (merged flow)") # ---- guide at plan (grounding now happens inside plan) ----------------- def test_guide_at_plan_cues_grounding(self): self._run("new-task", "feat", "--title", "Feat") - self._run("advance", "feat") # specify -> scenarios - self._run("advance", "feat") # scenarios -> plan + self._run("advance", "feat") # specify -> plan (merged flow) out, err, code = self._run("guide") low = out.lower() self.assertIn("plan", low, "guide must name the plan phase") @@ -238,8 +237,8 @@ def test_task_phases_render_specify_first(self): names = [p["phase"] for p in phases] self.assertEqual(names[0], "specify", "the drill-down renders specify first") self.assertEqual(names[-1], "observe", "the drill-down ends at observe") - self.assertEqual(len(phases), 7, - "specify..observe is 7 sections now (plan folds ground+contract)") + self.assertEqual(len(phases), 6, + "specify..observe is 6 phase blocks now (specify owns §1+§2)") # ---- heading scan captures section 0 ---------------------------------- def test_phase_spans_captures_section_0(self): diff --git a/add-method/tooling/test_hint_batch_ops.py b/add-method/tooling/test_hint_batch_ops.py index b9827da5..7b53f2ca 100644 --- a/add-method/tooling/test_hint_batch_ops.py +++ b/add-method/tooling/test_hint_batch_ops.py @@ -44,15 +44,15 @@ def _footer(self): def test_walk_phases_fill_then_bare(self): # ONE ordered walk (no cross-test ordering dependency). advance-chain-collapse: - # the front drafting phases {specify,scenarios} teach the COLLAPSED + # the front drafting phase (specify) teaches the COLLAPSED # `advance --to plan` while keeping the per-section `--fill` alt; `plan` # points at the freeze gate (not another advance); `tests` keeps the bare --fill. # expectations-first: the plan->tests crossing is now the freeze gate itself, so # the walk crosses it with the documented --skip-freeze escape (this test only # probes the FOOTER TEXT, not the freeze mechanics). - for expect_phase in ("specify", "scenarios", "plan", "tests"): + for expect_phase in ("specify", "plan", "tests"): out = self._footer() - if expect_phase in ("specify", "scenarios"): + if expect_phase == "specify": self.assertIn("add.py advance --to plan", out, f"{expect_phase} footer must teach the collapse:\n{out}") self.assertIn("add.py advance --fill ", out, diff --git a/add-method/tooling/test_machine_state.py b/add-method/tooling/test_machine_state.py index 11a3b928..4502820c 100644 --- a/add-method/tooling/test_machine_state.py +++ b/add-method/tooling/test_machine_state.py @@ -94,7 +94,7 @@ def test_guide_json_active_task_carries_owner_stop(self): def test_owner_map_stops_at_human_and_seam(self): # plan-phase-core: ground+contract collapsed into `plan` — it stays the seam owner. expect = { - "specify": ("human", True), "scenarios": ("human", True), + "specify": ("human", True), "plan": ("seam", True), "tests": ("ai", False), "build": ("ai", False), "verify": ("human", True), "observe": ("ai", False), "done": ("human", True), diff --git a/add-method/tooling/test_min_pillar.py b/add-method/tooling/test_min_pillar.py index b619d18a..c0f27498 100644 --- a/add-method/tooling/test_min_pillar.py +++ b/add-method/tooling/test_min_pillar.py @@ -79,8 +79,10 @@ def _meet_exit_criteria(ms: str) -> None: ["deactivate", "mvp"], # multi-active SET writer: drop mvp from the active set (pops its task entry; reads/writes state, never docs/) ["use", "t"], # re-focus t -> milestone-aware use re-activates mvp + restores active_task for the rest of the run ["phase", "specify", "t"], - ["advance", "t"], ["advance", "t"], ["advance", "t"], - ["advance", "t"], ["advance", "t"], # specify -> ... -> verify + ["advance", "t"], ["advance", "t"], + ["advance", "t"], ["advance", "t"], # specify -> plan -> tests -> build -> verify + # (phase-merge-specify: scenarios folded into specify, + # so verify is 4 hops from specify, not 5) ["heal", "t", "--reason", "lifecycle: a reported earned-green cheat"], # bounded self-heal: # the semantic entry — returns t to build (exit 3, # tolerated), exercised under the read-spy (reads diff --git a/add-method/tooling/test_next_footer_engine.py b/add-method/tooling/test_next_footer_engine.py index fe2378c4..061cf7e4 100644 --- a/add-method/tooling/test_next_footer_engine.py +++ b/add-method/tooling/test_next_footer_engine.py @@ -173,13 +173,11 @@ class FooterArmATest(_Board): def test_advance_footer_is_phase_command(self): self._silent("new-task", "foo") # active, specify - out, _, _ = self._run("advance", "foo") # specify -> scenarios + out, _, _ = self._run("advance", "foo") # specify -> plan (merged flow) footer = self._footer(out) - self.assertTrue(footer.startswith("next: add.py advance"), - f"a front phase points at advance, got: {footer!r}") - self.assertIn("write one Given/When/Then", footer, - "the why is the scenarios-phase PHASE_GUIDE copy") - self.assertIn("phase specify -> scenarios", out, + self.assertTrue(footer.startswith("next: add.py"), + f"a front phase points at the engine, got: {footer!r}") + self.assertIn("phase specify -> plan", out, "the footer is ADDITIVE — the verb's result line survives") def test_advance_into_verify_footer_is_gate(self): @@ -271,8 +269,7 @@ def test_marker_slot_filled(self): # through the frozen §3 into `tests` (owner ai) under the default `auto` rung # to exercise the AI-drives branch. self._silent("new-task", "foo") - self._silent("advance", "foo") # specify -> scenarios - self._silent("advance", "foo") # scenarios -> plan + self._silent("advance", "foo") # specify -> plan (merged flow) self._freeze("foo") out, _, _ = self._run("advance", "foo") # plan -> tests (frozen) footer = self._footer(out) @@ -303,7 +300,7 @@ def test_every_completing_verb_prints_one_next(self): covered["new-milestone"] = self._run("new-milestone", "m2", "--title", "M2", "--goal", "g")[0] covered["new-task"] = self._run("new-task", "t1")[0] # active t1 (m2), ground covered["advance"] = self._run("advance", "t1")[0] # ground -> specify - covered["phase"] = self._run("phase", "scenarios", "t1")[0] + covered["phase"] = self._run("phase", "plan", "t1")[0] self._silent("new-task", "t2") # active t2 covered["use"] = self._run("use", "t1")[0] # active t1 covered["set-milestone"] = self._run("set-milestone", "t2", "m2")[0] @@ -363,17 +360,17 @@ def test_specify_footer_teaches_collapse(self): # M1 + M2 @ specify self.assertIn("add.py advance --fill ", footer, f"specify footer keeps the --fill alt, got: {footer!r}") - def test_scenarios_footer_teaches_collapse(self): # M1 + M2 @ scenarios + def test_merged_first_advance_lands_at_freeze_hint(self): # M1 @ merged flow + # phase-merge-specify: the first advance lands AT plan — the footer already + # points at the freeze (the collapse hint's home is the specify footer above). self._silent("new-task", "foo") - out, _, _ = self._run("advance", "foo") # specify -> scenarios + out, _, _ = self._run("advance", "foo") # specify -> plan footer = self._footer(out) - self.assertIn("add.py advance --to plan", footer, footer) - self.assertIn("add.py advance --fill ", footer, footer) + self.assertIn("add.py freeze", footer, footer) def test_contract_footer_points_at_freeze(self): # M3 self._silent("new-task", "foo") - self._silent("advance", "foo") # -> scenarios - out, _, _ = self._run("advance", "foo") # -> plan + out, _, _ = self._run("advance", "foo") # -> plan (merged flow) footer = self._footer(out) self.assertIn("add.py freeze", footer, f"plan footer names the freeze gate, got: {footer!r}") @@ -385,8 +382,7 @@ def test_contract_footer_points_at_freeze(self): # M3 def test_no_front_footer_names_a_to_past_contract(self): # M4 self._silent("new-task", "foo") footers = [self._footer(self._run("new-task", "bar")[0])] # bar @ specify - for _ in range(2): # scenarios, plan - footers.append(self._footer(self._run("advance", "bar")[0])) + footers.append(self._footer(self._run("advance", "bar")[0])) # -> plan for f in footers: for bad in ("--to tests", "--to build", "--to verify", "--to observe", "--to done"): self.assertNotIn(bad, f, f"no front footer may name {bad!r}: {f!r}") @@ -422,8 +418,7 @@ def test_empty_milestone_names_flagged_new_task(self): # M3 def test_plain_status_carries_next_command_inline(self): # M2 self._silent("new-task", "foo") # -> specify - self._silent("advance", "foo") # -> scenarios - self._silent("advance", "foo") # -> plan + self._silent("advance", "foo") # -> plan (merged flow) out, _, _ = self._run("status") # plain full status self.assertIn("add.py freeze", out, f"plain status must name the next command inline: {out}") diff --git a/add-method/tooling/test_onboarding_brand.py b/add-method/tooling/test_onboarding_brand.py index 4a7c04b4..5dedca90 100644 --- a/add-method/tooling/test_onboarding_brand.py +++ b/add-method/tooling/test_onboarding_brand.py @@ -26,13 +26,13 @@ INSTALLER_PY = PKG_ROOT / "src" / "add_method" / "_installer.py" -# The 7 ADD phases the showcase loop must name (grounded in the method, never invented; +# The 6 ADD phases the showcase loop must name (grounded in the method, never invented; # grounding itself is folded into Plan, not a separate step). Build/Tests are excluded # from the OUTPUT markers below because the plain installer prose already says "build" — # these five are showcase-only words that never appear in normal installer output, so # they cleanly separate the two paths. -LOOP_STEPS = ("Specify", "Scenarios", "Plan", "Tests", "Build", "Verify", "Observe") -SHOWCASE_MARKERS = ("Specify", "Scenarios", "Plan", "Verify", "Observe") +LOOP_STEPS = ("Specify", "Plan", "Tests", "Build", "Verify", "Observe") +SHOWCASE_MARKERS = ("Specify", "Plan", "Verify", "Observe") def _markers_present(text): @@ -55,10 +55,10 @@ def test_loop_steps_are_the_real_pre_done_phases(self): for s in LOOP_STEPS: self.assertIn(s.lower(), phases, f"showcase step {s!r} must be a real ADD phase (grounded, not invented)") - # exactly the 7 steps before the terminal "done", in order (no separate ground - # phase — grounding is folded into step 3, Plan) - self.assertEqual([s.lower() for s in LOOP_STEPS], phases[0:7], - "the showcase must be the 7 pre-done phases in order") + # exactly the 6 steps before the terminal "done", in order (no separate ground + # or scenarios phase — grounding folded into Plan, scenarios into Specify) + self.assertEqual([s.lower() for s in LOOP_STEPS], phases[0:6], + "the showcase must be the 6 pre-done phases in order") # --- the banner is actually wired into BOTH twins (decision-equivalent) ----------- diff --git a/add-method/tooling/test_per_component_verify.py b/add-method/tooling/test_per_component_verify.py index 7e147647..8e776907 100644 --- a/add-method/tooling/test_per_component_verify.py +++ b/add-method/tooling/test_per_component_verify.py @@ -94,7 +94,7 @@ def _cite_in_six(self, slug, phrase): p.write_text(t, encoding="utf-8") def _to_verify(self, slug): - for _ in range(5): # specify->scenarios->plan->tests->build->verify + for _ in range(4): # specify->plan->tests->build->verify self._quiet(["advance", slug]) def _gate(self, slug, outcome): diff --git a/add-method/tooling/test_phase_build_guard.py b/add-method/tooling/test_phase_build_guard.py index 46baca39..b7eae207 100644 --- a/add-method/tooling/test_phase_build_guard.py +++ b/add-method/tooling/test_phase_build_guard.py @@ -103,8 +103,7 @@ def _fill_build_expectations(self, slug="t"): p.write_text(t, encoding="utf-8") def _to_plan(self, slug="t"): - for _ in range(2): # specify -> scenarios -> plan - self._quiet(["advance", slug]) + self._quiet(["advance", slug]) # specify -> plan def _optedin_task_at_plan(self, slug="t", ms="mvp"): self._quiet(["new-milestone", ms, "--goal", "g", "--stage", "mvp", "--await-confirm"]) @@ -199,9 +198,9 @@ def test_fast_unfrozen_blocks_phase_build(self): def test_non_build_target_never_gated(self): self._optedin_task_at_plan() self._quiet(["phase", "specify", "t"]) - self._quiet(["phase", "scenarios", "t"]) + self._quiet(["phase", "tests", "t"]) t = self._task() - self.assertEqual(t.get("phase"), "scenarios") + self.assertEqual(t.get("phase"), "tests") self.assertNotIn("tripwire", t, "a non-build target takes no tripwire snapshot") def test_advance_into_build_unchanged(self): diff --git a/add-method/tooling/test_phase_bundles.py b/add-method/tooling/test_phase_bundles.py index f70aa2d7..314cbdc4 100644 --- a/add-method/tooling/test_phase_bundles.py +++ b/add-method/tooling/test_phase_bundles.py @@ -79,10 +79,10 @@ def test_exact_keys(self): {"DIRECTION", "BUILD", "VERIFY"}) def test_tuples_match_frozen_shape(self): - # plan-phase-core: ground+contract collapsed into `plan`; DIRECTION now names - # specify/scenarios/plan/tests (4 phases, was 5) — see MIGRATION_CONTEXT M2. + # phase-merge-specify: scenarios merged into specify; DIRECTION now names + # specify/plan/tests (3 phases, was 4). self.assertEqual(engine_constants.PHASE_GROUPS["DIRECTION"], - ("specify", "scenarios", "plan", "tests")) + ("specify", "plan", "tests")) self.assertEqual(engine_constants.PHASE_GROUPS["BUILD"], ("build",)) self.assertEqual(engine_constants.PHASE_GROUPS["VERIFY"], ("verify", "observe")) @@ -114,7 +114,6 @@ def test_values_match_shipped_roster_ownership(self): # phase) is add-design owned, same as specify/scenarios were. pa = engine_constants.PHASE_AGENT self.assertEqual(pa["specify"], "add-design") - self.assertEqual(pa["scenarios"], "add-design") self.assertEqual(pa["plan"], "add-design") self.assertEqual(pa["tests"], "add-build") self.assertEqual(pa["build"], "add-build") @@ -136,7 +135,7 @@ class PhaseBundleResolverTest(unittest.TestCase): """M3 — _phase_bundle: normal resolve / terminal None / fail-closed unmapped.""" def test_resolves_direction_phases(self): - for phase in ("specify", "scenarios", "plan", "tests"): + for phase in ("specify", "plan", "tests"): self.assertEqual(engine_predicates._phase_bundle(phase), "DIRECTION", phase) def test_resolves_build_phase(self): diff --git a/add-method/tooling/test_phase_detail.py b/add-method/tooling/test_phase_detail.py index 98a255c8..aa7e0264 100644 --- a/add-method/tooling/test_phase_detail.py +++ b/add-method/tooling/test_phase_detail.py @@ -20,7 +20,7 @@ TASK.md (1=SPECIFY .. 7=OBSERVE, unchanged by the phase collapse — §3's heading number stayed 3, only its label moved CONTRACT->PLAN). Because `spans` has no key `0`, `specify`'s body always reads "(empty)"; every other phase's body reads its -PREDECESSOR heading's content (`plan` shows `scenarios`'s text, etc.); and the +PREDECESSOR heading's content; and the document's actual §7 OBSERVE content is never read at all (task_phases only ever looks up spans[0..6]). This is a live DATA-correctness bug, not a crash — three tests below catch it honestly (RED): test_task_phases_pure_extraction, test_unfilled_ @@ -120,15 +120,16 @@ def _done_pass(self, slug): add.main(["phase", "verify", slug]) add.main(["gate", "PASS", slug]) - # ---- scenarios -------------------------------------------------------- + # ---- the drill -------------------------------------------------------- def test_drill_renders_seven_phases(self): + # phase-merge-specify: six work phases now (specify owns §1+§2) add.main(["phase", "plan", "alpha"]) before = self._hash_state() out, _, code = self._report("vX", "alpha") self.assertEqual(code, 0) - names = ["0 SPECIFY", "1 SCENARIOS", "2 PLAN", "3 TESTS", - "4 BUILD", "5 VERIFY", "6 OBSERVE"] - # all seven present, in order + names = ["0 SPECIFY", "1 PLAN", "2 TESTS", + "3 BUILD", "4 VERIFY", "5 OBSERVE"] + # all six present, in order positions = [out.find(n) for n in names] self.assertNotIn(-1, positions, "a phase block is missing") self.assertEqual(positions, sorted(positions), "phase blocks out of order") @@ -138,14 +139,17 @@ def test_drill_renders_seven_phases(self): # heading keys, silently shifting every phase's body into its NEXT phase's # block (see the module docstring's KNOWN ENGINE BUG note). bounds = positions + [len(out)] - for i, marker in enumerate(("SPEC_MARKER", "SCEN_MARKER", "PLAN_MARKER")): + # phase-merge-specify: specify owns §1 AND §2 — both markers under its block + expected = {0: ("SPEC_MARKER", "SCEN_MARKER"), 1: ("PLAN_MARKER",)} + for i, markers in expected.items(): seg = out[bounds[i]:bounds[i + 1]] - self.assertIn(marker, seg, f"{marker} must render under its own block " - f"({names[i]}), not shifted into the next one") + for marker in markers: + self.assertIn(marker, seg, f"{marker} must render under its own block " + f"({names[i]}), not shifted into the next one") # ascii tier under a non-tty StringIO: reached '#', current '>', pending '.' - self.assertIn("> 2 PLAN", out) # plan is current + self.assertIn("> 1 PLAN", out) # plan is current self.assertIn("# 0 SPECIFY", out) # specify reached - self.assertIn(". 3 TESTS", out) # tests pending + self.assertIn(". 2 TESTS", out) # tests pending self.assertEqual(self._hash_state(), before) # read-only def test_verify_block_shows_gate_from_state(self): @@ -155,15 +159,15 @@ def test_verify_block_shows_gate_from_state(self): self.assertEqual(code, 0) self.assertIn("GATE PASS", out) # verify block sources gate from state self.assertNotIn("PASS", _TASK_MD) # guard: the fixture prose never says PASS - self.assertIn("# 5 VERIFY", out) # every block reached (done) - self.assertIn("# 6 OBSERVE", out) + self.assertIn("# 4 VERIFY", out) # every block reached (done) + self.assertIn("# 5 OBSERVE", out) def test_unfilled_phase_is_empty(self): out, _, code = self._report("vX", "alpha") self.assertEqual(code, 0) # §5 BUILD body is only a placeholder -> (empty), never a silent gap - build_at = out.find("4 BUILD") - verify_at = out.find("5 VERIFY") + build_at = out.find("3 BUILD") + verify_at = out.find("4 VERIFY") self.assertNotEqual(build_at, -1) self.assertIn("(empty)", out[build_at:verify_at]) @@ -213,8 +217,8 @@ def test_json_dumps_task_phases(self): out, _, code = self._report("vX", "alpha", "--json") self.assertEqual(code, 0) data = _json.loads(out) - self.assertEqual(len(data), 7) - self.assertEqual([d["n"] for d in data], [0, 1, 2, 3, 4, 5, 6]) + self.assertEqual(len(data), 6) + self.assertEqual([d["n"] for d in data], [0, 1, 2, 3, 4, 5]) for d in data: self.assertIn("phase", d) self.assertIn("body", d) @@ -226,20 +230,21 @@ def test_unreadable_file_failclosed(self): from unittest import mock with mock.patch.object(Path, "read_text", side_effect=OSError("boom")): phases = add.task_phases(self._root(), "alpha") - self.assertEqual(len(phases), 7) + self.assertEqual(len(phases), 6) self.assertTrue(all(p["body"] == "(empty)" for p in phases)) def test_task_phases_pure_extraction(self): phases = add.task_phases(self._root(), "alpha") - self.assertEqual(len(phases), 7) + self.assertEqual(len(phases), 6) self.assertEqual([p["phase"] for p in phases], - ["specify", "scenarios", "plan", "tests", + ["specify", "plan", "tests", "build", "verify", "observe"]) bodies = {p["n"]: p["body"] for p in phases} self.assertIn("SPEC_MARKER", bodies[0]) + self.assertIn("SCEN_MARKER", bodies[0]) # specify owns §2 too self.assertNotIn("a comment that must be stripped", bodies[0]) # HTML comment gone - self.assertNotIn("EXIT:", bodies[5]) # EXIT marker gone - self.assertEqual(bodies[4], "(empty)") # placeholder-only + self.assertNotIn("EXIT:", bodies[4]) # EXIT marker gone + self.assertEqual(bodies[3], "(empty)") # placeholder-only if __name__ == "__main__": diff --git a/add-method/tooling/test_phase_merge_specify.py b/add-method/tooling/test_phase_merge_specify.py new file mode 100644 index 00000000..99d14ed2 --- /dev/null +++ b/add-method/tooling/test_phase_merge_specify.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Red/green tests for phase-merge-specify (six-phase-loop 1/6, frozen v1): the +scenarios PHASE folds into specify — one drafting phase produces §1 AND §2; the +TASK.md §-section shape is untouched (sections are the stable API). Legacy state +tokens normalize on read (expectations-first precedent); a pre-merge header skip +declaration naming scenarios is tolerated loud, never a die. + +Run: python3 -m unittest test_phase_merge_specify -v +""" +import io +import json +import os +import re +import shutil +import tempfile +import unittest +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path + +import add +from add_engine.constants import (PHASES, _SKIPPABLE_PHASES, PHASE_GUIDE, + PHASE_OWNER, PHASE_GROUPS, PHASE_AGENT) + + +class _Harness(unittest.TestCase): + def setUp(self): + self._cwd = Path.cwd() + self.tmp = Path(tempfile.mkdtemp(prefix="add-pms-")).resolve() + self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True) + self.addCleanup(os.chdir, self._cwd) + os.chdir(self.tmp) + + def _run(self, *argv): + out, err = io.StringIO(), io.StringIO() + code = 0 + try: + with redirect_stdout(out), redirect_stderr(err): + add.main(list(argv)) + except SystemExit as e: + code = e.code if isinstance(e.code, int) else 1 + return out.getvalue() + err.getvalue(), code + + def _ok(self, *argv): + text, code = self._run(*argv) + self.assertEqual(code, 0, f"{argv} exited {code}: {text}") + return text + + def _board(self): + self._ok("init", "--name", "demo", "--stage", "mvp") + self._ok("lock", "--force") + self._ok("new-task", "t", "--title", "T", "--oneshot") + + def _phase(self, slug="t"): + state = json.loads((self.tmp / ".add" / "state.json").read_text(encoding="utf-8")) + return state["tasks"][slug]["phase"] + + +class PhaseListTest(_Harness): + def test_phases_has_no_scenarios(self): # M1 + self.assertNotIn("scenarios", PHASES) + self.assertEqual(PHASES, ("specify", "plan", "tests", "build", "verify", + "observe", "done")) + + def test_maps_carry_no_scenarios_key(self): # M1 + for name, mapping in (("PHASE_GUIDE", PHASE_GUIDE), ("PHASE_OWNER", PHASE_OWNER), + ("PHASE_AGENT", PHASE_AGENT)): + self.assertNotIn("scenarios", mapping, f"{name} still maps the retired phase") + self.assertEqual(PHASE_GROUPS["DIRECTION"], ("specify", "plan", "tests")) + self.assertEqual(_SKIPPABLE_PHASES, ("observe",)) + + def test_specify_guide_action_names_gwt(self): # M1 + self.assertIn("Given/When/Then", PHASE_GUIDE["specify"][0], + "the merged specify action must carry scenarios' Given/When/Then duty") + + +class AdvanceTest(_Harness): + def test_advance_specify_lands_plan(self): # M1 + Accept + self._board() + self._ok("advance") + self.assertEqual(self._phase(), "plan", + "a bare advance from specify must land at plan (no scenarios stop)") + + def test_new_phase_cmd_scenarios_refused(self): # R1 + self._board() + out, code = self._run("phase", "scenarios", "t") + self.assertNotEqual(code, 0, "scenarios is no longer a phase token") + out2, code2 = self._run("advance", "--to", "scenarios") + self.assertNotEqual(code2, 0) + + +class LegacyStateTest(_Harness): + def test_legacy_state_token_normalizes(self): # M2 + Boundary + self._board() + sp = self.tmp / ".add" / "state.json" + raw = json.loads(sp.read_text(encoding="utf-8")) + raw["tasks"]["t"]["phase"] = "scenarios" # a pre-merge record + sp.write_text(json.dumps(raw), encoding="utf-8") + out = self._ok("status") # any state read + self.assertNotIn("phase=scenarios", out.replace(" ", "")) + self._ok("advance") # specify -> plan works + self.assertEqual(self._phase(), "plan") + + +class LegacySkipDeclarationTest(_Harness): + def _declare_skip(self, token): + p = self.tmp / ".add" / "tasks" / "t" / "TASK.md" + text = p.read_text(encoding="utf-8") + m = re.search(r"(?m)^phase:.*$", text) + self.assertIsNotNone(m, "no phase marker line to anchor the skip header") + text = text[:m.end()] + f"\nskips: {token}" + text[m.end():] + p.write_text(text, encoding="utf-8") + + def test_old_skip_declaration_tolerated_loud(self): # M3 + R2 + Boundary + # The note fires at the crossing where the skip machinery reads the header — + # the observe crossing (M13 of fast-lane-skips: non-skippable crossings run + # ZERO skip logic; that frozen placement is preserved, so earlier advances + # sail through silently and nothing ever dies). + self._board() + self._declare_skip("scenarios") + out, code = self._run("advance") # specify -> plan, silent + self.assertEqual(code, 0, + f"a retired scenarios skip declaration must never die: {out}") + self.assertEqual(self._phase(), "plan") + self._ok("phase", "verify", "t") + out, code = self._run("advance") # verify -> observe crossing + self.assertEqual(code, 0, f"the observe crossing must tolerate it too: {out}") + self.assertIn("merged into specify", out, + "the ignored declaration must be noted loud at the skip crossing") + self.assertEqual(self._phase(), "observe") + + def test_truly_bad_skip_token_still_dies(self): # R2 (floor held) + self._board() + self._declare_skip("build") # never skippable + self._ok("phase", "verify", "t") + out, code = self._run("advance") # the observe crossing + self.assertNotEqual(code, 0, + "a non-skippable token must keep the existing die at the " + "crossing where a skip could take effect") + + +if __name__ == "__main__": + unittest.main() diff --git a/add-method/tooling/test_plan_phase_flow.py b/add-method/tooling/test_plan_phase_flow.py index a6a2cc93..c91d4ad6 100644 --- a/add-method/tooling/test_plan_phase_flow.py +++ b/add-method/tooling/test_plan_phase_flow.py @@ -3,7 +3,7 @@ Pins the collapsed `plan` phase: ground+contract fold into ONE `plan` phase (the change plan = grounding + frozen contract + build-strategy), the order becomes -specify -> scenarios -> plan -> tests -> build -> verify -> observe, the single human +specify -> plan -> tests -> build -> verify -> observe, the single human freeze moves to `plan`, the §3 PLAN template carries the sub-blocks, the grounding floor reads §3, and legacy `ground`/`contract` states still load. The cross-component contract system is UNTOUCHED. @@ -52,13 +52,13 @@ class ConstantsShape(unittest.TestCase): def test_phases_expectations_first(self): # M1 self.assertEqual( C.PHASES, - ("specify", "scenarios", "plan", "tests", "build", "verify", "observe", "done"), + ("specify", "plan", "tests", "build", "verify", "observe", "done"), ) self.assertNotIn("ground", C.PHASES) self.assertNotIn("contract", C.PHASES) def test_direction_bundle_names_plan(self): # M2 - self.assertEqual(C.PHASE_GROUPS["DIRECTION"], ("specify", "scenarios", "plan", "tests")) + self.assertEqual(C.PHASE_GROUPS["DIRECTION"], ("specify", "plan", "tests")) for name, d in (("PHASE_OWNER", C.PHASE_OWNER), ("PHASE_AGENT", C.PHASE_AGENT), ("PHASE_GUIDE", C.PHASE_GUIDE)): @@ -203,8 +203,7 @@ def test_legacy_ground_contract_states_load(self): # M4, R3 — crossing plan->tests needs a frozen §3 def test_plan_to_tests_needs_frozen(self): # advance to plan, draft a minimal frozen-able §3, then cross - self._run("advance") # specify -> scenarios - self._run("advance") # scenarios -> plan + self._run("advance") # specify -> plan (merged flow) self.assertEqual(self._state()["tasks"]["t"]["phase"], "plan") p = self._task_md() md = p.read_text() diff --git a/add-method/tooling/test_proof_harness.py b/add-method/tooling/test_proof_harness.py index a35581b9..d9016c2e 100644 --- a/add-method/tooling/test_proof_harness.py +++ b/add-method/tooling/test_proof_harness.py @@ -60,7 +60,7 @@ def test_gate_pass_refused_before_verify(self): # --- Matrix 3: done only when Verify reads PASS --------------------------- def test_gate_pass_at_verify_reaches_done(self): self._freeze() # freeze-gate-universal: §3 must be FROZEN before plan->tests crossing - for _ in range(5): # specify -> scenarios -> plan -> tests -> build -> verify + for _ in range(4): # specify -> plan -> tests -> build -> verify add.main(["advance"]) self.assertEqual(self._state()["tasks"]["t"]["phase"], "verify") add.main(["gate", "PASS"]) diff --git a/add-method/tooling/test_reopen_transition.py b/add-method/tooling/test_reopen_transition.py index 07c0b6f9..cf833a60 100644 --- a/add-method/tooling/test_reopen_transition.py +++ b/add-method/tooling/test_reopen_transition.py @@ -111,7 +111,7 @@ def _mk_done(self, slug: str, risk_accepted: bool = False): """Drive a fresh task specify -> verify -> done (PASS or RISK-ACCEPTED).""" self._run("new-task", slug, "--title", slug) self._freeze(slug) - for _ in range(5): # specify -> scenarios -> plan -> tests -> build -> verify + for _ in range(4): # specify -> plan -> tests -> build -> verify self._run("advance", slug) if risk_accepted: self._run("gate", "RISK-ACCEPTED", slug, *WAIVER) @@ -132,10 +132,10 @@ def test_reopen_moves_done_task_to_phase(self): "--reason", "wiring check failed post-merge") self.assertEqual(code, 0, f"reopen should succeed; err={err!r}") self.assertEqual(self._task("t")["phase"], "build") - # PHASES tuple keeps "done" as a terminal state, not removed by plan-phase-core - # (which collapsed ground+contract into `plan`, shrinking PHASES from 9 to 8). + # PHASES tuple keeps "done" as a terminal state (phase-merge-specify shrank + # the list to 7: scenarios merged into specify). self.assertEqual(add.PHASES[-1], "done") - self.assertEqual(len(add.PHASES), 8) + self.assertEqual(len(add.PHASES), 7) def test_reopen_resets_gate_to_none(self): self._mk_done("t") diff --git a/add-method/tooling/test_report.py b/add-method/tooling/test_report.py index d9bd8d23..bde00759 100644 --- a/add-method/tooling/test_report.py +++ b/add-method/tooling/test_report.py @@ -215,8 +215,8 @@ def test_phase_track_compact(self): self._done_pass("beta") root = add.find_root() out = add.render_report(root, add.load_state(root), "v9") # ascii=False (canonical) - self.assertIn("●●●◉○○○○", out) # alpha at 'tests': 3 reached, current, 4 pending - self.assertIn("●●●●●●●●", out) # beta done -> whole track reached + self.assertIn("●●◉○○○○", out) # alpha at 'tests': 2 reached, current, 4 pending + self.assertIn("●●●●●●●", out) # beta done -> whole track reached self.assertIn("PASS", out) # gate word (no glyph badge in column) self.assertIn("legend", out) # one legend, not per-row labels diff --git a/add-method/tooling/test_setup_lock.py b/add-method/tooling/test_setup_lock.py index 182ada30..c10af9a9 100644 --- a/add-method/tooling/test_setup_lock.py +++ b/add-method/tooling/test_setup_lock.py @@ -131,8 +131,7 @@ def test_first_task_allowed_second_refused(self): def test_front_advances_but_build_blocked(self): self._init_await() add.main(["new-task", "a"]) # phase=specify (plan-phase-core seed) - self.assertEqual(_run(["advance", "a"])[0], 0) # specify -> scenarios - self.assertEqual(_run(["advance", "a"])[0], 0) # scenarios -> plan + self.assertEqual(_run(["advance", "a"])[0], 0) # specify -> plan # plan-phase-core: the plan->tests crossing is now freeze-gated — freeze §3 here so # this test isolates the setup_unlocked block at tests->build, not contract_not_frozen. self._freeze("a") diff --git a/add-method/tooling/test_skip_error_ergonomics.py b/add-method/tooling/test_skip_error_ergonomics.py index c2fbc772..2f8f1d73 100644 --- a/add-method/tooling/test_skip_error_ergonomics.py +++ b/add-method/tooling/test_skip_error_ergonomics.py @@ -92,8 +92,10 @@ def test_malformed_skips_names_repair(self): self._init_project() self._silent("new-task", "t1", "--title", "T") self._write_task("t1", skips_line="skips: specify,scenarios") - self._silent("phase", "specify", "t1") - out, err, code = self._run("advance", "t1") # specify -> scenarios (skippable) + self._silent("phase", "verify", "t1") + out, err, code = self._run("advance", "t1") # verify -> observe (the skippable + # crossing — phase-merge-specify: scenarios is retired/filtered, `specify` is + # the surviving bad token, and validation lives where a skip could take effect) blob = out + err self.assertNotEqual(code, 0, "fail-closed discard unchanged") self.assertIn("skip_not_allowed", blob, "the code prefix survives") diff --git a/add-method/tooling/test_step_spawn_hint.py b/add-method/tooling/test_step_spawn_hint.py index 50f11324..7e17202f 100644 --- a/add-method/tooling/test_step_spawn_hint.py +++ b/add-method/tooling/test_step_spawn_hint.py @@ -3,7 +3,7 @@ CONTRACT (frozen @ v1): _SPAWN_HINTS: dict[str,str] phase -> idiom; done ABSENT - specify:"domain researcher" · scenarios:"wide scenario sweep" · + specify:"domain researcher + wide scenario sweep" · plan:"change-plan design sweep" · tests:"red-suite test-author" · build:"independent well-scoped batch" · verify:"earned-green refute-read" · observe:"lessons-mining reviewer" @@ -28,8 +28,7 @@ EXPECTED = { - "specify": "domain researcher", - "scenarios": "wide scenario sweep", + "specify": "domain researcher + wide scenario sweep", "plan": "change-plan design sweep", "tests": "red-suite test-author", "build": "independent well-scoped batch", @@ -91,7 +90,7 @@ def _run(self, *argv): def test_status_renders_hint_for_active_specify(self): # fresh task t is active at specify under the default (auto) project dial out = self._run("status") - self.assertIn("spawn hint: specify → domain researcher (tier: mid)", out) + self.assertIn("spawn hint: specify → domain researcher + wide scenario sweep (tier: mid)", out) def test_manual_dial_suppresses_hint(self): self._run("autonomy", "set", "manual", "--project") diff --git a/add-method/tooling/test_streams.py b/add-method/tooling/test_streams.py index 1fc9f847..84b9c31a 100644 --- a/add-method/tooling/test_streams.py +++ b/add-method/tooling/test_streams.py @@ -142,7 +142,7 @@ def test_advance_routes_to_explicit_slug_not_active(self): code, _, _ = _run(["advance", "a"]) self.assertEqual(code, 0) st = self._state() - self.assertEqual(st["tasks"]["a"]["phase"], "scenarios", "advance must step the NAMED task") + self.assertEqual(st["tasks"]["a"]["phase"], "plan", "advance must step the NAMED task") self.assertEqual(st["tasks"]["b"]["phase"], "specify", "the active task must NOT step") self.assertEqual(st["active_task"], "b") @@ -162,7 +162,7 @@ def test_omitted_slug_falls_back_to_active_task(self): code, _, _ = _run(["advance"]) self.assertEqual(code, 0) st = self._state() - self.assertEqual(st["tasks"]["b"]["phase"], "scenarios", "omitted slug must step the active task") + self.assertEqual(st["tasks"]["b"]["phase"], "plan", "omitted slug must step the active task") self.assertEqual(st["tasks"]["a"]["phase"], "specify", "the non-active task must be untouched") diff --git a/add-method/tooling/test_template_form_tags.py b/add-method/tooling/test_template_form_tags.py index 06957931..a6c003b8 100644 --- a/add-method/tooling/test_template_form_tags.py +++ b/add-method/tooling/test_template_form_tags.py @@ -190,8 +190,8 @@ def test_phase_marker_roundtrip_and_seams(self): text = self.task_md.read_text(encoding="utf-8") for name, pat in SEAM_PATTERNS.items(): self.assertTrue(pat.search(text), f"parsed seam survives in scaffold: {name}") - add.main(["advance"]) # specify -> scenarios, syncs the marker into TASK.md - self.assertRegex(self.task_md.read_text(encoding="utf-8"), r"(?m)^phase: scenarios", + add.main(["advance"]) # specify -> plan, syncs the marker into TASK.md + self.assertRegex(self.task_md.read_text(encoding="utf-8"), r"(?m)^phase: plan", "the phase: marker sync must keep working on the new template") def test_freeze_gate_and_declared_count_seams(self): diff --git a/add-method/tooling/test_unflagged_freeze.py b/add-method/tooling/test_unflagged_freeze.py index 17872dcf..3302244f 100644 --- a/add-method/tooling/test_unflagged_freeze.py +++ b/add-method/tooling/test_unflagged_freeze.py @@ -194,7 +194,7 @@ def test_allows_none_escape(self): # -- scope: the guard fires ONLY at the build boundary -- def test_below_build_boundary_unchecked(self): - # a frozen §3 with NO flag, advancing specify->scenarios->plan->tests, is never + # a frozen §3 with NO flag, advancing specify->plan->tests, is never # checked for the flag — the unflagged_freeze guard lives ONLY in `_build_entry` # (the tests->build crossing). The now-earlier plan->tests freeze gate (plan-phase- # core) only checks `_contract_frozen` (true here, via GOOD3), never the flag, so @@ -203,9 +203,7 @@ def test_below_build_boundary_unchecked(self): with redirect_stdout(buf), redirect_stderr(err): add.main(["new-task", "beta", "--title", "beta"]) # stays at specify self._task_md("beta").write_text(self._body("beta", flag=""), encoding="utf-8") - out, err, code = self._advance("beta") # specify -> scenarios - self.assertEqual(code, 0, out + err) - out, err, code = self._advance("beta") # scenarios -> plan + out, err, code = self._advance("beta") # specify -> plan self.assertEqual(code, 0, out + err) out, err, code = self._advance("beta") # plan -> tests (frozen, no flag check here) self.assertEqual(code, 0, out + err) From cd6bdffe9248d98662c95369ab6e9e490acfdd70 Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 01:32:13 +0700 Subject: [PATCH 3/9] =?UTF-8?q?feat(method):=20phase-merge-verify=20?= =?UTF-8?q?=E2=80=94=20observe=20merged=20into=20verify=20(six-phase-loop?= =?UTF-8?q?=202/6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second TRUE phase merge: the observe PHASE folds into verify — verify owns evidence + gate AND the watch/spec-delta duty. §7 OBSERVE stays as a section (the stable API) rendered under the verify block, exactly as §2 renders under specify. The lifecycle is now specify · plan · tests · build · verify (+ done). Engine (synced x3): - constants.py: PHASES=(specify,plan,tests,build,verify,done); _SKIPPABLE_PHASES=() — nothing is skippable; PHASE_GUIDE verify action gains the watch+spec-delta duty; PHASE_GROUPS VERIFY=(verify,); PHASE_OWNER/PHASE_AGENT drop observe - add.py: _legacy gains observe->verify (normalize-on-read; a recorded gate survives); _RETIRED_SKIP_TOKENS={scenarios,observe}; the cmd_advance skip block PRUNED whole (dead once the tuple emptied — the old M13 zero-skip- logic placement is now universal); gate/completion is the ONE seam that reads a vestigial `skips:` header (one loud note, never a die) and the new home of the fold nudge; advance --fill + status --section resolve through _PHASE_SECTIONS (verify: (6,7)) — repairing the two ordinal->section mappings that broke silently at phase-merge-specify (plan filled §2, not §3); membership lists/spawn hints/guide map/help text swept - templates: the fast template's skips scaffold removed (grammar retired end-to-end); both templates' phase-marker comments show the 6-token flow - installer showcase twins: BRAND_LOOP/_LOOP = 5 pre-done steps Tests: NEW test_phase_merge_verify.py (13: list+maps, advance/gate land done with the fold nudge, observe refused, legacy state normalizes keeping its gate, vestigial skips noted at gate never die, _PHASE_SECTIONS + --fill/ --section table pins). Fence r1 ripple: 49 failures/17 suites repaired — test_fast_lane_skips REWRITTEN to pin the retirement (resolver keeps naming bad tokens; historic recorded skips keep status/audit surface); fold nudge test re-aimed to gate; report dots 6-wide; phase-detail 5 blocks with §7 under verify; onboarding showcase 5 steps; len/range pins swept. Zero tests weakened. Full fence 3527/3527 OK. Pins: ENGINE_MD5 -> 5d5e0538 · ENGINE_PKG_MD5 -> fc40ad47 · SEAMS _declared_scope anchor -> add.py:5603. Gate: PASS (compound tick from build; the new fold nudge printed on this task's own gate — self-proved). refs: six-phase-loop milestone, stacked branch feat/six-phase-loop author: Tin Dang --- .add/SEAMS.md | 2 +- .add/state.json | 49 +++- .add/tasks/phase-merge-verify/TASK.md | 115 ++++++++++ add-method/bin/cli.js | 2 +- .../src/add_method/_bundled/tooling/add.py | 122 ++++------ .../_bundled/tooling/add_engine/constants.py | 18 +- .../add_method/_bundled/tooling/engine_pin.py | 4 +- .../tooling/templates/TASK.fast.md.tmpl | 6 +- .../_bundled/tooling/templates/TASK.md.tmpl | 2 +- add-method/src/add_method/_installer.py | 2 +- add-method/tooling/add.py | 122 ++++------ add-method/tooling/add_engine/constants.py | 18 +- add-method/tooling/engine_pin.py | 4 +- .../tooling/templates/TASK.fast.md.tmpl | 6 +- add-method/tooling/templates/TASK.md.tmpl | 2 +- add-method/tooling/test_agent_portability.py | 2 +- add-method/tooling/test_fast_lane_skips.py | 212 +++++++---------- add-method/tooling/test_fast_lane_template.py | 4 +- add-method/tooling/test_fold_nudge.py | 26 +-- add-method/tooling/test_gate_audit.py | 12 +- add-method/tooling/test_gate_owner_marker.py | 6 +- add-method/tooling/test_ground_phase.py | 21 +- add-method/tooling/test_machine_state.py | 2 +- add-method/tooling/test_onboarding_brand.py | 15 +- add-method/tooling/test_phase_bundles.py | 14 +- add-method/tooling/test_phase_detail.py | 19 +- .../tooling/test_phase_merge_specify.py | 39 ++-- add-method/tooling/test_phase_merge_verify.py | 214 ++++++++++++++++++ add-method/tooling/test_plan_phase_flow.py | 2 +- add-method/tooling/test_reopen_transition.py | 6 +- add-method/tooling/test_report.py | 4 +- .../tooling/test_skip_error_ergonomics.py | 22 +- add-method/tooling/test_step_spawn_hint.py | 3 +- 33 files changed, 680 insertions(+), 417 deletions(-) create mode 100644 .add/tasks/phase-merge-verify/TASK.md create mode 100644 add-method/tooling/test_phase_merge_verify.py diff --git a/.add/SEAMS.md b/.add/SEAMS.md index a6a0c679..2210104d 100644 --- a/.add/SEAMS.md +++ b/.add/SEAMS.md @@ -54,7 +54,7 @@ Citations: 232 files reference "byte-identical" in `.add/tasks/` — method: ## scope-token-grammar Name: §5 "Scope (may touch):" token-resolution grammar -Anchor: `add-method/tooling/add.py:5631` (`_declared_scope`) +Anchor: `add-method/tooling/add.py:5603` (`_declared_scope`) Contract: `_declared_scope` reads ONLY the first physical line after the §5 header — a wrapped multi-line list silently truncates. Each backticked token then resolves independently: `./...` = this task's dir, any token containing `/` = project-root-relative, diff --git a/.add/state.json b/.add/state.json index 85202d4e..c17a6825 100644 --- a/.add/state.json +++ b/.add/state.json @@ -1,7 +1,7 @@ { "project": "AIDD / ADD Methodology", "stage": "mvp", - "active_task": "phase-merge-specify", + "active_task": "phase-merge-verify", "tasks": { "add-check": { "title": "add.py check: validate .add project integrity", @@ -9095,10 +9095,53 @@ "email": "tindang.ht97@gmail.com", "source": "git" } + }, + "phase-merge-verify": { + "title": "Merge observe into verify: 7->6 phase list", + "phase": "done", + "gate": "PASS", + "milestone": "six-phase-loop", + "depends_on": [], + "created": "2026-07-13T17:59:25+00:00", + "updated": "2026-07-13T18:31:44+00:00", + "fast": true, + "freeze": { + "version": "v1", + "frozen_at": "2026-07-13T18:03:17+00:00", + "approved_by": "Tin Dang", + "actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } + }, + "flag_verified": true, + "tripwire": { + "contract_md5": "4f385bc417fc13cb0fc013bbd4286d63", + "tests": { + "add-method/tooling/test_phase_merge_verify.py": "83fdc2d1a6dd3a37410dfe28cbfa9527" + } + }, + "scope": { + "declared": [ + "add-method/tooling/", + "add-method/src/add_method/_bundled/tooling/", + ".add/tooling/", + "add-method/.add/", + "add-method/bin/", + "add-method/src/add_method/_installer.py" + ], + "snapshot_md5": "6234372f220b10a474efaf5b09eff4f3" + }, + "gate_actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } } }, "created": "2026-05-28T15:39:04+00:00", - "updated": "2026-07-13T17:57:58+00:00", + "updated": "2026-07-13T18:31:44+00:00", "milestones": { "v1-1": { "title": "v1.1 \u2014 adoption & ergonomics", @@ -10360,7 +10403,7 @@ "plan-legibility": "relations-surface", "ceremony-to-effort": "fold-draft-at-close", "call-floor": "skill-orient-split", - "six-phase-loop": "phase-merge-specify" + "six-phase-loop": "phase-merge-verify" }, "todos": [ { diff --git a/.add/tasks/phase-merge-verify/TASK.md b/.add/tasks/phase-merge-verify/TASK.md new file mode 100644 index 00000000..6ddb9bc9 --- /dev/null +++ b/.add/tasks/phase-merge-verify/TASK.md @@ -0,0 +1,115 @@ +# TASK: Merge observe into verify: 7->6 phase list + +slug: phase-merge-verify · created: 2026-07-14 · stage: mvp +milestone: six-phase-loop +autonomy: auto +phase: done +fast: true + +> Fast lane — one small task, minimal sections, filled top-to-bottom. The trust floor still +> holds: a FROZEN §3 contract · ≥1 red test before build · a recorded §6 gate (security = HARD-STOP). +> The acceptance scenario collapses into §1 `Accept:`; OBSERVE is one optional line at the gate. + +--- + +## 1 · SPECIFY — the rules + +> Project the expectations from the milestone Ground + this request — light, not re-invented. +Feature: phase-merge-verify — the observe PHASE folds into verify (six-phase-loop 2/6, the second human-decided TRUE merge): verify owns evidence + gate AND the watch/spec-delta duty; §7 OBSERVE stays as a section (the stable API) rendered under verify, exactly as §2 renders under specify +Must: + - PHASES drops "observe" (6 tokens incl done: specify,plan,tests,build,verify,done); a bare `advance` from verify lands at done; every phase map follows (PHASE_GUIDE verify action gains the watch+spec-delta duty · PHASE_OWNER · PHASE_GROUPS VERIFY=("verify",) · PHASE_AGENT · spawn hints · guide-file map · every phase-set membership list: autonomy audit, deltas-gate, status recorded-lens, audit verdict/refute scans) + - legacy state migrates on read: phase token "observe" -> "verify" via _normalize_phase_tokens's _legacy map (same silent/TOTAL/idempotent normalize-on-read as scenarios->specify) + - _SKIPPABLE_PHASES = () — nothing is skippable; the cmd_advance skip-hop block (dead once the tuple empties) is PRUNED; a vestigial `skips:` header declaration (any token) is tolerated LOUD at gate/completion time — one advisory note, never a die (the grammar itself is retired) + - _PHASE_SECTIONS gains "verify": (6, 7) — the §7 body renders under the verify block; task_phases emits 5 pre-done blocks + - the two residual ordinal->section mappings repair to the _PHASE_SECTIONS table: advance --fill (add.py ~1537) and status --section (~2706) — both broke silently at phase-merge-specify (plan resolved to §2, not §3); each targets the phase's PRIMARY (first) section; --section digits 0-7 still reach any section directly + - the observe-crossing fold nudge (add.py ~1722) moves to gate completion output (where the ADR harvest already lives); installer showcase twins (cli.js BRAND_LOOP · _installer.py _LOOP) shrink to the 5 pre-done steps +Reject: + - `advance --to observe` / `phase observe ` -> the existing unknown/invalid-phase refusal (observe is no longer a phase token) + - a `skips:` declaration naming ANY token (retired or not) -> the advisory gate-time note (not a die, not a silent accept — loud-never-silent floor held with the grammar retired) +Accept: Given a task at verify with §6 filled, When `gate PASS` runs, Then the task is done and the fold nudge printed at gate; and Given a pre-merge state.json with a task at phase "observe", When any command loads state, Then the task reads as phase "verify" and its recorded gate (if any) still counts it as gate-recorded. +Boundary: two legacy dialects again — the state token ("observe" in state.json, possibly WITH a recorded gate) and the header skip declaration (`skips: observe`); both land tolerated (silent normalize vs loud gate-note) +Assumptions: ⚠ pruning the cmd_advance skip block rewrites test_fast_lane_skips (just rebuilt around observe-only) plus the M13 zero-skip-logic pins and task 1's own LegacySkipDeclarationTest — why: those tests pin the observe crossing, which stops existing; if wrong (a pin resists rewrite because it guards a live behavior I missed): re-cross and re-shape the note placement (cost: fence rounds). Ripple estimate ~30 suites again (report dots · phase-detail blocks · len/range pins · onboarding slice). + +--- + +## 3 · PLAN — the change plan: ground · contract · build-strategy + +### Grounding +Touches (files · symbols): add-method/tooling/add_engine/constants.py:PHASES/_SKIPPABLE_PHASES/PHASE_GUIDE/PHASE_OWNER/PHASE_GROUPS/PHASE_AGENT · add-method/tooling/add.py:_normalize_phase_tokens/_PHASE_SECTIONS/cmd_advance-skip-block-1602/observe-note-1722/fill-ordinal-1537/section-ordinal-2706/spawn-hint-2042/guide-map-3073/membership-lists-3825-4025-6354-7420-7551-7580/help-8455-8650 · add-method/bin/cli.js:BRAND_LOOP · add-method/src/add_method/_installer.py:_LOOP · fixture files stepping the observe phase (fence-named, declared ripples) +Context (working folder): ENGINE_MD5 + ENGINE_PKG_MD5 both re-aim; SEAMS _declared_scope pin drifts on add.py line shifts; sync engine x3 never tests +Honors (patterns / conventions): the phase-merge-specify precedent verbatim (_legacy map · _RETIRED_SKIP_TOKENS · explicit _PHASE_SECTIONS over ordinal math · sections never renumber) · defaults loud, never silent-skip · guides/SKILL.md prose re-cut deferred to guide-recut +Anchors the contract cites: PHASES · _normalize_phase_tokens · _SKIPPABLE_PHASES · _RETIRED_SKIP_TOKENS · _PHASE_SECTIONS · PHASE_GROUPS +Ground SHA: fa12a4d — stamped by freeze + +### Contract + +``` +constants.py: + PHASES = ("specify", "plan", "tests", "build", "verify", "done") + _SKIPPABLE_PHASES = () + PHASE_GUIDE["verify"] action += "; then note what to watch + the spec delta + for the next loop (§7)" + PHASE_GROUPS["VERIFY"] = ("verify",) + PHASE_OWNER/PHASE_AGENT/PHASE_GUIDE: observe keys removed +add.py: + _normalize_phase_tokens: _legacy gains "observe": "verify" + _RETIRED_SKIP_TOKENS = frozenset({"scenarios", "observe"}) + cmd_advance: the `if nxt in _SKIPPABLE_PHASES:` hop block PRUNED (dead); + the observe fold-nudge block PRUNED (moves to gate) + cmd_gate completion: header carries a `skips:` line -> print one note + "the skip grammar is retired — no phase can be skipped (six-phase-loop); + the declaration is ignored"; then the fold nudge (add.py + --task ) prints on every completion + _PHASE_SECTIONS = {"specify": (1,2), "plan": (3,), "tests": (4,), + "build": (5,), "verify": (6,7)} + advance --fill + status --section : n = _PHASE_SECTIONS[cur][0] + (ordinal+1 math retired; digit args unchanged 0-7) + membership lists drop "observe"; spawn hint + guide-map entries removed; + help text: --to (specify..verify) · skip-unlock prose retired +installer twins: BRAND_LOOP/_LOOP = (Specify, Plan, Tests, Build, Verify) +``` + +`Least-sure flag surfaced at freeze:` [test] the gate-time note placement — why: gate has TWO paths to completion (direct `gate` at verify and the compound tick from build) plus reopen->re-gate; the note must fire on all completion paths without double-printing or tripping the report/e2e suites that pin gate output lines; if wrong: anchor the note in the one shared completion helper the paths already funnel through (cost: one refactor round) +Status: FROZEN @ v1 — approved by Tin Dang +### Build-strategy +Scope (may touch): `add-method/tooling/` `add-method/src/add_method/_bundled/tooling/` `.add/tooling/` `add-method/.add/` `add-method/bin/` `add-method/src/add_method/_installer.py` +Strategy & known-problem fixes: red tests -> constants maps -> _legacy/_RETIRED tokens -> prune skip block + observe nudge -> gate-time note + fold nudge -> _PHASE_SECTIONS repairs (--fill/--section) -> membership/help sweeps -> installer twins -> fence -> fixture ripple rounds. Traps: the ordinal->section class (NO index math anywhere — table only) · test_fast_lane_skips + M13 pins + task 1's LegacySkipDeclarationTest all pin the observe crossing (rewrite to gate-time) · a pre-merge observe task WITH recorded gate must stay gate-recorded after normalize · sync x3 · both MD5 pins re-aim · SEAMS line pin drifts. +Approach (domain strategy): lifecycle shrinks again, section API frozen; migrate-on-read; retire the skip grammar loud; duties move to the gate seam where the ADR harvest already lives + +### AI-verify record (required when gate_mode: ai-plan-verify) +- [x] §3 PLAN grounding anchors resolve in the current tree (constants.py:59/83/94/106/117/347 + add.py:1537/1602/1722/2042/2706/3073/3825/4025/6354/7420/7551/7580/8455/8650 all read this session) +- [x] §1 every Must + every Reject present, each paired with its outcome +- [x] §3 Contract shape is concrete (no template placeholder text remains) +- [x] Lowest-confidence flag surfaced and substantive (the gate-note double-print risk across the two completion paths) +Verified by: claude-fable-5 (orchestrator, inline) · at: 2026-07-14T01:35:00Z + +--- + +## 4 · TESTS — failing-first (red) + +Plan: test_phases_has_no_observe · test_advance_verify_lands_done · test_gate_pass_at_verify_marks_done_with_fold_nudge · test_legacy_observe_state_normalizes (plain AND with a recorded gate) · test_phase_cmd_observe_refused · test_vestigial_skips_header_noted_at_gate_never_dies · test_fill_and_section_use_the_sections_table (plan->§3, verify->§6) · test_phase_sections_verify_owns_6_and_7. +Tests live in: `add-method/tooling/test_phase_merge_verify.py` · MUST run red before Build. + +--- + +## 5 · BUILD — AI writes the code (execution) + +> The change plan was frozen in §3 PLAN. Build to it: honor the §3 Build-strategy Scope; improve on the strategy if the code teaches you better. +Strategy actually used: as planned, plus: the whole cmd_advance skip block pruned clean (not just the hop) — with _SKIPPABLE_PHASES empty every line of it was dead; the gate note anchors AFTER the verify-command lines on the `completing` branch (the one shared path both completion routes funnel through — the freeze flag's double-print risk never materialized); the skip HELPERS stay as read-tolerance (status line for historic recorded skips · resolver still names a bad token · audit rationale glint on historic boards); the fast template's skips scaffold removed (grammar retired end-to-end). Ripple: 49 failures/17 suites in fence r1, fast_lane_skips rewritten to pin the RETIREMENT, all repaired, zero weakened. +Code lives in: `./src/` · Constraints: change no test, no frozen contract; stay inside the §3 Build-strategy Scope; allow-list packages only. + +--- + +## 6 · VERIFY — evidence + gate + +- [x] all tests pass · coverage held · no test or contract altered during build +- [x] green was EARNED — no overfit / vacuous asserts / stubbed-away logic +- [x] input dialect held — tests speak the spec's example formats (spec-dialect floor) +- [x] no exposed secrets, injection openings, or unexpected dependencies (security = HARD-STOP) + +Build expectations (from §1 Accept + §3 CONTRACT): PHASES carries no `observe`; a bare advance from verify lands at done; gate PASS at verify completes with the fold nudge printed; a legacy `observe` state token normalizes to verify keeping any recorded gate; a vestigial `skips:` header (any token) is noted loud at gate, never a die; --fill and --section resolve through _PHASE_SECTIONS (verify owns §6+§7) — confirmed by test_phase_merge_verify 13/13 (12 red first + 1 boundary-keeper) + the FULL fence 3527 tests OK / REAL_EXIT=0 (fence-pmv-r2.log; 49-failure ripple across 17 suites repaired as the §1-declared rewrite, zero tests weakened); engine twins ×3 byte-identical; ENGINE_MD5 5d5e0538 + ENGINE_PKG_MD5 fc40ad47 re-pinned; SEAMS `_declared_scope` anchor re-pinned to add.py:5603. + +### GATE RECORD +Outcome: PASS +Reviewed by: Tin Dang · date: 2026-07-14 + diff --git a/add-method/bin/cli.js b/add-method/bin/cli.js index 82d923c2..45f1562f 100755 --- a/add-method/bin/cli.js +++ b/add-method/bin/cli.js @@ -414,7 +414,7 @@ async function loadClack() { // itself is folded into step 3, Plan, not a separate phase). Fail-soft: any draw error is // swallowed so a banner can never abort the install. No color is emitted (default accent: // none); the glyphs / tagline / accent are a SWAPPABLE content slot. -const BRAND_LOOP = ["Specify", "Plan", "Tests", "Build", "Verify", "Observe"]; +const BRAND_LOOP = ["Specify", "Plan", "Tests", "Build", "Verify"]; function terminalCaps(env, stream) { const width = Number(env.COLUMNS) || (stream && stream.columns) || 80; diff --git a/add-method/src/add_method/_bundled/tooling/add.py b/add-method/src/add_method/_bundled/tooling/add.py index baf21a47..805ee739 100644 --- a/add-method/src/add_method/_bundled/tooling/add.py +++ b/add-method/src/add_method/_bundled/tooling/add.py @@ -134,7 +134,8 @@ def _normalize_phase_tokens(state: dict) -> dict: tasks = state.get("tasks") if not isinstance(tasks, dict): return state - _legacy = {"ground": "specify", "contract": "plan", "scenarios": "specify"} + _legacy = {"ground": "specify", "contract": "plan", "scenarios": "specify", + "observe": "verify"} for rec in tasks.values(): if isinstance(rec, dict) and rec.get("phase") in _legacy: rec["phase"] = _legacy[rec["phase"]] @@ -1534,8 +1535,10 @@ def _fill_and_advance(args: argparse.Namespace, root: Path, state: dict, slug: s if ln.startswith("## ") or re.match(r"^---\s*$", ln): _die("fill_body_unparseable: the payload contains a line-start '## ' or a bare " "'---' line — these truncate the §-section scan; reword or indent them") - n = PHASES.index(cur) + 1 # phase ordinal → TASK.md §-number: specify→§1 … observe→§7 - # (§0 GROUND is gone, so the section number is ordinal + 1) + n = _PHASE_SECTIONS[cur][0] # phase → its PRIMARY TASK.md §-number, via the explicit + # table (never ordinal math — the off-by-one bug class; + # specify owns §1+§2, verify owns §6+§7: --fill targets + # the first, the drafting section) f = root / "tasks" / slug / "TASK.md" try: original = f.read_bytes() @@ -1594,47 +1597,12 @@ def cmd_advance(args: argparse.Namespace) -> None: if PHASES.index(_to) <= idx: _die(f"advance_to_not_forward: task '{slug}' is already at {cur}") nxt = PHASES[idx + 1] - # fast-lane-skips: observe - # may be jumped as a SINGLE crossing (never entered) when the task declares it, is - # lane-eligible (fast:true | oneshot:true | project benchmark_mode:true), and states a - # reason BEFORE the jump — no silent skip. Every _die below fires BEFORE the phase write - # further down (validate-then-write, matching every other guard in this function). - if nxt in _SKIPPABLE_PHASES: - hdr = _task_header(root, slug) - tokens, skip_err = _task_skip_set(hdr) - if skip_err: - _die(skip_err) # "skip_not_allowed" — fires the first time a skippable - # phase is reached (the observe crossing), BEFORE any - # skip could be honored — fail-closed placement unchanged - # phase-merge-specify: a pre-merge declaration naming the retired scenarios token is - # tolerated-and-ignored, noted loud at the same crossing the machinery already reads it. - _m_sk = _SKIPS_LINE_RE.search(hdr) - if _m_sk and any(t.strip() in _RETIRED_SKIP_TOKENS for t in _m_sk.group(1).split(",")): - print("note: `skips: scenarios` — scenarios merged into specify " - "(phase-merge-specify); the declaration is ignored") - if nxt in tokens: - eligible = _skip_lane_eligible( - state["tasks"][slug].get("fast") is True, - state["tasks"][slug].get("oneshot") is True, - _project_benchmark_mode(root)) - skip_ok, skip_code = _skip_set_allowed(tokens, eligible) - if not skip_ok: - _die(skip_code) # "skip_lane_required" - # the Skip rationale line now lives with the `skips:` declaration in the task - # header/preamble (§0 GROUND is gone); read hdr first, fall back to a legacy §0 body. - rat_src = hdr + "\n" + _raw_phase_bodies(root, slug).get(0, "") - reason = _skip_rationale(rat_src, nxt) - if not reason: - _die("skip_reason_missing") - state["tasks"][slug].setdefault("skips", []).append({ - "phase": nxt, "reason": reason, - "by": identity._actor_stamp(state)["name"], "at": _now()}) - nxt = PHASES[idx + 2] # hop the skipped phase; always in-bounds ("observe" - # is never PHASES[-1]) - # else: nxt not in tokens -> fall through unchanged, phase entered normally (no-op) + # phase-merge-verify: the skip grammar is RETIRED — _SKIPPABLE_PHASES is empty, no + # crossing runs skip logic (the old M13 placement, now universal). A vestigial + # `skips:` header declaration is read once, at gate/completion, and noted loud there. # build-boundary gate: pre-lock the front (specify..tests) is allowed, but crossing - # into build/verify/observe/done is refused until `add.py lock`. - if not _setup_locked(state) and nxt in ("build", "verify", "observe", "done"): + # into build/verify/done is refused until `add.py lock`. + if not _setup_locked(state) and nxt in ("build", "verify", "done"): _die("setup_unlocked: lock the foundation first — add.py lock") # intra-milestone cross-component HOLD (cross-component-milestone): a consumer of a DECLARED # contract may not enter §3 (scenarios->contract) until its producer froze — proven by the @@ -1719,15 +1687,6 @@ def cmd_advance(args: argparse.Namespace) -> None: save_state(root, state) # F12: durable state FIRST (source of truth) — may _die _sync_task_marker(root, slug, nxt) # then mirror into TASK.md (best-effort) — no split-brain print(f"task '{slug}' phase {cur} -> {nxt}") - if nxt == "observe": - # OBSERVE is where this loop's lessons get captured (TASK.md §7) — suggest routing - # them into PROJECT.md right away (a per-task fold is engine-legal; otherwise the - # lessons sit unconsolidated until milestone close). Additive: fires only at this - # one transition, and the human still decides whether/when to run it. The verb word - # is interpolated via _FOLD_VERB so the domain wording-lint sees no bare slang. - print(" note: record the lessons this loop taught the foundation in §7 " - "OBSERVE, then update PROJECT.md when ready:") - print(f" add.py {_FOLD_VERB} --task {slug} (review first: add.py deltas)") # bundle fast-forward: keep stepping (each pass re-loads state and re-runs every # crossing guard) until the validated --to target is reached. if _to is not None and PHASES.index(nxt) < PHASES.index(_to): @@ -1793,7 +1752,7 @@ def _task_gate_mode(hdr: str) -> str | None: _SKIPS_LINE_RE = re.compile(r"(?:^|·)[ \t]*skips:[ \t]*([^\s<#|]+)", re.MULTILINE) # phase-merge-specify: header tokens for phases that were MERGED away — a pre-merge # declaration naming one is ignored with a note, never a die (old boards keep working). -_RETIRED_SKIP_TOKENS = frozenset({"scenarios"}) +_RETIRED_SKIP_TOKENS = frozenset({"scenarios", "observe"}) def _task_skip_set(hdr: str) -> tuple[frozenset[str], str | None]: @@ -2039,7 +1998,6 @@ def _ai_verify_checklist_complete(raw3: str) -> bool: "tests": "red-suite test-author", "build": "independent well-scoped batch", "verify": "earned-green refute-read", - "observe": "lessons-mining reviewer", } @@ -2236,6 +2194,18 @@ def cmd_gate(args: argparse.Namespace) -> None: _vfy = _task_verify(root, slug) # component-registry-fill: surface the verify command if _vfy: print(f"verify: {_vfy} # run this suite — the engine does not (NO-EXEC)") + if completing: + # phase-merge-verify: gate/completion is the ONE seam that reads a vestigial + # `skips:` header (the grammar is retired — no crossing runs skip logic) and the + # new home of the fold nudge observe's crossing used to print. Both advisory, + # both on every completing outcome; this is the shared path the compound tick + # and the direct verify gate already funnel through (no double-print). + if _SKIPS_LINE_RE.search(_task_header(root, slug)): + print(" note: the skip grammar is retired — no phase can be skipped " + "(six-phase-loop); the `skips:` declaration is ignored") + print(" note: record what to watch + the lessons this loop taught the " + "foundation in §7, then update PROJECT.md when ready:") + print(f" add.py {_FOLD_VERB} --task {slug} (review first: add.py deltas)") # the engine-sourced next step (next-footer-engine): a completing gate hands off to the # state arm; HARD-STOP routes to "resolve HARD-STOP …" — converging the old bespoke line. print(_next_footer(root, state)) @@ -2459,7 +2429,7 @@ def cmd_reopen(args: argparse.Namespace) -> None: if not reason: _die("reopen_reason_required: reopen records WHY — supply a non-empty --reason") target = args.to - if target not in PHASES[:-1]: # specify..observe; never "done", never an unknown name + if target not in PHASES[:-1]: # specify..verify; never "done", never an unknown name _die(f"reopen_target_invalid: --to must be one of {', '.join(PHASES[:-1])} (got {target!r})") now = _now() entry = {"from": "done", "to": target, "reason": reason, "at": now, @@ -2703,7 +2673,9 @@ def cmd_status(args: argparse.Namespace) -> None: if tok.isdigit() and int(tok) <= 7: n = int(tok) elif tok in PHASES and tok != "done": - n = PHASES.index(tok) + 1 # phase name → TASK.md §-number (specify→§1 … observe→§7) + n = _PHASE_SECTIONS[tok][0] # phase → its PRIMARY §-number via the explicit + # table (never ordinal math); digits 0-7 above + # still reach any section directly (§2, §7) else: _die(f"section_unknown: '{_section}' is not 0-7 or a phase name " f"({', '.join(p for p in PHASES if p != 'done')})") @@ -3070,7 +3042,7 @@ def cmd_status(args: argparse.Namespace) -> None: _PHASE_GUIDE_FILES = { "specify": "1-specify.md", "plan": "3-plan.md", "tests": "4-tests.md", - "build": "5-build.md", "verify": "6-verify.md", "observe": "7-observe.md", + "build": "5-build.md", "verify": "6-verify.md", } _SKILL_PHASES_DIR = Path(".claude") / "skills" / "add" / "phases" @@ -3816,13 +3788,13 @@ def cmd_check(args: argparse.Namespace) -> None: "and no §4 test covering it (coverage gap) — add a scenario tag " "or a covers: line")) # autonomy level (task explicit-autonomy-dial): a REAL out-of-set token is a hard - # unknown_autonomy_level; a LIVE task (phase before done/observe) with no `autonomy:` - # line is implicit_autonomy — a WARN, never red. Done/observe predecessors are SKIPPED + # unknown_autonomy_level; a LIVE task (phase before done) with no `autonomy:` + # line is implicit_autonomy — a WARN, never red. Done predecessors are SKIPPED # (a fresh live-only predicate, NOT the audit open-front skip) so the board never floods. _alvl = _autonomy_level(_task_header(root, slug)) checks.append((_alvl != "?", f"task '{slug}' autonomy level recognized", "unknown_autonomy_level (token outside manual|conservative|auto)")) - if _alvl is None and t.get("phase") not in ("done", "observe"): + if _alvl is None and t.get("phase") != "done": warnings.append((f"task '{slug}'", "has no explicit autonomy level (implicit_autonomy) " "— run `add.py autonomy set ` to set it")) # per-component-verify: a bound task whose component declares no green_bar can't be @@ -4022,7 +3994,7 @@ def cmd_check(args: argparse.Namespace) -> None: # ACTIVE state only (never archived history): the audit-scan mitigation the freeze flag pinned. _dialect_gapped = [] for _dg_slug in sorted(tasks): - if (tasks[_dg_slug] or {}).get("phase") not in ("build", "verify", "observe", "done"): + if (tasks[_dg_slug] or {}).get("phase") not in ("build", "verify", "done"): continue if _dialect_gaps(root, _dg_slug): _dialect_gapped.append(_dg_slug) @@ -5959,18 +5931,18 @@ def _clean_phase_body(body: str) -> str: # renumber (§3 is the frozen contract everywhere); the phase list shrank instead, so # specify owns two sections. Explicit — never derive a section from a phase index. _PHASE_SECTIONS = {"specify": (1, 2), "plan": (3,), "tests": (4,), - "build": (5,), "verify": (6,), "observe": (7,)} + "build": (5,), "verify": (6, 7)} def task_phases(root: Path, slug: str) -> list[dict]: """The frozen per-task PHASE-DETAIL shape (v9-1): parse the task section blocks into - the non-terminal phases specify→observe. PURE — NO writes. Each entry is + the non-terminal phases specify→verify. PURE — NO writes. Each entry is { "phase": , "n": <0..len(names)-1>, "body": }. The heading scan lives in _phase_spans (shared with the decide digest); this view CLEANS each body. Missing file / missing section / placeholder-only body -> "(empty)" (fail-closed). The bound tracks len(names) so it follows PHASES length.""" - names = PHASES[:-1] # specify..observe; "done" is a terminal STATE, not a section + names = PHASES[:-1] # specify..verify; "done" is a terminal STATE, not a section f = root / "tasks" / slug / "TASK.md" try: text = f.read_text(encoding="utf-8") @@ -6044,7 +6016,7 @@ def _detail_body(body: str, width: int) -> list[str]: def render_task_detail(root: Path, state: dict, mslug: str, slug: str, *, width: int = _DEFAULT_WIDTH, ascii: bool = False) -> str: - """Format ONE task's seven phase blocks (specify→observe) as the read-only PHASE + """Format ONE task's pre-done phase blocks (specify→verify) as the read-only PHASE DETAIL: each block shows its number+name, a reached/current/pending marker (from the task's state phase), and its captured §N body (fail-closed to "(empty)"). The verify block additionally prints the recorded GATE from state.json — authoritative, NEVER @@ -6062,7 +6034,7 @@ def render_task_detail(root: Path, state: dict, mslug: str, slug: str, *, L.append(f" PHASE {phase} GATE {gate}") L.append(banner) for p in task_phases(root, slug): - i = p["n"] # n IS the PHASES index now (specify=0 .. observe=6) + i = p["n"] # n IS the PHASES index now (specify=0 .. verify=4) mk = (g["reached"] if (phase == "done" or i < ci) else g["current"] if i == ci else g["pending"]) L.append("") @@ -6345,13 +6317,13 @@ def _build_plan(raw3: str) -> list[dict]: def decide_data(root: Path, state: dict, mslug: str, slug: str) -> dict: """FACTS for the task-level decision-point digest (frozen shape). The decision comes - from STATE ONLY: recorded (gate set / observe / done) · front (specify→tests) · + from STATE ONLY: recorded (gate set / done) · front (specify→tests) · gate (build/verify). judgment = extracted markers, byte-verbatim. PURE.""" tasks = state.get("tasks") or {} t = tasks.get(slug, {}) phase = t.get("phase", "specify") gate = t.get("gate", "none") - if gate != "none" or phase in ("observe", "done"): + if gate != "none" or phase == "done": seam = "recorded" elif phase in _FRONT_PHASES: seam = "front" @@ -7405,7 +7377,7 @@ def _audit_findings(root: Path, state: dict) -> tuple[int, list[dict]]: """The gate-audit core: verify that human decision points left WELL-FORMED records. Judgment-free — checks record SHAPE (a named human at the freeze, exactly one gate outcome, prose ≡ state, a marked security note never auto-reviewed), - never re-decides an outcome. Scope: active tasks done/observe or gated; open + never re-decides an outcome. Scope: active tasks done or gated; open fronts skipped. PURE — reads only. Honest limit: shape, not engagement — a forged name passes; CI wiring makes forgery explicit and attributable.""" tasks = state.get("tasks") or {} @@ -7417,7 +7389,7 @@ def f(slug: str, code: str, detail: str) -> None: for slug in sorted(tasks): t = tasks[slug] phase, gate = t.get("phase", "specify"), t.get("gate", "none") - if phase not in ("done", "observe") and gate == "none": + if phase != "done" and gate == "none": continue # the front is still open — nothing recorded to audit checked += 1 raw = _raw_phase_bodies(root, slug) @@ -7462,7 +7434,7 @@ def f(slug: str, code: str, detail: str) -> None: elif gate == "none": # F13 ungated_verdict: §6 carries a verdict the engine never gated. # cmd_gate is the ONLY writer of `gate` (it also marks done), so a - # done/observe task at gate=="none" reached its verdict without the + # done task at gate=="none" reached its verdict without the # engine — a hand-stamped §6 or an `advance` past verify. Constraint 4 # requires a RECORDED outcome; a §6 verdict without one is not trusted. f(slug, "ungated_verdict", @@ -7548,7 +7520,7 @@ def _reported_unrecorded(body_text: str) -> bool: def _guarantee_lint_notices(root: Path, state: dict) -> dict: """PRESENCE-ONLY, MEASURE-NOT-BLOCK lints SURFACED (never failed-on) by `add.py audit` - (guarantee-audit-lints). For tasks that reached verify (phase ∈ {verify, observe, done}): + (guarantee-audit-lints). For tasks that reached verify (phase ∈ {verify, done}): shallow[] = §6 '### Deep checks' block present-but-unfilled (_section_unfilled; an ABSENT block grandfathers a legacy task — never retro-flagged); risk_unset[] = the header carries NO `risk:` token (an undeclared risk level at verify); @@ -7577,7 +7549,7 @@ def _guarantee_lint_notices(root: Path, state: dict) -> dict: contract_report_unrecorded = [] verify_report_unrecorded = [] for slug in sorted(state.get("tasks") or {}): - if (state["tasks"][slug] or {}).get("phase") not in ("verify", "observe", "done"): + if (state["tasks"][slug] or {}).get("phase") not in ("verify", "done"): continue bodies = _raw_phase_bodies(root, slug) body3 = bodies.get(3, "") @@ -8452,7 +8424,7 @@ def build_parser() -> argparse.ArgumentParser: help="fast lane + a REQUEST for the AI-plan-verify contract-freeze gate: " "scaffolds fast:true, oneshot:true, gate_mode:ai-plan-verify (task2's " "_ai_freeze_allowed, unchanged, is the sole arbiter of whether it is " - "ever honored) — also unlocks scenarios/observe skip declarations") + "ever honored; the skip grammar itself is retired — six-phase-loop)") pn.add_argument("--full", action="store_true", help="under a tiny milestone: opt back into the FULL TASK.md template " "(tiny members default to the fast lane)") @@ -8647,7 +8619,7 @@ def build_parser() -> argparse.ArgumentParser: pr.add_argument("slug", nargs="?", default=None) # --to / --reason are validated in-body (not argparse choices) so the named reject # codes fire (reopen_target_invalid / reopen_reason_required), not a bare exit-2. - pr.add_argument("--to", default=None, help="target phase (specify..observe)") + pr.add_argument("--to", default=None, help="target phase (specify..verify)") pr.add_argument("--reason", default="", help="why the task is reopened (required, non-empty)") pr.set_defaults(func=cmd_reopen, _opt_positionals=("slug",)) diff --git a/add-method/src/add_method/_bundled/tooling/add_engine/constants.py b/add-method/src/add_method/_bundled/tooling/add_engine/constants.py index 0da5d5d3..bd9ab3bf 100644 --- a/add-method/src/add_method/_bundled/tooling/add_engine/constants.py +++ b/add-method/src/add_method/_bundled/tooling/add_engine/constants.py @@ -56,7 +56,7 @@ # a sibling of CHANGELOG.md — NOT inside .add/. The ledger IS the attribution source: # a milestone is "released" iff its slug appears on a `milestones:` row. RELEASES_FILE = "RELEASES.md" -PHASES = ("specify", "plan", "tests", "build", "verify", "observe", "done") +PHASES = ("specify", "plan", "tests", "build", "verify", "done") GATES = ("none", "PASS", "RISK-ACCEPTED", "HARD-STOP") # heal-then-escalate (verify-integrity): the bounded self-heal loop cap. A CONFIRMED cheat # (mechanical tripwire divergence, or an agent-reported semantic refute-read finding) returns @@ -78,20 +78,18 @@ "06-step-4-tests.md"), "build": ("write the minimum code to pass the tests; change no test and no contract", "07-step-5-build.md"), - "verify": ("run the suite + non-functional checks, then record the gate", + "verify": ("run the suite + non-functional checks, then record the gate; then note what to watch + the spec delta for the next loop (§7)", "08-step-6-verify.md"), - "observe": ("note what to watch + the spec delta for the next loop", - "09-the-loop.md"), "done": ("this task is done — pick the next feature", "02-the-flow.md"), } # Phase -> who owns it, for the `--json` autonomy signal. An autonomous harness may run a # phase only when owner=="ai" (stop is false); every other phase is a checkpoint. The map -# follows the book's who-does-what table (Verify is "human only"); `tests`/`build`/`observe` +# follows the book's who-does-what table (Verify is "human only"); `tests`/`build` # are AI-led. A phase missing here is `unmapped_phase` (fail closed) — never defaulted. PHASE_OWNER = { "specify": "human", "plan": "seam", - "tests": "ai", "build": "ai", "verify": "human", "observe": "ai", "done": "human", + "tests": "ai", "build": "ai", "verify": "human", "done": "human", } # phase-bundles: the work phases (PHASES minus the terminal "done") group into 3 # agent-owned bundles surfaced at `status`/`guide` — DIRECTION fixes the shape (through @@ -103,18 +101,18 @@ PHASE_GROUPS = { "DIRECTION": ("specify", "plan", "tests"), "BUILD": ("build",), - "VERIFY": ("verify", "observe"), + "VERIFY": ("verify",), } # phase-bundles: the roster agent PREFERRED for each phase (per-PHASE, not per-bundle — # `tests` bundles into DIRECTION above yet its preferred agent is still add-build, the # shipped roster's own boundary: add-design owns specify/plan, add-build owns -# tests/build, add-verify owns verify/observe). A phase missing here is a bug (PHASE_GROUPS' +# tests/build, add-verify owns verify). A phase missing here is a bug (PHASE_GROUPS' # own union covers every key); `_phase_bundle` is the fail-closed resolver for an # unmapped/corrupted phase token, not this map directly. PHASE_AGENT = { "specify": "add-design", "plan": "add-design", "tests": "add-build", "build": "add-build", - "verify": "add-verify", "observe": "add-verify", + "verify": "add-verify", } SETUP_FILES = ("PROJECT.md", "CONVENTIONS.md", "GLOSSARY.md", "MODEL_REGISTRY.md", "dependencies.allowlist", "DESIGN.md", "SOUL.md", "personas/_template.md") @@ -344,7 +342,7 @@ # exclusion (this tuple never names them), not a runtime-checked policy. Same relative order # as PHASES. Listed in __all__ (mirrors _GATE_MODES): a new trust-loosening capability is # deliberately surfaced via `from add_engine.constants import *`, not tucked away. --- -_SKIPPABLE_PHASES = ("observe",) +_SKIPPABLE_PHASES = () # --- format-dialect registry (shared: _dialect_gaps + the tests->build crossing warning + # cmd_check's dialect_gap lint) — quality-floors floor 1. Closed (name, regex) pairs: a diff --git a/add-method/src/add_method/_bundled/tooling/engine_pin.py b/add-method/src/add_method/_bundled/tooling/engine_pin.py index 50db2fb8..c989474f 100644 --- a/add-method/src/add_method/_bundled/tooling/engine_pin.py +++ b/add-method/src/add_method/_bundled/tooling/engine_pin.py @@ -17,5 +17,5 @@ this file only ever holds the newest pointer. """ -ENGINE_MD5 = "b27ce845f42aa832b1338c4881f9cb27" # re-aimed @ phase-merge-specify (six-phase-loop 1/6: scenarios PHASE merged into specify — legacy state token + retired skips: token both tolerated; sections untouched). prior: 26f78f04… @ compound-ticks -ENGINE_PKG_MD5 = "870a4ce0603599ca359302910df01614" # re-aimed @ phase-merge-specify (constants.py: PHASES drops scenarios; _SKIPPABLE_PHASES=(observe,); PHASE_GUIDE/OWNER/GROUPS/AGENT follow). prior: d83fc67f… @ kickoff-truth v2 +ENGINE_MD5 = "5d5e053843cf456a9c9f490267c92793" # re-aimed @ phase-merge-verify (six-phase-loop 2/6: observe PHASE merged into verify — skip grammar retired, gate carries the fold nudge + vestigial-skips note, --fill/--section repaired to the _PHASE_SECTIONS table). prior: b27ce845… @ phase-merge-specify +ENGINE_PKG_MD5 = "fc40ad47544db6f5204b6197b95daf04" # re-aimed @ phase-merge-verify (constants.py: PHASES drops observe; _SKIPPABLE_PHASES=(); PHASE_GUIDE/OWNER/GROUPS/AGENT follow). prior: 870a4ce0… @ phase-merge-specify diff --git a/add-method/src/add_method/_bundled/tooling/templates/TASK.fast.md.tmpl b/add-method/src/add_method/_bundled/tooling/templates/TASK.fast.md.tmpl index 8f691415..99f41f16 100644 --- a/add-method/src/add_method/_bundled/tooling/templates/TASK.fast.md.tmpl +++ b/add-method/src/add_method/_bundled/tooling/templates/TASK.fast.md.tmpl @@ -3,14 +3,12 @@ slug: {{slug}} · created: {{date}} · stage: {{stage}} milestone: {{milestone}} autonomy: {{autonomy}} -phase: specify +phase: specify fast: true - > Fast lane — one small task, minimal sections, filled top-to-bottom. The trust floor still > holds: a FROZEN §3 contract · ≥1 red test before build · a recorded §6 gate (security = HARD-STOP). -> The acceptance scenario collapses into §1 `Accept:`; OBSERVE is one optional line at the gate. +> The acceptance scenario collapses into §1 `Accept:`; the observe note is one optional line at the gate. --- diff --git a/add-method/src/add_method/_bundled/tooling/templates/TASK.md.tmpl b/add-method/src/add_method/_bundled/tooling/templates/TASK.md.tmpl index 2631661e..415944c7 100644 --- a/add-method/src/add_method/_bundled/tooling/templates/TASK.md.tmpl +++ b/add-method/src/add_method/_bundled/tooling/templates/TASK.md.tmpl @@ -3,7 +3,7 @@ slug: {{slug}} · created: {{date}} · stage: {{stage}} milestone: {{milestone}} autonomy: {{autonomy}} -phase: specify +phase: specify > One file = one task — fill top-to-bottom; the phase marker above is the single source of truth (`add.py phase`); unclear phase → its book chapter. diff --git a/add-method/src/add_method/_installer.py b/add-method/src/add_method/_installer.py index 1159ea58..3e795414 100644 --- a/add-method/src/add_method/_installer.py +++ b/add-method/src/add_method/_installer.py @@ -220,7 +220,7 @@ def _write_gemini_settings(target) -> str: # the engine's phase flow), never invented marketing. Grounding itself is folded into # step 3, Plan, not a separate phase. The wordmark glyphs / tagline / accent are a # SWAPPABLE content slot, not part of the frozen boundary. -_LOOP = ("Specify", "Plan", "Tests", "Build", "Verify", "Observe") +_LOOP = ("Specify", "Plan", "Tests", "Build", "Verify") def _terminal_caps(env): diff --git a/add-method/tooling/add.py b/add-method/tooling/add.py index baf21a47..805ee739 100644 --- a/add-method/tooling/add.py +++ b/add-method/tooling/add.py @@ -134,7 +134,8 @@ def _normalize_phase_tokens(state: dict) -> dict: tasks = state.get("tasks") if not isinstance(tasks, dict): return state - _legacy = {"ground": "specify", "contract": "plan", "scenarios": "specify"} + _legacy = {"ground": "specify", "contract": "plan", "scenarios": "specify", + "observe": "verify"} for rec in tasks.values(): if isinstance(rec, dict) and rec.get("phase") in _legacy: rec["phase"] = _legacy[rec["phase"]] @@ -1534,8 +1535,10 @@ def _fill_and_advance(args: argparse.Namespace, root: Path, state: dict, slug: s if ln.startswith("## ") or re.match(r"^---\s*$", ln): _die("fill_body_unparseable: the payload contains a line-start '## ' or a bare " "'---' line — these truncate the §-section scan; reword or indent them") - n = PHASES.index(cur) + 1 # phase ordinal → TASK.md §-number: specify→§1 … observe→§7 - # (§0 GROUND is gone, so the section number is ordinal + 1) + n = _PHASE_SECTIONS[cur][0] # phase → its PRIMARY TASK.md §-number, via the explicit + # table (never ordinal math — the off-by-one bug class; + # specify owns §1+§2, verify owns §6+§7: --fill targets + # the first, the drafting section) f = root / "tasks" / slug / "TASK.md" try: original = f.read_bytes() @@ -1594,47 +1597,12 @@ def cmd_advance(args: argparse.Namespace) -> None: if PHASES.index(_to) <= idx: _die(f"advance_to_not_forward: task '{slug}' is already at {cur}") nxt = PHASES[idx + 1] - # fast-lane-skips: observe - # may be jumped as a SINGLE crossing (never entered) when the task declares it, is - # lane-eligible (fast:true | oneshot:true | project benchmark_mode:true), and states a - # reason BEFORE the jump — no silent skip. Every _die below fires BEFORE the phase write - # further down (validate-then-write, matching every other guard in this function). - if nxt in _SKIPPABLE_PHASES: - hdr = _task_header(root, slug) - tokens, skip_err = _task_skip_set(hdr) - if skip_err: - _die(skip_err) # "skip_not_allowed" — fires the first time a skippable - # phase is reached (the observe crossing), BEFORE any - # skip could be honored — fail-closed placement unchanged - # phase-merge-specify: a pre-merge declaration naming the retired scenarios token is - # tolerated-and-ignored, noted loud at the same crossing the machinery already reads it. - _m_sk = _SKIPS_LINE_RE.search(hdr) - if _m_sk and any(t.strip() in _RETIRED_SKIP_TOKENS for t in _m_sk.group(1).split(",")): - print("note: `skips: scenarios` — scenarios merged into specify " - "(phase-merge-specify); the declaration is ignored") - if nxt in tokens: - eligible = _skip_lane_eligible( - state["tasks"][slug].get("fast") is True, - state["tasks"][slug].get("oneshot") is True, - _project_benchmark_mode(root)) - skip_ok, skip_code = _skip_set_allowed(tokens, eligible) - if not skip_ok: - _die(skip_code) # "skip_lane_required" - # the Skip rationale line now lives with the `skips:` declaration in the task - # header/preamble (§0 GROUND is gone); read hdr first, fall back to a legacy §0 body. - rat_src = hdr + "\n" + _raw_phase_bodies(root, slug).get(0, "") - reason = _skip_rationale(rat_src, nxt) - if not reason: - _die("skip_reason_missing") - state["tasks"][slug].setdefault("skips", []).append({ - "phase": nxt, "reason": reason, - "by": identity._actor_stamp(state)["name"], "at": _now()}) - nxt = PHASES[idx + 2] # hop the skipped phase; always in-bounds ("observe" - # is never PHASES[-1]) - # else: nxt not in tokens -> fall through unchanged, phase entered normally (no-op) + # phase-merge-verify: the skip grammar is RETIRED — _SKIPPABLE_PHASES is empty, no + # crossing runs skip logic (the old M13 placement, now universal). A vestigial + # `skips:` header declaration is read once, at gate/completion, and noted loud there. # build-boundary gate: pre-lock the front (specify..tests) is allowed, but crossing - # into build/verify/observe/done is refused until `add.py lock`. - if not _setup_locked(state) and nxt in ("build", "verify", "observe", "done"): + # into build/verify/done is refused until `add.py lock`. + if not _setup_locked(state) and nxt in ("build", "verify", "done"): _die("setup_unlocked: lock the foundation first — add.py lock") # intra-milestone cross-component HOLD (cross-component-milestone): a consumer of a DECLARED # contract may not enter §3 (scenarios->contract) until its producer froze — proven by the @@ -1719,15 +1687,6 @@ def cmd_advance(args: argparse.Namespace) -> None: save_state(root, state) # F12: durable state FIRST (source of truth) — may _die _sync_task_marker(root, slug, nxt) # then mirror into TASK.md (best-effort) — no split-brain print(f"task '{slug}' phase {cur} -> {nxt}") - if nxt == "observe": - # OBSERVE is where this loop's lessons get captured (TASK.md §7) — suggest routing - # them into PROJECT.md right away (a per-task fold is engine-legal; otherwise the - # lessons sit unconsolidated until milestone close). Additive: fires only at this - # one transition, and the human still decides whether/when to run it. The verb word - # is interpolated via _FOLD_VERB so the domain wording-lint sees no bare slang. - print(" note: record the lessons this loop taught the foundation in §7 " - "OBSERVE, then update PROJECT.md when ready:") - print(f" add.py {_FOLD_VERB} --task {slug} (review first: add.py deltas)") # bundle fast-forward: keep stepping (each pass re-loads state and re-runs every # crossing guard) until the validated --to target is reached. if _to is not None and PHASES.index(nxt) < PHASES.index(_to): @@ -1793,7 +1752,7 @@ def _task_gate_mode(hdr: str) -> str | None: _SKIPS_LINE_RE = re.compile(r"(?:^|·)[ \t]*skips:[ \t]*([^\s<#|]+)", re.MULTILINE) # phase-merge-specify: header tokens for phases that were MERGED away — a pre-merge # declaration naming one is ignored with a note, never a die (old boards keep working). -_RETIRED_SKIP_TOKENS = frozenset({"scenarios"}) +_RETIRED_SKIP_TOKENS = frozenset({"scenarios", "observe"}) def _task_skip_set(hdr: str) -> tuple[frozenset[str], str | None]: @@ -2039,7 +1998,6 @@ def _ai_verify_checklist_complete(raw3: str) -> bool: "tests": "red-suite test-author", "build": "independent well-scoped batch", "verify": "earned-green refute-read", - "observe": "lessons-mining reviewer", } @@ -2236,6 +2194,18 @@ def cmd_gate(args: argparse.Namespace) -> None: _vfy = _task_verify(root, slug) # component-registry-fill: surface the verify command if _vfy: print(f"verify: {_vfy} # run this suite — the engine does not (NO-EXEC)") + if completing: + # phase-merge-verify: gate/completion is the ONE seam that reads a vestigial + # `skips:` header (the grammar is retired — no crossing runs skip logic) and the + # new home of the fold nudge observe's crossing used to print. Both advisory, + # both on every completing outcome; this is the shared path the compound tick + # and the direct verify gate already funnel through (no double-print). + if _SKIPS_LINE_RE.search(_task_header(root, slug)): + print(" note: the skip grammar is retired — no phase can be skipped " + "(six-phase-loop); the `skips:` declaration is ignored") + print(" note: record what to watch + the lessons this loop taught the " + "foundation in §7, then update PROJECT.md when ready:") + print(f" add.py {_FOLD_VERB} --task {slug} (review first: add.py deltas)") # the engine-sourced next step (next-footer-engine): a completing gate hands off to the # state arm; HARD-STOP routes to "resolve HARD-STOP …" — converging the old bespoke line. print(_next_footer(root, state)) @@ -2459,7 +2429,7 @@ def cmd_reopen(args: argparse.Namespace) -> None: if not reason: _die("reopen_reason_required: reopen records WHY — supply a non-empty --reason") target = args.to - if target not in PHASES[:-1]: # specify..observe; never "done", never an unknown name + if target not in PHASES[:-1]: # specify..verify; never "done", never an unknown name _die(f"reopen_target_invalid: --to must be one of {', '.join(PHASES[:-1])} (got {target!r})") now = _now() entry = {"from": "done", "to": target, "reason": reason, "at": now, @@ -2703,7 +2673,9 @@ def cmd_status(args: argparse.Namespace) -> None: if tok.isdigit() and int(tok) <= 7: n = int(tok) elif tok in PHASES and tok != "done": - n = PHASES.index(tok) + 1 # phase name → TASK.md §-number (specify→§1 … observe→§7) + n = _PHASE_SECTIONS[tok][0] # phase → its PRIMARY §-number via the explicit + # table (never ordinal math); digits 0-7 above + # still reach any section directly (§2, §7) else: _die(f"section_unknown: '{_section}' is not 0-7 or a phase name " f"({', '.join(p for p in PHASES if p != 'done')})") @@ -3070,7 +3042,7 @@ def cmd_status(args: argparse.Namespace) -> None: _PHASE_GUIDE_FILES = { "specify": "1-specify.md", "plan": "3-plan.md", "tests": "4-tests.md", - "build": "5-build.md", "verify": "6-verify.md", "observe": "7-observe.md", + "build": "5-build.md", "verify": "6-verify.md", } _SKILL_PHASES_DIR = Path(".claude") / "skills" / "add" / "phases" @@ -3816,13 +3788,13 @@ def cmd_check(args: argparse.Namespace) -> None: "and no §4 test covering it (coverage gap) — add a scenario tag " "or a covers: line")) # autonomy level (task explicit-autonomy-dial): a REAL out-of-set token is a hard - # unknown_autonomy_level; a LIVE task (phase before done/observe) with no `autonomy:` - # line is implicit_autonomy — a WARN, never red. Done/observe predecessors are SKIPPED + # unknown_autonomy_level; a LIVE task (phase before done) with no `autonomy:` + # line is implicit_autonomy — a WARN, never red. Done predecessors are SKIPPED # (a fresh live-only predicate, NOT the audit open-front skip) so the board never floods. _alvl = _autonomy_level(_task_header(root, slug)) checks.append((_alvl != "?", f"task '{slug}' autonomy level recognized", "unknown_autonomy_level (token outside manual|conservative|auto)")) - if _alvl is None and t.get("phase") not in ("done", "observe"): + if _alvl is None and t.get("phase") != "done": warnings.append((f"task '{slug}'", "has no explicit autonomy level (implicit_autonomy) " "— run `add.py autonomy set ` to set it")) # per-component-verify: a bound task whose component declares no green_bar can't be @@ -4022,7 +3994,7 @@ def cmd_check(args: argparse.Namespace) -> None: # ACTIVE state only (never archived history): the audit-scan mitigation the freeze flag pinned. _dialect_gapped = [] for _dg_slug in sorted(tasks): - if (tasks[_dg_slug] or {}).get("phase") not in ("build", "verify", "observe", "done"): + if (tasks[_dg_slug] or {}).get("phase") not in ("build", "verify", "done"): continue if _dialect_gaps(root, _dg_slug): _dialect_gapped.append(_dg_slug) @@ -5959,18 +5931,18 @@ def _clean_phase_body(body: str) -> str: # renumber (§3 is the frozen contract everywhere); the phase list shrank instead, so # specify owns two sections. Explicit — never derive a section from a phase index. _PHASE_SECTIONS = {"specify": (1, 2), "plan": (3,), "tests": (4,), - "build": (5,), "verify": (6,), "observe": (7,)} + "build": (5,), "verify": (6, 7)} def task_phases(root: Path, slug: str) -> list[dict]: """The frozen per-task PHASE-DETAIL shape (v9-1): parse the task section blocks into - the non-terminal phases specify→observe. PURE — NO writes. Each entry is + the non-terminal phases specify→verify. PURE — NO writes. Each entry is { "phase": , "n": <0..len(names)-1>, "body": }. The heading scan lives in _phase_spans (shared with the decide digest); this view CLEANS each body. Missing file / missing section / placeholder-only body -> "(empty)" (fail-closed). The bound tracks len(names) so it follows PHASES length.""" - names = PHASES[:-1] # specify..observe; "done" is a terminal STATE, not a section + names = PHASES[:-1] # specify..verify; "done" is a terminal STATE, not a section f = root / "tasks" / slug / "TASK.md" try: text = f.read_text(encoding="utf-8") @@ -6044,7 +6016,7 @@ def _detail_body(body: str, width: int) -> list[str]: def render_task_detail(root: Path, state: dict, mslug: str, slug: str, *, width: int = _DEFAULT_WIDTH, ascii: bool = False) -> str: - """Format ONE task's seven phase blocks (specify→observe) as the read-only PHASE + """Format ONE task's pre-done phase blocks (specify→verify) as the read-only PHASE DETAIL: each block shows its number+name, a reached/current/pending marker (from the task's state phase), and its captured §N body (fail-closed to "(empty)"). The verify block additionally prints the recorded GATE from state.json — authoritative, NEVER @@ -6062,7 +6034,7 @@ def render_task_detail(root: Path, state: dict, mslug: str, slug: str, *, L.append(f" PHASE {phase} GATE {gate}") L.append(banner) for p in task_phases(root, slug): - i = p["n"] # n IS the PHASES index now (specify=0 .. observe=6) + i = p["n"] # n IS the PHASES index now (specify=0 .. verify=4) mk = (g["reached"] if (phase == "done" or i < ci) else g["current"] if i == ci else g["pending"]) L.append("") @@ -6345,13 +6317,13 @@ def _build_plan(raw3: str) -> list[dict]: def decide_data(root: Path, state: dict, mslug: str, slug: str) -> dict: """FACTS for the task-level decision-point digest (frozen shape). The decision comes - from STATE ONLY: recorded (gate set / observe / done) · front (specify→tests) · + from STATE ONLY: recorded (gate set / done) · front (specify→tests) · gate (build/verify). judgment = extracted markers, byte-verbatim. PURE.""" tasks = state.get("tasks") or {} t = tasks.get(slug, {}) phase = t.get("phase", "specify") gate = t.get("gate", "none") - if gate != "none" or phase in ("observe", "done"): + if gate != "none" or phase == "done": seam = "recorded" elif phase in _FRONT_PHASES: seam = "front" @@ -7405,7 +7377,7 @@ def _audit_findings(root: Path, state: dict) -> tuple[int, list[dict]]: """The gate-audit core: verify that human decision points left WELL-FORMED records. Judgment-free — checks record SHAPE (a named human at the freeze, exactly one gate outcome, prose ≡ state, a marked security note never auto-reviewed), - never re-decides an outcome. Scope: active tasks done/observe or gated; open + never re-decides an outcome. Scope: active tasks done or gated; open fronts skipped. PURE — reads only. Honest limit: shape, not engagement — a forged name passes; CI wiring makes forgery explicit and attributable.""" tasks = state.get("tasks") or {} @@ -7417,7 +7389,7 @@ def f(slug: str, code: str, detail: str) -> None: for slug in sorted(tasks): t = tasks[slug] phase, gate = t.get("phase", "specify"), t.get("gate", "none") - if phase not in ("done", "observe") and gate == "none": + if phase != "done" and gate == "none": continue # the front is still open — nothing recorded to audit checked += 1 raw = _raw_phase_bodies(root, slug) @@ -7462,7 +7434,7 @@ def f(slug: str, code: str, detail: str) -> None: elif gate == "none": # F13 ungated_verdict: §6 carries a verdict the engine never gated. # cmd_gate is the ONLY writer of `gate` (it also marks done), so a - # done/observe task at gate=="none" reached its verdict without the + # done task at gate=="none" reached its verdict without the # engine — a hand-stamped §6 or an `advance` past verify. Constraint 4 # requires a RECORDED outcome; a §6 verdict without one is not trusted. f(slug, "ungated_verdict", @@ -7548,7 +7520,7 @@ def _reported_unrecorded(body_text: str) -> bool: def _guarantee_lint_notices(root: Path, state: dict) -> dict: """PRESENCE-ONLY, MEASURE-NOT-BLOCK lints SURFACED (never failed-on) by `add.py audit` - (guarantee-audit-lints). For tasks that reached verify (phase ∈ {verify, observe, done}): + (guarantee-audit-lints). For tasks that reached verify (phase ∈ {verify, done}): shallow[] = §6 '### Deep checks' block present-but-unfilled (_section_unfilled; an ABSENT block grandfathers a legacy task — never retro-flagged); risk_unset[] = the header carries NO `risk:` token (an undeclared risk level at verify); @@ -7577,7 +7549,7 @@ def _guarantee_lint_notices(root: Path, state: dict) -> dict: contract_report_unrecorded = [] verify_report_unrecorded = [] for slug in sorted(state.get("tasks") or {}): - if (state["tasks"][slug] or {}).get("phase") not in ("verify", "observe", "done"): + if (state["tasks"][slug] or {}).get("phase") not in ("verify", "done"): continue bodies = _raw_phase_bodies(root, slug) body3 = bodies.get(3, "") @@ -8452,7 +8424,7 @@ def build_parser() -> argparse.ArgumentParser: help="fast lane + a REQUEST for the AI-plan-verify contract-freeze gate: " "scaffolds fast:true, oneshot:true, gate_mode:ai-plan-verify (task2's " "_ai_freeze_allowed, unchanged, is the sole arbiter of whether it is " - "ever honored) — also unlocks scenarios/observe skip declarations") + "ever honored; the skip grammar itself is retired — six-phase-loop)") pn.add_argument("--full", action="store_true", help="under a tiny milestone: opt back into the FULL TASK.md template " "(tiny members default to the fast lane)") @@ -8647,7 +8619,7 @@ def build_parser() -> argparse.ArgumentParser: pr.add_argument("slug", nargs="?", default=None) # --to / --reason are validated in-body (not argparse choices) so the named reject # codes fire (reopen_target_invalid / reopen_reason_required), not a bare exit-2. - pr.add_argument("--to", default=None, help="target phase (specify..observe)") + pr.add_argument("--to", default=None, help="target phase (specify..verify)") pr.add_argument("--reason", default="", help="why the task is reopened (required, non-empty)") pr.set_defaults(func=cmd_reopen, _opt_positionals=("slug",)) diff --git a/add-method/tooling/add_engine/constants.py b/add-method/tooling/add_engine/constants.py index 0da5d5d3..bd9ab3bf 100644 --- a/add-method/tooling/add_engine/constants.py +++ b/add-method/tooling/add_engine/constants.py @@ -56,7 +56,7 @@ # a sibling of CHANGELOG.md — NOT inside .add/. The ledger IS the attribution source: # a milestone is "released" iff its slug appears on a `milestones:` row. RELEASES_FILE = "RELEASES.md" -PHASES = ("specify", "plan", "tests", "build", "verify", "observe", "done") +PHASES = ("specify", "plan", "tests", "build", "verify", "done") GATES = ("none", "PASS", "RISK-ACCEPTED", "HARD-STOP") # heal-then-escalate (verify-integrity): the bounded self-heal loop cap. A CONFIRMED cheat # (mechanical tripwire divergence, or an agent-reported semantic refute-read finding) returns @@ -78,20 +78,18 @@ "06-step-4-tests.md"), "build": ("write the minimum code to pass the tests; change no test and no contract", "07-step-5-build.md"), - "verify": ("run the suite + non-functional checks, then record the gate", + "verify": ("run the suite + non-functional checks, then record the gate; then note what to watch + the spec delta for the next loop (§7)", "08-step-6-verify.md"), - "observe": ("note what to watch + the spec delta for the next loop", - "09-the-loop.md"), "done": ("this task is done — pick the next feature", "02-the-flow.md"), } # Phase -> who owns it, for the `--json` autonomy signal. An autonomous harness may run a # phase only when owner=="ai" (stop is false); every other phase is a checkpoint. The map -# follows the book's who-does-what table (Verify is "human only"); `tests`/`build`/`observe` +# follows the book's who-does-what table (Verify is "human only"); `tests`/`build` # are AI-led. A phase missing here is `unmapped_phase` (fail closed) — never defaulted. PHASE_OWNER = { "specify": "human", "plan": "seam", - "tests": "ai", "build": "ai", "verify": "human", "observe": "ai", "done": "human", + "tests": "ai", "build": "ai", "verify": "human", "done": "human", } # phase-bundles: the work phases (PHASES minus the terminal "done") group into 3 # agent-owned bundles surfaced at `status`/`guide` — DIRECTION fixes the shape (through @@ -103,18 +101,18 @@ PHASE_GROUPS = { "DIRECTION": ("specify", "plan", "tests"), "BUILD": ("build",), - "VERIFY": ("verify", "observe"), + "VERIFY": ("verify",), } # phase-bundles: the roster agent PREFERRED for each phase (per-PHASE, not per-bundle — # `tests` bundles into DIRECTION above yet its preferred agent is still add-build, the # shipped roster's own boundary: add-design owns specify/plan, add-build owns -# tests/build, add-verify owns verify/observe). A phase missing here is a bug (PHASE_GROUPS' +# tests/build, add-verify owns verify). A phase missing here is a bug (PHASE_GROUPS' # own union covers every key); `_phase_bundle` is the fail-closed resolver for an # unmapped/corrupted phase token, not this map directly. PHASE_AGENT = { "specify": "add-design", "plan": "add-design", "tests": "add-build", "build": "add-build", - "verify": "add-verify", "observe": "add-verify", + "verify": "add-verify", } SETUP_FILES = ("PROJECT.md", "CONVENTIONS.md", "GLOSSARY.md", "MODEL_REGISTRY.md", "dependencies.allowlist", "DESIGN.md", "SOUL.md", "personas/_template.md") @@ -344,7 +342,7 @@ # exclusion (this tuple never names them), not a runtime-checked policy. Same relative order # as PHASES. Listed in __all__ (mirrors _GATE_MODES): a new trust-loosening capability is # deliberately surfaced via `from add_engine.constants import *`, not tucked away. --- -_SKIPPABLE_PHASES = ("observe",) +_SKIPPABLE_PHASES = () # --- format-dialect registry (shared: _dialect_gaps + the tests->build crossing warning + # cmd_check's dialect_gap lint) — quality-floors floor 1. Closed (name, regex) pairs: a diff --git a/add-method/tooling/engine_pin.py b/add-method/tooling/engine_pin.py index 50db2fb8..c989474f 100644 --- a/add-method/tooling/engine_pin.py +++ b/add-method/tooling/engine_pin.py @@ -17,5 +17,5 @@ this file only ever holds the newest pointer. """ -ENGINE_MD5 = "b27ce845f42aa832b1338c4881f9cb27" # re-aimed @ phase-merge-specify (six-phase-loop 1/6: scenarios PHASE merged into specify — legacy state token + retired skips: token both tolerated; sections untouched). prior: 26f78f04… @ compound-ticks -ENGINE_PKG_MD5 = "870a4ce0603599ca359302910df01614" # re-aimed @ phase-merge-specify (constants.py: PHASES drops scenarios; _SKIPPABLE_PHASES=(observe,); PHASE_GUIDE/OWNER/GROUPS/AGENT follow). prior: d83fc67f… @ kickoff-truth v2 +ENGINE_MD5 = "5d5e053843cf456a9c9f490267c92793" # re-aimed @ phase-merge-verify (six-phase-loop 2/6: observe PHASE merged into verify — skip grammar retired, gate carries the fold nudge + vestigial-skips note, --fill/--section repaired to the _PHASE_SECTIONS table). prior: b27ce845… @ phase-merge-specify +ENGINE_PKG_MD5 = "fc40ad47544db6f5204b6197b95daf04" # re-aimed @ phase-merge-verify (constants.py: PHASES drops observe; _SKIPPABLE_PHASES=(); PHASE_GUIDE/OWNER/GROUPS/AGENT follow). prior: 870a4ce0… @ phase-merge-specify diff --git a/add-method/tooling/templates/TASK.fast.md.tmpl b/add-method/tooling/templates/TASK.fast.md.tmpl index 8f691415..99f41f16 100644 --- a/add-method/tooling/templates/TASK.fast.md.tmpl +++ b/add-method/tooling/templates/TASK.fast.md.tmpl @@ -3,14 +3,12 @@ slug: {{slug}} · created: {{date}} · stage: {{stage}} milestone: {{milestone}} autonomy: {{autonomy}} -phase: specify +phase: specify fast: true - > Fast lane — one small task, minimal sections, filled top-to-bottom. The trust floor still > holds: a FROZEN §3 contract · ≥1 red test before build · a recorded §6 gate (security = HARD-STOP). -> The acceptance scenario collapses into §1 `Accept:`; OBSERVE is one optional line at the gate. +> The acceptance scenario collapses into §1 `Accept:`; the observe note is one optional line at the gate. --- diff --git a/add-method/tooling/templates/TASK.md.tmpl b/add-method/tooling/templates/TASK.md.tmpl index 2631661e..415944c7 100644 --- a/add-method/tooling/templates/TASK.md.tmpl +++ b/add-method/tooling/templates/TASK.md.tmpl @@ -3,7 +3,7 @@ slug: {{slug}} · created: {{date}} · stage: {{stage}} milestone: {{milestone}} autonomy: {{autonomy}} -phase: specify +phase: specify > One file = one task — fill top-to-bottom; the phase marker above is the single source of truth (`add.py phase`); unclear phase → its book chapter. diff --git a/add-method/tooling/test_agent_portability.py b/add-method/tooling/test_agent_portability.py index bbf607c7..e204bc03 100644 --- a/add-method/tooling/test_agent_portability.py +++ b/add-method/tooling/test_agent_portability.py @@ -23,7 +23,7 @@ PHASE_FILES = { "specify": "1-specify.md", "plan": "3-plan.md", "tests": "4-tests.md", # plan-phase-core: "contract" -> "plan" - "build": "5-build.md", "verify": "6-verify.md", "observe": "7-observe.md", + "build": "5-build.md", "verify": "6-verify.md", } diff --git a/add-method/tooling/test_fast_lane_skips.py b/add-method/tooling/test_fast_lane_skips.py index 1a77111e..2fc9cff3 100644 --- a/add-method/tooling/test_fast_lane_skips.py +++ b/add-method/tooling/test_fast_lane_skips.py @@ -1,29 +1,17 @@ #!/usr/bin/env python3 -"""Red/green tests for fast-lane-skips (task: fast-lane-skips, milestone: three-phase-flow). - -CONTRACT (frozen @ v1, §3 of .add/tasks/fast-lane-skips/TASK.md): a per-task, AI-declared -skip-set (subset of `{scenarios, observe}`) that lets `cmd_advance` jump those two phases as a -REAL engine-level crossing (never entered) — gated by lane eligibility (`fast:true` | -`oneshot:true` | project `benchmark_mode:true`), fail-closed on any out-of-set token, and never -honored without a stated per-phase reason (§0 "Skip rationale:") recorded BEFORE the jump. Every -actual skip is recorded in `state["tasks"][slug]["skips"]`. Composed with, never entangled in, -task2's (`ai-plan-verify-gate`) `gate_mode: ai-plan-verify` contract-auto-freeze mechanism via a -new `--oneshot` flag that declares both at once. - - add_engine/constants.py _SKIPPABLE_PHASES = ("scenarios", "observe") — joins __all__ - add.py _SKIPS_LINE_RE / _task_skip_set(hdr) — mirrors _task_gate_mode - add_engine/predicates.py _skip_lane_eligible(fast, oneshot, benchmark_mode) — mirrors _ai_freeze_allowed - add_engine/predicates.py _skip_set_allowed(skip_tokens, eligible) - add.py _skip_rationale(raw0, phase) - add.py _project_benchmark_mode(root) — mirrors _project_autonomy_token - add.py cmd_advance skip pre-pass (`if nxt in _SKIPPABLE_PHASES:`) - add.py cmd_new_task --oneshot (sibling of --fast) - add.py cmd_status/guide additive "skips : ..." line - add.py _gate_explain additive "skip-set: ..." line - add.py cmd_audit skip_rationale_missing_post_hoc residual glint (MEASURE-NOT-BLOCK) - templates/TASK.fast.md.tmpl scaffold: commented `skips:` header hint + §0 placeholder line - -The normal flow (no fast/oneshot/benchmark, no skips:) is BYTE-IDENTICAL to before this task. +"""Tests for the RETIRED fast-lane skip grammar (fast-lane-skips shipped it; the two +six-phase-loop merges retired it — first scenarios, then observe left PHASES, and with +them _SKIPPABLE_PHASES emptied). What survives, pinned here: + + _SKIPPABLE_PHASES = () — nothing is skippable; no crossing runs skip logic + _task_skip_set — resolver kept for read-tolerance: retired tokens filter + to the empty set; a truly bad token still errs + cmd_gate (completion) — the ONE seam that reads a vestigial `skips:` header: + one loud advisory note, never a die + status/guide + audit — historic boards with RECORDED skips keep their surface + (the line renders; a deleted rationale still audits) + templates — NEITHER template scaffolds the skip grammar anymore + --oneshot / benchmark_mode — the lane flags live on (unrelated to skipping) Run: cd add-method/tooling && python3 -m unittest test_fast_lane_skips -v """ @@ -181,8 +169,8 @@ def _new_fast_task(self, slug="t", fast=True, oneshot=False, benchmark=False): class SkippablePhasesConstantTest(unittest.TestCase): def test_value_and_all(self): - self.assertEqual(engine_constants._SKIPPABLE_PHASES, ("observe",)) - # phase-merge-specify: scenarios left the tuple (merged into specify) + self.assertEqual(engine_constants._SKIPPABLE_PHASES, ()) + # phase-merge-verify: observe left the tuple too — nothing is skippable self.assertIn("_SKIPPABLE_PHASES", engine_constants.__all__) def test_importable_via_star_import(self): @@ -200,13 +188,13 @@ def test_absent_line_returns_empty_set_no_error(self): (frozenset(), None)) def test_retired_token_filtered_from_csv(self): - # phase-merge-specify: a pre-merge two-token declaration keeps its live half + # phase-merge-verify: BOTH pre-merge tokens are retired — the set is empty self.assertEqual(add._task_skip_set("skips: scenarios,observe\n"), - (frozenset({"observe"}), None)) + (frozenset(), None)) - def test_valid_single_token(self): + def test_observe_token_retired_resolves_empty(self): self.assertEqual(add._task_skip_set("skips: observe\n"), - (frozenset({"observe"}), None)) + (frozenset(), None)) def test_retired_only_declaration_resolves_empty(self): self.assertEqual(add._task_skip_set("skips: scenarios\n"), @@ -292,46 +280,35 @@ def test_empty_reason_text_returns_none(self): # --------------------------------------------------------------------------- class CmdAdvanceSkipMechanicTest(_Harness): - def test_retired_scenarios_declaration_ignored_with_note(self): - # phase-merge-specify: specify -> plan is the NATURAL crossing now; a pre-merge - # scenarios declaration is tolerated-and-ignored (noted at the observe crossing), - # and no skip is ever recorded for the retired phase. + def test_retired_declaration_never_touches_a_crossing(self): + # phase-merge-verify: NO crossing runs skip logic; a vestigial declaration + # (either retired token) is invisible to advance and records nothing. self._new_fast_task("t", fast=True) self._silent("phase", "specify", "t") - self._set_header("t", skips="scenarios") - out = self._silent("advance", "t") + self._set_header("t", skips="scenarios,observe") + self._silent("advance", "t") self.assertEqual(self._state()["tasks"]["t"]["phase"], "plan") self.assertNotIn("skips", self._state()["tasks"]["t"]) self._silent("phase", "verify", "t") - out = self._silent("advance", "t") # the observe crossing - self.assertIn("merged into specify", out) - self.assertEqual(self._state()["tasks"]["t"]["phase"], "observe") + out = self._silent("advance", "t") # verify -> done, silent + self.assertEqual(self._state()["tasks"]["t"]["phase"], "done") self.assertNotIn("skips", self._state()["tasks"]["t"]) + self.assertNotIn("note:", out, "crossings stay silent — the note lives at gate") - def test_jumps_verify_to_done_when_observe_declared_eligible_reasoned(self): + def test_vestigial_declaration_noted_loud_at_gate(self): + # the ONE seam that reads the header: gate/completion (where the ADR + # harvest + fold nudge already live). self._new_fast_task("t", fast=True) - self._silent("phase", "verify", "t") self._set_header("t", skips="observe") - self._set_skip_rationale("t", "observe — no rollout to watch") - out = self._silent("advance", "t") + self._silent("phase", "verify", "t") + out = self._silent("gate", "PASS", "t") + self.assertIn("retired", out) self.assertEqual(self._state()["tasks"]["t"]["phase"], "done") - self.assertIn("phase verify -> done", out) - skips = self._state()["tasks"]["t"]["skips"] - self.assertEqual(len(skips), 1) - self.assertEqual(skips[0]["phase"], "observe") - - def test_ordinary_path_when_phase_not_in_declared_skip_set(self): - self._new_fast_task("t", fast=True) - self._silent("phase", "specify", "t") - self._set_header("t", skips="observe") - self._set_skip_rationale("t", "observe — no rollout to watch") - self._silent("advance", "t") - self.assertEqual(self._state()["tasks"]["t"]["phase"], "plan") self.assertNotIn("skips", self._state()["tasks"]["t"]) # --------------------------------------------------------------------------- -# M6 / M13 — the 6 non-skippable crossings execute none of this task's new logic +# M6 / M13 (generalized by phase-merge-verify) — EVERY crossing runs zero skip logic # --------------------------------------------------------------------------- class NonSkippableCrossingsUntouchedTest(_Harness): @@ -354,18 +331,18 @@ def _set_section3_and_freeze(self, slug): self._fill_boundary(slug) self._silent("freeze", "--by", "Human") - def test_five_non_skippable_crossings_never_invoke_task_skip_set(self): - # phase-merge-specify: scenarios merged into specify, so the flow is now - # specify -> plan -> tests -> build -> verify -> observe -> done — - # 6 crossings total, 1 "by design" (verify->observe) and FIVE non-skippable. + def test_every_crossing_never_invokes_task_skip_set(self): + # phase-merge-verify: observe merged into verify and the skip grammar retired, + # so the flow is specify -> plan -> tests -> build -> verify -> done — + # 5 crossings total, ALL of them run ZERO skip logic (the old M13 pin, + # now universal; the header is read once, at gate, not here). self._new_fast_task("t", fast=True) # no skips: declared anywhere self.assertEqual(self._advance_spy_count(), 0, "specify->plan") self._set_section3_and_freeze("t") self.assertEqual(self._advance_spy_count(), 0, "plan->tests") self.assertEqual(self._advance_spy_count(), 0, "tests->build") self.assertEqual(self._advance_spy_count(), 0, "build->verify") - self.assertGreater(self._advance_spy_count(), 0, "verify->observe (by design)") - self.assertEqual(self._advance_spy_count(), 0, "observe->done") + self.assertEqual(self._advance_spy_count(), 0, "verify->done") self.assertEqual(self._state()["tasks"]["t"]["phase"], "done") @@ -429,17 +406,19 @@ def test_malformed_token_is_false(self): # --------------------------------------------------------------------------- class StatusGuideSurfaceTest(_Harness): - def test_status_and_guide_show_declared_csv_and_progress(self): + def test_historic_recorded_skip_keeps_its_status_surface(self): + # No live path records a skip anymore; a HISTORIC board (pre-merge) may + # still carry one — its status/guide line must keep rendering (read-tolerance). self._new_fast_task("t", fast=True) - self._silent("phase", "verify", "t") - self._set_header("t", skips="observe") - self._set_skip_rationale("t", "observe — no rollout to watch") - self._silent("advance", "t") # consumes the "observe" skip -> 1 recorded + sp = self.tmp / ".add" / "state.json" + raw = json.loads(sp.read_text(encoding="utf-8")) + raw["tasks"]["t"]["skips"] = [{"phase": "observe", "reason": "historic", + "by": "Tester", "at": "2026-01-01T00:00:00Z"}] + sp.write_text(json.dumps(raw), encoding="utf-8") out_status = self._silent("status") out_guide = self._silent("guide") for out in (out_status, out_guide): - self.assertIn("declared observe", out) - self.assertIn("skipped so far 1/1", out) + self.assertIn("skipped so far 1/", out) def test_no_declaration_no_recorded_skip_prints_no_line(self): self._new_fast_task("t", fast=True) @@ -452,13 +431,15 @@ def test_no_declaration_no_recorded_skip_prints_no_line(self): # --------------------------------------------------------------------------- class GateExplainSkipSetTest(_Harness): - def test_blocked_outcome_for_ineligible_declared_task(self): + def test_vestigial_declaration_prints_no_skip_set_line(self): + # a retired-token declaration resolves to the empty set — gate-explain + # shows no skip-set line (there is no live skip mechanism to explain) self._silent("lock", "--force") self._silent("new-task", "t", "--title", "F") # NOT fast/oneshot self._set_header("t", skips="observe") out = self._silent("gate", "--explain", "t") - self.assertIn("skip-set: blocked (skip_lane_required)", out) - # the existing gate_mode line (task2, absent here) never crashes / interferes + self.assertNotIn("skip-set:", out) + # the surrounding explain surface never crashes / interferes self.assertIn("advisor-gate-relax:", out) def test_no_declaration_prints_no_skip_set_line(self): @@ -474,11 +455,17 @@ def test_no_declaration_prints_no_skip_set_line(self): class AuditSkipRationaleMissingPostHocTest(_Harness): def _skipped_task(self, slug="t"): + # HISTORIC board simulation: no live path records a skip anymore, but the + # audit glint must keep guarding boards that recorded one pre-merge. self._new_fast_task(slug, fast=True) self._silent("phase", "verify", slug) self._set_header(slug, skips="observe") self._set_skip_rationale(slug, "observe — no rollout to watch") - self._silent("advance", slug) # verify -> done, records the skip + sp = self.tmp / ".add" / "state.json" + raw = json.loads(sp.read_text(encoding="utf-8")) + raw["tasks"][slug]["skips"] = [{"phase": "observe", "reason": "no rollout to watch", + "by": "Tester", "at": "2026-01-01T00:00:00Z"}] + sp.write_text(json.dumps(raw), encoding="utf-8") self._silent("gate", "HARD-STOP", slug) # recordable from any phase def test_intact_rationale_not_flagged(self): @@ -500,19 +487,18 @@ def test_deleted_rationale_flagged_measure_not_block(self): # --------------------------------------------------------------------------- class TemplateScaffoldTest(unittest.TestCase): - def test_fast_template_has_skips_hint_and_rationale_placeholder(self): + def test_fast_template_carries_no_skips_machinery(self): + # phase-merge-verify: the grammar is retired — the fast template no longer + # scaffolds the `skips:` hint or the rationale placeholder. text = (HERE / "templates" / "TASK.fast.md.tmpl").read_text(encoding="utf-8") - self.assertIn("skips:", text) - self.assertIn("Skip rationale:", text) + self.assertNotIn("skips:", text) + self.assertNotIn("Skip rationale:", text) def test_full_template_carries_no_skips_machinery(self): - # the skip lane is fast-template-only: the full template must never gain a - # `skips:` header hint. (Amended @ dialect-check-and-data-vocab TESTS re-cross: - # the original empty-git-diff guard was task-local and could not survive any - # later legitimate full-template task — re-pinned to the invariant it protected.) + # (pre-dates the retirement: the full template NEVER carried the grammar) body = (REPO_ROOT / "add-method" / "tooling" / "templates" / "TASK.md.tmpl").read_text(encoding="utf-8") - self.assertNotIn("skips:", body, "skip machinery must stay fast-lane-only") + self.assertNotIn("skips:", body) # --------------------------------------------------------------------------- @@ -546,47 +532,26 @@ def test_fast_template_trees_byte_identical(self): # --------------------------------------------------------------------------- class RejectPathsTest(_Harness): - def test_malformed_token_refused_as_whole_not_partially_honored(self): + # The advance-time refusals died with the grammar; what survives is (a) the + # resolver still names a truly bad token (TaskSkipSetResolverTest above) and + # (b) NO declaration — malformed, ineligible, or unskippable — can block or + # alter a crossing anymore; the loud surface is the single gate note. + def test_malformed_declaration_cannot_block_a_crossing(self): self._new_fast_task("t", fast=True) self._silent("phase", "verify", "t") self._set_header("t", skips="observe,build") - before = self._state() - out, code = self._run("advance", "t") - self.assertNotEqual(code, 0) - self.assertIn("skip_not_allowed", out) - self.assertEqual(self._state()["tasks"]["t"]["phase"], "verify") - self.assertNotIn("skips", self._state()["tasks"]["t"]) - self.assertEqual(self._state(), before) - - def test_lane_ineligible_declaration_refused(self): - self._silent("lock", "--force") - self._silent("new-milestone", "m", "--goal", "g", "--stage", "mvp") - self._silent("new-task", "t", "--title", "F") # NOT fast/oneshot - self._silent("phase", "verify", "t") - self._set_header("t", skips="observe") - out, code = self._run("advance", "t") - self.assertNotEqual(code, 0) - self.assertIn("skip_lane_required", out) - self.assertEqual(self._state()["tasks"]["t"]["phase"], "verify") - - def test_missing_rationale_refused(self): - self._new_fast_task("t", fast=True) - self._silent("phase", "verify", "t") - self._set_header("t", skips="observe") out, code = self._run("advance", "t") - self.assertNotEqual(code, 0) - self.assertIn("skip_reason_missing", out) - self.assertEqual(self._state()["tasks"]["t"]["phase"], "verify") + self.assertEqual(code, 0, f"the retired grammar must never die at advance: {out}") + self.assertEqual(self._state()["tasks"]["t"]["phase"], "done") self.assertNotIn("skips", self._state()["tasks"]["t"]) - def test_unskippable_phase_name_refused_via_same_code(self): + def test_unskippable_token_noted_at_gate_not_died(self): self._new_fast_task("t", fast=True) - self._silent("phase", "specify", "t") self._set_header("t", skips="build") self._silent("phase", "verify", "t") - out, code = self._run("advance", "t") - self.assertNotEqual(code, 0) - self.assertIn("skip_not_allowed", out) + out = self._silent("gate", "PASS", "t") + self.assertIn("retired", out) + self.assertEqual(self._state()["tasks"]["t"]["phase"], "done") # --------------------------------------------------------------------------- @@ -595,16 +560,15 @@ def test_unskippable_phase_name_refused_via_same_code(self): # --------------------------------------------------------------------------- class FloorCompositionTest(_Harness): - def test_oneshot_security_task_skip_fires_but_freeze_stays_human(self): + def test_oneshot_security_task_freeze_stays_human(self): self._silent("lock", "--force") self._silent("new-milestone", "m", "--goal", "g", "--stage", "mvp") self._silent("new-task", "risky", "--title", "F", "--oneshot") - self._set_header("risky", skips="observe") + self._set_header("risky", skips="observe") # vestigial — must change nothing p = self._task_md("risky") t = p.read_text(encoding="utf-8") t = re.sub(r"(?m)^(autonomy:[^\n]*)$", r"\1\nsensitivity: security", t, count=1) p.write_text(t, encoding="utf-8") - self._set_skip_rationale("risky", "observe — b") self._silent("phase", "specify", "risky") self._silent("advance", "risky") # specify -> plan (the natural crossing now) self.assertEqual(self._state()["tasks"]["risky"]["phase"], "plan") @@ -625,16 +589,15 @@ def test_oneshot_security_task_skip_fires_but_freeze_stays_human(self): out, code = self._run("freeze", "--ai-plan-verify", "--by", "agent:x") self.assertNotEqual(code, 0) self.assertIn("ai_freeze_blocked_sensitivity", out) - # the human path still works, and the earlier skip record is untouched + # the human path still works; downstream verify -> done is the natural + # last crossing now — no skip fires, none is recorded self._silent("freeze", "--by", "A Human") self.assertRegex(self._silent("status", "--section", "3"), r"Status:\s*FROZEN") - # the skip axis composes downstream: verify -> done consumes the observe skip self._silent("phase", "verify", "risky") self._silent("advance", "risky") st = self._state()["tasks"]["risky"] self.assertEqual(st["phase"], "done") - self.assertEqual(len(st["skips"]), 1) - self.assertEqual(st["skips"][0]["phase"], "observe") + self.assertNotIn("skips", st) # --------------------------------------------------------------------------- @@ -642,7 +605,7 @@ def test_oneshot_security_task_skip_fires_but_freeze_stays_human(self): # --------------------------------------------------------------------------- class NormalFlowUnchangedTest(_Harness): - def test_plain_task_visits_scenarios_and_observe_every_crossing(self): + def test_plain_task_visits_every_phase_every_crossing(self): self._silent("lock", "--force") self._silent("new-milestone", "m", "--goal", "g", "--stage", "mvp") self._silent("new-task", "t", "--title", "F") # full-lane, no fast/oneshot; seeds specify @@ -661,10 +624,9 @@ def test_plain_task_visits_scenarios_and_observe_every_crossing(self): self._silent("advance", "t") # plan -> tests self._silent("advance", "t") # tests -> build self._silent("advance", "t") # build -> verify - self._silent("advance", "t") # verify -> observe - self.assertEqual(self._state()["tasks"]["t"]["phase"], "observe") + self.assertEqual(self._state()["tasks"]["t"]["phase"], "verify") self.assertNotIn("skips", self._state()["tasks"]["t"]) - self._silent("advance", "t") # observe -> done + self._silent("advance", "t") # verify -> done (observe merged into verify) self.assertEqual(self._state()["tasks"]["t"]["phase"], "done") self.assertNotIn("skips", self._state()["tasks"]["t"]) diff --git a/add-method/tooling/test_fast_lane_template.py b/add-method/tooling/test_fast_lane_template.py index 64cbf5d8..2928ccf4 100644 --- a/add-method/tooling/test_fast_lane_template.py +++ b/add-method/tooling/test_fast_lane_template.py @@ -34,7 +34,7 @@ KEPT = {1, 3, 4, 5, 6} # the frozen kept-section set (§0 folded into §3 PLAN) DROPPED = {2, 7} # §2 SCENARIOS, §7 OBSERVE (TASK.md SECTION numbers) -DROPPED_IDX = {5} # observe (n=5); §2 SCENARIOS now folds into specify's +DROPPED_IDX = set() # no whole-phase drop left: §2 folds into specify's # body (phase-merge-specify) so its absence is not empty # (plan=1, observe=5; n is the ordinal, not the §-number) @@ -139,7 +139,7 @@ def test_task_phases_renders_without_error(self): (root / "tasks" / "s").mkdir(parents=True) (root / "tasks" / "s" / "TASK.md").write_text(self.rendered, encoding="utf-8") phases = add.task_phases(root, "s") - self.assertEqual(len(phases), 6) # specify..observe always 6 entries now + self.assertEqual(len(phases), 5) # specify..verify always 5 entries now bynum = {p["n"]: p["body"] for p in phases} # keyed by PHASE INDEX (n), not §-number for n in DROPPED_IDX: self.assertEqual(bynum[n], "(empty)") # dropped phases render empty, no crash diff --git a/add-method/tooling/test_fold_nudge.py b/add-method/tooling/test_fold_nudge.py index ad1c6ef4..551723c9 100644 --- a/add-method/tooling/test_fold_nudge.py +++ b/add-method/tooling/test_fold_nudge.py @@ -120,26 +120,26 @@ def test_milestone_done_previews_deltas_and_fold_command(self): self.assertIn("add.py deltas", out, "must still point at the review command") # --- observe transition: suggest folding into PROJECT.md DURING implement ----- - def test_advance_into_observe_suggests_fold(self): - """Advancing a task INTO observe - the phase that captures competency - deltas - suggests folding them into PROJECT.md (add.py fold --task ).""" + def test_gate_completion_suggests_fold(self): + """phase-merge-verify moved the nudge: gate/completion — where the loop's + lessons are captured now (§7 renders under verify) — suggests folding + them into PROJECT.md (add.py fold --task ).""" add.main(["new-task", "a"]) # fresh project is grandfathered-locked - add.main(["phase", "verify", "a"]) # escape hatch to the edge of observe - code, out, _ = _run(["advance", "a"]) # verify -> observe + add.main(["phase", "verify", "a"]) # to the gate + code, out, _ = _run(["gate", "PASS", "a"]) self.assertEqual(code, 0) - self.assertIn("observe", out) self.assertIn("add.py fold --task a", out, - "entering observe must suggest the per-task fold") + "a completing gate must suggest the per-task fold") self.assertIn("PROJECT.md", out, "the suggestion names the foundation it updates") - def test_advance_into_non_observe_silent_on_fold(self): - """The fold suggestion fires ONLY at the observe transition - advancing - into any other phase stays silent on folding (additive, no noise).""" - add.main(["new-task", "a"]) # at ground - code, out, _ = _run(["advance", "a"]) # ground -> specify + def test_advance_crossings_silent_on_fold(self): + """The fold suggestion fires ONLY at gate/completion — advancing + into any phase stays silent on folding (additive, no noise).""" + add.main(["new-task", "a"]) # at specify + code, out, _ = _run(["advance", "a"]) # specify -> plan self.assertEqual(code, 0) self.assertNotIn("add.py fold", out, - "non-observe transitions must not suggest folding") + "advance crossings must not suggest folding") if __name__ == "__main__": diff --git a/add-method/tooling/test_gate_audit.py b/add-method/tooling/test_gate_audit.py index 9fddb4fb..31a6bf9b 100644 --- a/add-method/tooling/test_gate_audit.py +++ b/add-method/tooling/test_gate_audit.py @@ -96,14 +96,16 @@ def _mk_done(self, slug, sec3=GOOD3, sec6=None, gate="PASS"): ]) (self._root() / "tasks" / slug / "TASK.md").write_text(body, encoding="utf-8") - def _mk_ungated(self, slug, sec6=None, phase="observe"): - """A task pushed to done/observe via the admin `phase` override — NO engine - gate, so state.gate stays 'none' — whose §6 we then control byte-exactly. - This is the F13 shape: a §6 verdict the engine never recorded.""" + def _mk_ungated(self, slug, sec6=None): + """A task walked to done WITHOUT a gate (phase-merge-verify: verify -> done is + a bare advance; `gate` is the only recorder) — state.gate stays 'none' — whose + §6 we then control byte-exactly. The F13 shape: a §6 verdict the engine never + recorded.""" buf, err = io.StringIO(), io.StringIO() with redirect_stdout(buf), redirect_stderr(err): add.main(["new-task", slug, "--title", slug]) # active task; gate stays 'none' - add.main(["phase", phase, slug]) # admin override — no gate recorded + add.main(["phase", "verify", slug]) # admin override — no gate recorded + add.main(["advance", slug]) # verify -> done, still ungated body = "\n".join([ f"# TASK: {slug}", "", # a well-formed done task declares its risk + sensitivity level — keeps the board clean diff --git a/add-method/tooling/test_gate_owner_marker.py b/add-method/tooling/test_gate_owner_marker.py index f4ccde28..1e50b6df 100644 --- a/add-method/tooling/test_gate_owner_marker.py +++ b/add-method/tooling/test_gate_owner_marker.py @@ -12,7 +12,7 @@ _driver_marker(stop) -> " [human gate]" if stop else " [you drive]" The phase × autonomy table (only the verify row moves with the dial): - tests/build/observe -> [you drive] (always) + tests/build -> [you drive] (always) specify/plan/done -> [human gate] (always; the plan freeze stays human, run.md:21) verify -> [you drive] under auto, [human gate] under conservative/manual <- exit criterion @@ -42,12 +42,12 @@ "auto": { "specify": " [human gate]", "plan": " [human gate]", "tests": " [you drive]", "build": " [you drive]", - "verify": " [you drive]", "observe": " [you drive]", "done": " [human gate]", + "verify": " [you drive]", "done": " [human gate]", }, "conservative": { "specify": " [human gate]", "plan": " [human gate]", "tests": " [you drive]", "build": " [you drive]", - "verify": " [human gate]", "observe": " [you drive]", "done": " [human gate]", + "verify": " [human gate]", "done": " [human gate]", }, } diff --git a/add-method/tooling/test_ground_phase.py b/add-method/tooling/test_ground_phase.py index 35ac61c6..9fcc78d2 100644 --- a/add-method/tooling/test_ground_phase.py +++ b/add-method/tooling/test_ground_phase.py @@ -109,24 +109,23 @@ def _freeze(self, slug: str): ), encoding="utf-8") def _mk_done(self, slug: str): - """Drive a fresh task specify -> observe -> done (PASS). SIX advances now - (specify -> plan -> tests -> build -> verify -> observe), then - `gate PASS` finalizes to done. (plan-phase-core: ground+contract collapsed - into plan, so the transition count is unchanged from before the rename — - verified empirically against the live engine, not assumed.)""" + """Drive a fresh task specify -> verify -> done (PASS). FOUR advances now + (specify -> plan -> tests -> build -> verify — six-phase-loop merged + scenarios into specify and observe into verify), then `gate PASS` + finalizes to done.""" self._run("new-task", slug, "--title", slug) self._freeze(slug) # freeze-gate-universal: §3 must be FROZEN before plan->tests crossing - for _ in range(6): + for _ in range(4): self._run("advance", slug) self._run("gate", "PASS", slug) assert self._task(slug)["phase"] == "done", "fixture: task did not reach done" # ---- the ladder shape ------------------------------------------------- - def test_phases_has_specify_first_len_7(self): - # new truth (plan-phase-core): ground+contract collapsed into plan; PHASES is 8. + def test_phases_has_specify_first_len_6(self): + # new truth (six-phase-loop): scenarios merged into specify, observe into verify. self.assertEqual(add.PHASES[0], "specify", "specify must be the first phase") self.assertEqual(add.PHASES[-1], "done", "done stays the terminal phase") - self.assertEqual(len(add.PHASES), 7, "scenarios merged into specify: PHASES is now 7") + self.assertEqual(len(add.PHASES), 6, "the merged lifecycle: 5 work phases + done") def test_every_phase_is_owned_plan_is_seam(self): # PHASE_OWNER is fail-closed (unmapped_phase) -> every phase MUST be mapped; @@ -236,8 +235,8 @@ def test_task_phases_render_specify_first(self): phases = add.task_phases(self._root(), "feat") names = [p["phase"] for p in phases] self.assertEqual(names[0], "specify", "the drill-down renders specify first") - self.assertEqual(names[-1], "observe", "the drill-down ends at observe") - self.assertEqual(len(phases), 6, + self.assertEqual(names[-1], "verify", "the drill-down ends at verify (owns §6+§7)") + self.assertEqual(len(phases), 5, "specify..observe is 6 phase blocks now (specify owns §1+§2)") # ---- heading scan captures section 0 ---------------------------------- diff --git a/add-method/tooling/test_machine_state.py b/add-method/tooling/test_machine_state.py index 4502820c..b7f1a30f 100644 --- a/add-method/tooling/test_machine_state.py +++ b/add-method/tooling/test_machine_state.py @@ -97,7 +97,7 @@ def test_owner_map_stops_at_human_and_seam(self): "specify": ("human", True), "plan": ("seam", True), "tests": ("ai", False), "build": ("ai", False), "verify": ("human", True), - "observe": ("ai", False), "done": ("human", True), + "done": ("human", True), } for phase, (owner, stop) in expect.items(): self._task_at(phase) diff --git a/add-method/tooling/test_onboarding_brand.py b/add-method/tooling/test_onboarding_brand.py index 5dedca90..e1dcc2ce 100644 --- a/add-method/tooling/test_onboarding_brand.py +++ b/add-method/tooling/test_onboarding_brand.py @@ -2,7 +2,7 @@ """Red-first tests for onboarding-brand (installer-smarts task 1). Contract FROZEN @ v1: an ADD logo banner + a feature showcase (a value line + the -7-step Specify->Observe loop) render on the INTERACTIVE installer path only, fail-soft +5-step Specify->Verify loop) render on the INTERACTIVE installer path only, fail-soft (any render error falls through to the prompts, never aborts), while the NON-interactive path stays byte-identical to today. Both twins (npm cli.js / pip _installer.py) render the same DECISIONS; pip is condensed (stdlib), npm fuller (clack). @@ -31,8 +31,8 @@ # from the OUTPUT markers below because the plain installer prose already says "build" — # these five are showcase-only words that never appear in normal installer output, so # they cleanly separate the two paths. -LOOP_STEPS = ("Specify", "Plan", "Tests", "Build", "Verify", "Observe") -SHOWCASE_MARKERS = ("Specify", "Plan", "Verify", "Observe") +LOOP_STEPS = ("Specify", "Plan", "Tests", "Build", "Verify") +SHOWCASE_MARKERS = ("Specify", "Plan", "Build", "Verify") def _markers_present(text): @@ -55,10 +55,11 @@ def test_loop_steps_are_the_real_pre_done_phases(self): for s in LOOP_STEPS: self.assertIn(s.lower(), phases, f"showcase step {s!r} must be a real ADD phase (grounded, not invented)") - # exactly the 6 steps before the terminal "done", in order (no separate ground - # or scenarios phase — grounding folded into Plan, scenarios into Specify) - self.assertEqual([s.lower() for s in LOOP_STEPS], phases[0:6], - "the showcase must be the 6 pre-done phases in order") + # exactly the 5 steps before the terminal "done", in order (ground/scenarios/ + # observe all folded: grounding into Plan, scenarios into Specify, observe + # into Verify — six-phase-loop) + self.assertEqual([s.lower() for s in LOOP_STEPS], phases[0:5], + "the showcase must be the 5 pre-done phases in order") # --- the banner is actually wired into BOTH twins (decision-equivalent) ----------- diff --git a/add-method/tooling/test_phase_bundles.py b/add-method/tooling/test_phase_bundles.py index 314cbdc4..14bac96b 100644 --- a/add-method/tooling/test_phase_bundles.py +++ b/add-method/tooling/test_phase_bundles.py @@ -9,12 +9,12 @@ PHASE_GROUPS = { "DIRECTION": ("specify", "scenarios", "plan", "tests"), "BUILD": ("build",), - "VERIFY": ("verify", "observe"), + "VERIFY": ("verify",), } PHASE_AGENT (per-PHASE, 7 keys) = { "specify": "add-design", "scenarios": "add-design", "plan": "add-design", "tests": "add-build", "build": "add-build", - "verify": "add-verify", "observe": "add-verify", + "verify": "add-verify", } `_phase_bundle(phase)` resolves a phase to its bundle name; None for the terminal @@ -84,7 +84,7 @@ def test_tuples_match_frozen_shape(self): self.assertEqual(engine_constants.PHASE_GROUPS["DIRECTION"], ("specify", "plan", "tests")) self.assertEqual(engine_constants.PHASE_GROUPS["BUILD"], ("build",)) - self.assertEqual(engine_constants.PHASE_GROUPS["VERIFY"], ("verify", "observe")) + self.assertEqual(engine_constants.PHASE_GROUPS["VERIFY"], ("verify",)) def test_union_and_pairwise_disjoint(self): groups = engine_constants.PHASE_GROUPS @@ -118,7 +118,6 @@ def test_values_match_shipped_roster_ownership(self): self.assertEqual(pa["tests"], "add-build") self.assertEqual(pa["build"], "add-build") self.assertEqual(pa["verify"], "add-verify") - self.assertEqual(pa["observe"], "add-verify") def test_values_are_roster_slugs_only(self): self.assertTrue(set(engine_constants.PHASE_AGENT.values()) @@ -143,7 +142,6 @@ def test_resolves_build_phase(self): def test_resolves_verify_phases(self): self.assertEqual(engine_predicates._phase_bundle("verify"), "VERIFY") - self.assertEqual(engine_predicates._phase_bundle("observe"), "VERIFY") def test_done_returns_none_no_exception(self): self.assertIsNone(engine_predicates._phase_bundle("done")) @@ -197,7 +195,6 @@ def test_status_prints_bundle_for_non_done_task(self): def test_status_silent_at_done(self): self._set_phase("build") self._set_phase("verify") - self._set_phase("observe") self._set_phase("done") _, out, _ = _run(["status"]) self.assertFalse(any(ln.strip().startswith("bundle") for ln in out.splitlines()), out) @@ -230,7 +227,6 @@ def test_guide_prints_bundle_for_non_done_task(self): def test_guide_silent_at_done(self): self._set_phase("build") self._set_phase("verify") - self._set_phase("observe") self._set_phase("done") _, out, _ = _run(["guide"]) self.assertFalse(any(ln.strip().startswith("bundle") for ln in out.splitlines()), out) @@ -261,7 +257,6 @@ def test_single_task_object_bundle_field(self): def test_bundle_null_at_done(self): self._set_phase("build") self._set_phase("verify") - self._set_phase("observe") self._set_phase("done") _, out, _ = _run(["status", "--json", "--task", "feat"]) data = json.loads(out) @@ -282,7 +277,6 @@ def test_bundle_field_non_done(self): def test_bundle_null_at_done(self): self._set_phase("build") self._set_phase("verify") - self._set_phase("observe") self._set_phase("done") _, out, _ = _run(["guide", "--json"]) data = json.loads(out) @@ -309,7 +303,7 @@ def test_phase_table_has_bundle_column_populated(self): expected_bundle = { "specify": "DIRECTION", "scenarios": "DIRECTION", "plan": "DIRECTION", "tests": "DIRECTION", "build": "BUILD", - "verify": "VERIFY", "observe": "VERIFY", + "verify": "VERIFY", } for phase, bundle in expected_bundle.items(): self.assertIn(phase, rows, f"phase row '{phase}' missing") diff --git a/add-method/tooling/test_phase_detail.py b/add-method/tooling/test_phase_detail.py index aa7e0264..1bb5dcfa 100644 --- a/add-method/tooling/test_phase_detail.py +++ b/add-method/tooling/test_phase_detail.py @@ -122,14 +122,14 @@ def _done_pass(self, slug): # ---- the drill -------------------------------------------------------- def test_drill_renders_seven_phases(self): - # phase-merge-specify: six work phases now (specify owns §1+§2) + # phase-merge-verify: five work phases now (specify owns §1+§2, verify §6+§7) add.main(["phase", "plan", "alpha"]) before = self._hash_state() out, _, code = self._report("vX", "alpha") self.assertEqual(code, 0) names = ["0 SPECIFY", "1 PLAN", "2 TESTS", - "3 BUILD", "4 VERIFY", "5 OBSERVE"] - # all six present, in order + "3 BUILD", "4 VERIFY"] + # all five present, in order positions = [out.find(n) for n in names] self.assertNotIn(-1, positions, "a phase block is missing") self.assertEqual(positions, sorted(positions), "phase blocks out of order") @@ -160,7 +160,7 @@ def test_verify_block_shows_gate_from_state(self): self.assertIn("GATE PASS", out) # verify block sources gate from state self.assertNotIn("PASS", _TASK_MD) # guard: the fixture prose never says PASS self.assertIn("# 4 VERIFY", out) # every block reached (done) - self.assertIn("# 5 OBSERVE", out) + self.assertIn("OBSERVE_MARKER", out) # §7 renders under the verify block def test_unfilled_phase_is_empty(self): out, _, code = self._report("vX", "alpha") @@ -217,8 +217,8 @@ def test_json_dumps_task_phases(self): out, _, code = self._report("vX", "alpha", "--json") self.assertEqual(code, 0) data = _json.loads(out) - self.assertEqual(len(data), 6) - self.assertEqual([d["n"] for d in data], [0, 1, 2, 3, 4, 5]) + self.assertEqual(len(data), 5) + self.assertEqual([d["n"] for d in data], [0, 1, 2, 3, 4]) for d in data: self.assertIn("phase", d) self.assertIn("body", d) @@ -230,18 +230,19 @@ def test_unreadable_file_failclosed(self): from unittest import mock with mock.patch.object(Path, "read_text", side_effect=OSError("boom")): phases = add.task_phases(self._root(), "alpha") - self.assertEqual(len(phases), 6) + self.assertEqual(len(phases), 5) self.assertTrue(all(p["body"] == "(empty)" for p in phases)) def test_task_phases_pure_extraction(self): phases = add.task_phases(self._root(), "alpha") - self.assertEqual(len(phases), 6) + self.assertEqual(len(phases), 5) self.assertEqual([p["phase"] for p in phases], ["specify", "plan", "tests", - "build", "verify", "observe"]) + "build", "verify"]) bodies = {p["n"]: p["body"] for p in phases} self.assertIn("SPEC_MARKER", bodies[0]) self.assertIn("SCEN_MARKER", bodies[0]) # specify owns §2 too + self.assertIn("OBSERVE_MARKER", bodies[4]) # verify owns §7 too self.assertNotIn("a comment that must be stripped", bodies[0]) # HTML comment gone self.assertNotIn("EXIT:", bodies[4]) # EXIT marker gone self.assertEqual(bodies[3], "(empty)") # placeholder-only diff --git a/add-method/tooling/test_phase_merge_specify.py b/add-method/tooling/test_phase_merge_specify.py index 99d14ed2..930bbbaa 100644 --- a/add-method/tooling/test_phase_merge_specify.py +++ b/add-method/tooling/test_phase_merge_specify.py @@ -58,15 +58,16 @@ def _phase(self, slug="t"): class PhaseListTest(_Harness): def test_phases_has_no_scenarios(self): # M1 self.assertNotIn("scenarios", PHASES) - self.assertEqual(PHASES, ("specify", "plan", "tests", "build", "verify", - "observe", "done")) + # (phase-merge-verify later dropped observe too; this suite pins ITS merge — + # scenarios gone, specify first, plan second) + self.assertEqual(PHASES[:2], ("specify", "plan")) def test_maps_carry_no_scenarios_key(self): # M1 for name, mapping in (("PHASE_GUIDE", PHASE_GUIDE), ("PHASE_OWNER", PHASE_OWNER), ("PHASE_AGENT", PHASE_AGENT)): self.assertNotIn("scenarios", mapping, f"{name} still maps the retired phase") self.assertEqual(PHASE_GROUPS["DIRECTION"], ("specify", "plan", "tests")) - self.assertEqual(_SKIPPABLE_PHASES, ("observe",)) + self.assertNotIn("scenarios", _SKIPPABLE_PHASES) def test_specify_guide_action_names_gwt(self): # M1 self.assertIn("Given/When/Then", PHASE_GUIDE["specify"][0], @@ -111,10 +112,10 @@ def _declare_skip(self, token): p.write_text(text, encoding="utf-8") def test_old_skip_declaration_tolerated_loud(self): # M3 + R2 + Boundary - # The note fires at the crossing where the skip machinery reads the header — - # the observe crossing (M13 of fast-lane-skips: non-skippable crossings run - # ZERO skip logic; that frozen placement is preserved, so earlier advances - # sail through silently and nothing ever dies). + # Tolerated-and-ignored, noted LOUD, never a die — the frozen behavior. + # (phase-merge-verify retired the whole grammar and moved the note from the + # observe crossing — which no longer exists — to gate/completion, the one + # seam that still reads the header.) self._board() self._declare_skip("scenarios") out, code = self._run("advance") # specify -> plan, silent @@ -122,20 +123,16 @@ def test_old_skip_declaration_tolerated_loud(self): # M3 + R2 + Bound f"a retired scenarios skip declaration must never die: {out}") self.assertEqual(self._phase(), "plan") self._ok("phase", "verify", "t") - out, code = self._run("advance") # verify -> observe crossing - self.assertEqual(code, 0, f"the observe crossing must tolerate it too: {out}") - self.assertIn("merged into specify", out, - "the ignored declaration must be noted loud at the skip crossing") - self.assertEqual(self._phase(), "observe") - - def test_truly_bad_skip_token_still_dies(self): # R2 (floor held) - self._board() - self._declare_skip("build") # never skippable - self._ok("phase", "verify", "t") - out, code = self._run("advance") # the observe crossing - self.assertNotEqual(code, 0, - "a non-skippable token must keep the existing die at the " - "crossing where a skip could take effect") + out = self._ok("gate", "PASS", "t") + self.assertIn("ignored", out, + "the ignored declaration must be noted loud at the gate seam") + + def test_truly_bad_skip_token_still_refused_by_resolver(self): # R2 (floor held) + # The advance-time die retired with the grammar; the resolver keeps naming + # a truly bad token so no reader ever silently honors one. + toks, err = add._task_skip_set("skips: build\n") + self.assertEqual(toks, frozenset()) + self.assertTrue(err and err.startswith("skip_not_allowed"), err) if __name__ == "__main__": diff --git a/add-method/tooling/test_phase_merge_verify.py b/add-method/tooling/test_phase_merge_verify.py new file mode 100644 index 00000000..003854b8 --- /dev/null +++ b/add-method/tooling/test_phase_merge_verify.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""Red/green tests for phase-merge-verify (six-phase-loop 2/6, frozen v1): the +observe PHASE folds into verify — verify owns evidence + gate AND the watch/ +spec-delta duty; §7 OBSERVE stays as a section (the stable API) rendered under +verify exactly as §2 renders under specify. The skip grammar retires whole: +nothing is skippable, vestigial declarations are noted loud at gate, never a die. +The two residual ordinal->section mappings (advance --fill · status --section) +repair to the _PHASE_SECTIONS table. + +Run: python3 -m unittest test_phase_merge_verify -v +""" +import io +import json +import os +import re +import shutil +import tempfile +import unittest +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path + +import add +from add_engine.constants import (PHASES, _SKIPPABLE_PHASES, PHASE_GUIDE, + PHASE_OWNER, PHASE_GROUPS, PHASE_AGENT) + + +class _Harness(unittest.TestCase): + def setUp(self): + self._cwd = Path.cwd() + self.tmp = Path(tempfile.mkdtemp(prefix="add-pmv-")).resolve() + self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True) + self.addCleanup(os.chdir, self._cwd) + os.chdir(self.tmp) + + def _run(self, *argv): + out, err = io.StringIO(), io.StringIO() + code = 0 + try: + with redirect_stdout(out), redirect_stderr(err): + add.main(list(argv)) + except SystemExit as e: + code = e.code if isinstance(e.code, int) else 1 + return out.getvalue() + err.getvalue(), code + + def _ok(self, *argv): + text, code = self._run(*argv) + self.assertEqual(code, 0, f"{argv} exited {code}: {text}") + return text + + def _board(self): + self._ok("init", "--name", "demo", "--stage", "mvp") + self._ok("lock", "--force") + self._ok("new-task", "t", "--title", "T", "--oneshot") + + def _freeze(self, slug="t"): + p = self.tmp / ".add" / "tasks" / slug / "TASK.md" + p.write_text(p.read_text(encoding="utf-8").replace( + "Status: DRAFT", + "Status: FROZEN @ v1 — approved by Tester 2026-07-14.\n" + "Least-sure flag surfaced at freeze: [contract] fixture stub — cost: none", + ), encoding="utf-8") + + def _phase(self, slug="t"): + state = json.loads((self.tmp / ".add" / "state.json").read_text(encoding="utf-8")) + return state["tasks"][slug]["phase"] + + def _to_verify(self): + """Drive t specify -> verify (4 hops post-merge; freeze first for the tests gate).""" + self._freeze() + for _ in range(4): + self._ok("advance", "t") + self.assertEqual(self._phase(), "verify") + + +class PhaseListTest(_Harness): + def test_phases_has_no_observe(self): # M1 + self.assertNotIn("observe", PHASES) + self.assertEqual(PHASES, ("specify", "plan", "tests", "build", "verify", "done")) + + def test_maps_carry_no_observe_key(self): # M1 + for name, mapping in (("PHASE_GUIDE", PHASE_GUIDE), ("PHASE_OWNER", PHASE_OWNER), + ("PHASE_AGENT", PHASE_AGENT)): + self.assertNotIn("observe", mapping, f"{name} still maps the retired phase") + self.assertEqual(PHASE_GROUPS["VERIFY"], ("verify",)) + self.assertEqual(_SKIPPABLE_PHASES, ()) + + def test_verify_guide_action_names_the_observe_duty(self): # M1 + action = PHASE_GUIDE["verify"][0] + self.assertIn("spec delta", action, + "the merged verify action must carry observe's watch/spec-delta duty") + + +class AdvanceAndGateTest(_Harness): + def test_advance_verify_lands_done(self): # M1 + Accept + self._board() + self._to_verify() + self._ok("advance", "t") + self.assertEqual(self._phase(), "done", + "a bare advance from verify must land at done (no observe stop)") + + def test_gate_pass_at_verify_marks_done_with_fold_nudge(self): # M6 + Accept + self._board() + self._to_verify() + out = self._ok("gate", "PASS", "t") + self.assertEqual(self._phase(), "done") + self.assertIn("fold", out, + "gate completion must carry the fold nudge observe used to print") + + def test_phase_cmd_observe_refused(self): # R1 + self._board() + out, code = self._run("phase", "observe", "t") + self.assertNotEqual(code, 0, "observe is no longer a phase token") + out2, code2 = self._run("advance", "--to", "observe") + self.assertNotEqual(code2, 0) + + +class LegacyStateTest(_Harness): + def test_legacy_observe_state_normalizes(self): # M2 + Boundary + # Normalize-on-read (the frozen §1 wording): a READ shows verify and never + # observe; the next WRITE persists the normalized token — proven by an + # advance that lands done (verify -> done), impossible from a dead token. + self._board() + self._freeze() + sp = self.tmp / ".add" / "state.json" + raw = json.loads(sp.read_text(encoding="utf-8")) + raw["tasks"]["t"]["phase"] = "observe" # a pre-merge record + sp.write_text(json.dumps(raw), encoding="utf-8") + out = self._ok("status") # any state read + self.assertNotIn("phase=observe", out.replace(" ", "")) + self._ok("advance", "t") # verify -> done, persisted + self.assertEqual(self._phase(), "done") + + def test_legacy_observe_with_recorded_gate_stays_recorded(self): # M2 + Boundary + self._board() + self._to_verify() + self._ok("gate", "PASS", "t") + sp = self.tmp / ".add" / "state.json" + raw = json.loads(sp.read_text(encoding="utf-8")) + raw["tasks"]["t"]["phase"] = "observe" # pre-merge post-gate parking + sp.write_text(json.dumps(raw), encoding="utf-8") + self._ok("status") + raw2 = json.loads(sp.read_text(encoding="utf-8")) + self.assertEqual(raw2["tasks"]["t"].get("gate"), "PASS", + "normalizing observe->verify must not drop the recorded gate") + + +class VestigialSkipsTest(_Harness): + def _declare_skip(self, token): + p = self.tmp / ".add" / "tasks" / "t" / "TASK.md" + text = p.read_text(encoding="utf-8") + m = re.search(r"(?m)^phase:.*$", text) + self.assertIsNotNone(m, "no phase marker line to anchor the skip header") + p.write_text(text[:m.end()] + f"\nskips: {token}" + text[m.end():], + encoding="utf-8") + + def test_vestigial_skips_header_noted_at_gate_never_dies(self): # M3 + R2 + Boundary + # The grammar itself is retired: EVERY crossing runs zero skip logic, the + # declaration is read once, at gate/completion, and ignored loud. + self._board() + self._declare_skip("observe") + self._freeze() + for _ in range(4): # every advance silent + out, code = self._run("advance", "t") + self.assertEqual(code, 0, + f"a vestigial skips: header must never die at a crossing: {out}") + out = self._ok("gate", "PASS", "t") + self.assertIn("retired", out, + "the ignored declaration must be noted loud at gate") + self.assertEqual(self._phase(), "done") + + def test_any_token_tolerated_not_just_retired_phases(self): # R2 (grammar retired whole) + self._board() + self._declare_skip("build") # was a die pre-merge + self._freeze() + for _ in range(4): + self._ok("advance", "t") + out = self._ok("gate", "PASS", "t") + self.assertIn("retired", out) + + +class SectionsTableTest(_Harness): + def test_phase_sections_verify_owns_6_and_7(self): # M4 + self.assertEqual(add._PHASE_SECTIONS["verify"], (6, 7)) + self.assertEqual(add._PHASE_SECTIONS["specify"], (1, 2)) + + def test_section_flag_uses_the_sections_table(self): # M5 + self._board() + self._ok("advance", "t") # specify -> plan + out = self._ok("status", "--section", "plan") + self.assertIn("Grounding", out, + "--section plan must print §3 PLAN (the table), not §2 (ordinal math)") + out6 = self._ok("status", "--section", "verify") + self.assertIn("GATE RECORD", out6, + "--section verify must print §6 VERIFY (its primary section)") + + def test_fill_targets_the_primary_section(self): # M5 + # At tests the table says §4; ordinal math would say §3 (index 2 + 1) — the + # discriminating hop. (plan can't host this fixture: --fill is all-or-nothing + # and the plan->tests crossing demands the freeze the fill would clobber.) + self._board() + self._freeze() + self._ok("advance", "t") # specify -> plan + self._ok("advance", "t") # plan -> tests + draft = self.tmp / "d.txt" + draft.write_text("PMV-FILL-MARKER red-suite note", encoding="utf-8") + self._ok("advance", "t", "--fill", str(draft)) # fills §4 then advances + body = (self.tmp / ".add" / "tasks" / "t" / "TASK.md").read_text(encoding="utf-8") + sec4 = body.split("## 4 ·", 1)[1].split("## 5 ·", 1)[0] + self.assertIn("PMV-FILL-MARKER", sec4, + "--fill at tests must write §4 (the table), not §3 (ordinal math)") + + +if __name__ == "__main__": + unittest.main() diff --git a/add-method/tooling/test_plan_phase_flow.py b/add-method/tooling/test_plan_phase_flow.py index c91d4ad6..1ba4f90f 100644 --- a/add-method/tooling/test_plan_phase_flow.py +++ b/add-method/tooling/test_plan_phase_flow.py @@ -52,7 +52,7 @@ class ConstantsShape(unittest.TestCase): def test_phases_expectations_first(self): # M1 self.assertEqual( C.PHASES, - ("specify", "plan", "tests", "build", "verify", "observe", "done"), + ("specify", "plan", "tests", "build", "verify", "done"), ) self.assertNotIn("ground", C.PHASES) self.assertNotIn("contract", C.PHASES) diff --git a/add-method/tooling/test_reopen_transition.py b/add-method/tooling/test_reopen_transition.py index cf833a60..5f8161a9 100644 --- a/add-method/tooling/test_reopen_transition.py +++ b/add-method/tooling/test_reopen_transition.py @@ -132,10 +132,10 @@ def test_reopen_moves_done_task_to_phase(self): "--reason", "wiring check failed post-merge") self.assertEqual(code, 0, f"reopen should succeed; err={err!r}") self.assertEqual(self._task("t")["phase"], "build") - # PHASES tuple keeps "done" as a terminal state (phase-merge-specify shrank - # the list to 7: scenarios merged into specify). + # PHASES tuple keeps "done" as a terminal state (six-phase-loop shrank the + # list to 6: scenarios merged into specify, observe into verify). self.assertEqual(add.PHASES[-1], "done") - self.assertEqual(len(add.PHASES), 7) + self.assertEqual(len(add.PHASES), 6) def test_reopen_resets_gate_to_none(self): self._mk_done("t") diff --git a/add-method/tooling/test_report.py b/add-method/tooling/test_report.py index bde00759..22d6a05c 100644 --- a/add-method/tooling/test_report.py +++ b/add-method/tooling/test_report.py @@ -215,8 +215,8 @@ def test_phase_track_compact(self): self._done_pass("beta") root = add.find_root() out = add.render_report(root, add.load_state(root), "v9") # ascii=False (canonical) - self.assertIn("●●◉○○○○", out) # alpha at 'tests': 2 reached, current, 4 pending - self.assertIn("●●●●●●●", out) # beta done -> whole track reached + self.assertIn("●●◉○○○", out) # alpha at 'tests': 2 reached, current, 3 pending + self.assertIn("●●●●●●", out) # beta done -> whole track reached (6 phases) self.assertIn("PASS", out) # gate word (no glyph badge in column) self.assertIn("legend", out) # one legend, not per-row labels diff --git a/add-method/tooling/test_skip_error_ergonomics.py b/add-method/tooling/test_skip_error_ergonomics.py index 2f8f1d73..ecb463bb 100644 --- a/add-method/tooling/test_skip_error_ergonomics.py +++ b/add-method/tooling/test_skip_error_ergonomics.py @@ -89,21 +89,21 @@ def _write_task(self, slug, *, skips_line, rationale=""): class MalformedSkipsTest(_Board): def test_malformed_skips_names_repair(self): + # phase-merge-verify retired the grammar: no crossing dies on a declaration + # anymore. The ERGONOMIC floor survives in the resolver — its error string + # still names the bad token and echoes the raw declaration for any reader. + err = add._task_skip_set("skips: specify,scenarios\n")[1] + self.assertTrue(err and err.startswith("skip_not_allowed"), err) + self.assertIn("specify", err, "the bad token is named") + self.assertIn("skips: specify,scenarios", err, "the raw declaration is echoed") + # ...and the crossing itself never dies now self._init_project() self._silent("new-task", "t1", "--title", "T") self._write_task("t1", skips_line="skips: specify,scenarios") self._silent("phase", "verify", "t1") - out, err, code = self._run("advance", "t1") # verify -> observe (the skippable - # crossing — phase-merge-specify: scenarios is retired/filtered, `specify` is - # the surviving bad token, and validation lives where a skip could take effect) - blob = out + err - self.assertNotEqual(code, 0, "fail-closed discard unchanged") - self.assertIn("skip_not_allowed", blob, "the code prefix survives") - self.assertIn("specify", blob, "the bad token is named") - self.assertIn("skips: specify,scenarios", blob, "the raw declaration is echoed") - for allowed in sorted(add._SKIPPABLE_PHASES): - self.assertIn(allowed, blob, f"the allowed set names {allowed}") - self.assertIn("remove", blob.lower(), "the fix (correct-or-remove) is stated") + out, err2, code = self._run("advance", "t1") # verify -> done, tolerated + self.assertEqual(code, 0, f"the retired grammar must never die at advance: {out + err2}") + self.assertIn("remove", err.lower(), "the fix (correct-or-remove) is stated") def test_valid_skip_unaffected(self): # UNDECLARED (no skips line) crossing — the universal default stays silent diff --git a/add-method/tooling/test_step_spawn_hint.py b/add-method/tooling/test_step_spawn_hint.py index 7e17202f..9b60f2ae 100644 --- a/add-method/tooling/test_step_spawn_hint.py +++ b/add-method/tooling/test_step_spawn_hint.py @@ -6,7 +6,7 @@ specify:"domain researcher + wide scenario sweep" · plan:"change-plan design sweep" · tests:"red-suite test-author" · build:"independent well-scoped batch" · - verify:"earned-green refute-read" · observe:"lessons-mining reviewer" + verify:"earned-green refute-read" (observe merged into verify) _spawn_hint_line(task: dict, autonomy: str) -> str | None # PURE None if phase ∉ _SPAWN_HINTS OR autonomy == "manual" else f"spawn hint: {phase} → {_SPAWN_HINTS[phase]} (tier: {tier})" @@ -33,7 +33,6 @@ "tests": "red-suite test-author", "build": "independent well-scoped batch", "verify": "earned-green refute-read", - "observe": "lessons-mining reviewer", } From 1cb3914fdf1421f91ca9c5f181e2bef908cb43a9 Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 01:53:24 +0700 Subject: [PATCH 4/9] =?UTF-8?q?feat(method):=20guide-recut=20=E2=80=94=20p?= =?UTF-8?q?hase=20guides=20re-cut=20to=20the=206-phase=20loop=20(six-phase?= =?UTF-8?q?-loop=203/6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill's guide tree follows the engine's merged lifecycle: 1-specify.md absorbs 2-scenarios' load-bearing duties (gherkin Given/When/Then format, one scenario per Must AND per Reject, the And-unchanged clause on every rejection, the edge-case sweep, the exit-gate boxes); 6-verify.md absorbs 7-observe's as a post-gate Observe block (scope-of-impact release, scenarios-as-monitors, the next SPEC delta, the voice-delta -> soul.md route, the persona tag line, the self-improve.md map pointer). The two absorbed files are DELETED from all three guide trees. SKILL.md's phase table shows the 6-row loop (specify row owns §1+§2, verify row §6+§7); 9355 B, under the orient-split 9500 ceiling. soul.md re-routes its Emit line to the verify guide. Pools: phases baseline re-anchored 41605 -> 40205 — the INVERSE of the rebaseline-for-new-surface method: deletion is won ground the ceiling locks in, so the baseline shrinks by NET won bytes ÷ ratio (deleted 3855 B minus the absorbing guides' +2735 B = 1120 net; −⌈1120/0.80⌉=1400), ratio 0.80 kept exactly. test_ground_anchor_sha's baseline floor forward-migrated 40280 -> 40205 with the accounting signed. Tests: NEW test_guide_recut.py (8: deletion ×3 trees · duty markers in both absorbing guides · no live prose names a deleted file · table rows/§-spans · ceiling held). Ripple: 11 failures/10 suites re-aimed (wording-surface census 32→30 in two pins; soul/persona/ADR/xml-census/per-step-hook pins moved onto the absorbing guides; pool lists trimmed). Zero tests weakened. Full fence 3535/3535 OK. Engine untouched — no MD5 re-aim. refs: six-phase-loop milestone, stacked branch feat/six-phase-loop author: Tin Dang --- .add/state.json | 49 +++++++- .add/tasks/guide-recut/TASK.md | 106 ++++++++++++++++++ .claude/skills/add/SKILL.md | 6 +- .claude/skills/add/phases/1-specify.md | 37 +++++- .claude/skills/add/phases/2-scenarios.md | 48 -------- .claude/skills/add/phases/6-verify.md | 21 +++- .claude/skills/add/phases/7-observe.md | 45 -------- .claude/skills/add/soul.md | 2 +- add-method/skill/add/SKILL.md | 6 +- add-method/skill/add/phases/1-specify.md | 37 +++++- add-method/skill/add/phases/2-scenarios.md | 48 -------- add-method/skill/add/phases/6-verify.md | 21 +++- add-method/skill/add/phases/7-observe.md | 45 -------- add-method/skill/add/soul.md | 2 +- .../add_method/_bundled/skill/add/SKILL.md | 6 +- .../_bundled/skill/add/phases/1-specify.md | 37 +++++- .../_bundled/skill/add/phases/2-scenarios.md | 48 -------- .../_bundled/skill/add/phases/6-verify.md | 21 +++- .../_bundled/skill/add/phases/7-observe.md | 45 -------- .../src/add_method/_bundled/skill/add/soul.md | 2 +- add-method/tooling/test_adr_audit.py | 6 +- .../tooling/test_domain_test_mapping.py | 4 +- .../tooling/test_fold_persona_sections.py | 4 +- add-method/tooling/test_ground_anchor_sha.py | 13 ++- add-method/tooling/test_guide_recut.py | 106 ++++++++++++++++++ add-method/tooling/test_per_step_hooks.py | 11 +- add-method/tooling/test_phase_bundles.py | 2 +- add-method/tooling/test_rewrite_guides.py | 2 - .../tooling/test_self_improving_guide.py | 9 +- add-method/tooling/test_skill_lean.py | 16 ++- add-method/tooling/test_soul_self_improve.py | 9 +- add-method/tooling/test_wording_lint.py | 7 +- add-method/tooling/test_xml_convention.py | 6 - 33 files changed, 471 insertions(+), 356 deletions(-) create mode 100644 .add/tasks/guide-recut/TASK.md delete mode 100644 .claude/skills/add/phases/2-scenarios.md delete mode 100644 .claude/skills/add/phases/7-observe.md delete mode 100644 add-method/skill/add/phases/2-scenarios.md delete mode 100644 add-method/skill/add/phases/7-observe.md delete mode 100644 add-method/src/add_method/_bundled/skill/add/phases/2-scenarios.md delete mode 100644 add-method/src/add_method/_bundled/skill/add/phases/7-observe.md create mode 100644 add-method/tooling/test_guide_recut.py diff --git a/.add/state.json b/.add/state.json index c17a6825..8b8fa45c 100644 --- a/.add/state.json +++ b/.add/state.json @@ -1,7 +1,7 @@ { "project": "AIDD / ADD Methodology", "stage": "mvp", - "active_task": "phase-merge-verify", + "active_task": "guide-recut", "tasks": { "add-check": { "title": "add.py check: validate .add project integrity", @@ -9138,10 +9138,53 @@ "email": "tindang.ht97@gmail.com", "source": "git" } + }, + "guide-recut": { + "title": "Re-cut phase guides to the 6-phase loop", + "phase": "done", + "gate": "PASS", + "milestone": "six-phase-loop", + "depends_on": [], + "created": "2026-07-13T18:32:30+00:00", + "updated": "2026-07-13T18:52:57+00:00", + "fast": true, + "freeze": { + "version": "v1", + "frozen_at": "2026-07-13T18:34:45+00:00", + "approved_by": "Tin Dang", + "actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } + }, + "flag_verified": true, + "tripwire": { + "contract_md5": "bef4bee95ec727cf39f11673e11b0d02", + "tests": { + "add-method/tooling/test_guide_recut.py": "5dec75052622d2c4b0c3588c04fffd3e" + } + }, + "scope": { + "declared": [ + "add-method/skill/", + "add-method/src/add_method/_bundled/skill/", + ".claude/skills/add/", + "add-method/tooling/", + ".add/tooling/", + "add-method/.add/" + ], + "snapshot_md5": "c0c1ba7288177c635b46b15eadcf8962" + }, + "gate_actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } } }, "created": "2026-05-28T15:39:04+00:00", - "updated": "2026-07-13T18:31:44+00:00", + "updated": "2026-07-13T18:52:57+00:00", "milestones": { "v1-1": { "title": "v1.1 \u2014 adoption & ergonomics", @@ -10403,7 +10446,7 @@ "plan-legibility": "relations-surface", "ceremony-to-effort": "fold-draft-at-close", "call-floor": "skill-orient-split", - "six-phase-loop": "phase-merge-verify" + "six-phase-loop": "guide-recut" }, "todos": [ { diff --git a/.add/tasks/guide-recut/TASK.md b/.add/tasks/guide-recut/TASK.md new file mode 100644 index 00000000..c53bd70d --- /dev/null +++ b/.add/tasks/guide-recut/TASK.md @@ -0,0 +1,106 @@ +# TASK: Re-cut phase guides to the 6-phase loop + +slug: guide-recut · created: 2026-07-14 · stage: mvp +milestone: six-phase-loop +autonomy: auto +phase: done +fast: true + +> Fast lane — one small task, minimal sections, filled top-to-bottom. The trust floor still +> holds: a FROZEN §3 contract · ≥1 red test before build · a recorded §6 gate (security = HARD-STOP). +> The acceptance scenario collapses into §1 `Accept:`; the observe note is one optional line at the gate. + +--- + +## 1 · SPECIFY — the rules + +> Project the expectations from the milestone Ground + this request — light, not re-invented. +Feature: guide-recut — the skill's phase-guide tree re-cuts to the merged 6-phase loop (six-phase-loop 3/6): 1-specify absorbs 2-scenarios' duties, 6-verify absorbs 7-observe's; the two absorbed files are DELETED; SKILL.md's phase table shows 6 rows; every doc-truth pin follows +Must: + - phases/1-specify.md gains a Scenarios (§2) block carrying 2-scenarios' load-bearing duties (the Given/When/Then output format · one scenario per Must AND per Reject · the And-unchanged clause on every rejection · the edge-case sweep) and its Exit gate absorbs the scenario boxes; its Next pointer routes to 3-plan.md + - phases/6-verify.md gains a post-gate Observe block carrying 7-observe's duties (scope-of-impact release · scenarios-as-monitors · the next SPEC delta · the voice delta / soul.md route · the persona-tag line); its Next stays the loop + - phases/2-scenarios.md and phases/7-observe.md are DELETED from all three guide trees (canonical skill/add · _bundled · .claude/skills); no engine map references them (phase-merge tasks 1-2 already dropped the keys) + - SKILL.md's phase table drops the scenarios and observe rows (6 rows incl setup); the specify row produces §1+§2, the verify row §6+§7; synced ×3 + - test_skill_lean's phases POOL + PHASE_GUIDES drop the two files (baseline shrinks by the two files' won bytes — a DELETION is not new surface; ratio untouched); every suite referencing the two filenames re-aims (fence-named, declared ripples) +Reject: + - a stale `phases/2-scenarios.md` / `phases/7-observe.md` reference anywhere in the live skill tree or SKILL.md -> the doc-truth grep-guard test goes red + - a guide duty dropped in the merge (GWT format · And-unchanged · monitors · spec delta · voice delta) -> the marker-pin test goes red +Accept: Given the recut tree, When a reader follows SKILL.md's phase table at specify or verify, Then the one guide it names carries BOTH merged phases' duties and no pointer names a deleted file. +Boundary: none — no external input (a prose+test recut; the engine is untouched) +Assumptions: ⚠ the phases pool has no LOWER bound so deleting two guides passes the byte fence — why: POOLS asserts actual ≤ baseline×ratio (a ceiling); if wrong (a floor exists or TREE_BASELINE derivation breaks): rebaseline the pool by the deleted bytes with the M6-style signed comment (cost: one fence round) + +--- + +## 3 · PLAN — the change plan: ground · contract · build-strategy + +### Grounding +Touches (files · symbols): add-method/skill/add/phases/1-specify.md + 2-scenarios.md (absorb+delete) · 6-verify.md + 7-observe.md (absorb+delete) · add-method/skill/add/SKILL.md:phase-table · the two twin guide trees (src/add_method/_bundled/skill/add · .claude/skills/add) · add-method/tooling/test_skill_lean.py:POOLS/PHASE_GUIDES · ~19 suites naming the two filenames (fence-named ripples) +Context (working folder): guide trees sync ×3 (NOT the tooling ×3 — skill trees); SKILL.md byte ceiling 9500 (test_skill_orient_split) binds any table edit; engine untouched (no MD5 re-aim) +Honors (patterns / conventions): pools-are-ceilings + M6 signed-rebaseline method · doc-truth grep-guards · pinned-phrase census (agent-call-preferred · default execution mode · persona loop etc. stay) · sections never renumber (§2/§7 headings stay in TASK templates) +Anchors the contract cites: phases/1-specify.md · phases/6-verify.md · SKILL.md phase table · POOLS["phases"] · PHASE_GUIDES +Ground SHA: 8467b46 — stamped by freeze + +### Contract + +``` +phases/1-specify.md: + "## Scenarios (§2)" block — output_format (gherkin GWT, + And-unchanged REQUIRED per rejection) · one per Must AND per Reject · edge sweep + (boundary/duplicate/partial failure/concurrency/malformed) · exit gate gains the + scenario boxes · Next -> phases/3-plan.md +phases/6-verify.md: + "## Observe (post-gate, §7)" block — scope-of-impact release · + scenarios-as-monitors · next SPEC delta (re-enters at Specify) · voice delta -> + soul.md · persona tag line · ADR-harvest note (already at gate) · Next: the loop +phases/2-scenarios.md · phases/7-observe.md: DELETED x3 trees +SKILL.md phase table: 6 rows — specify row "§1 rules + §2 Given/When/Then"; + verify row "§6 checks + gate record + §7 spec delta"; scenarios/observe rows gone; + <= 9500 B held; synced x3 +test_skill_lean: POOLS["phases"].guides drops the 2 files, baseline shrinks by their + live bytes (signed comment; deletion != new surface); PHASE_GUIDES 6 entries +NEW test_guide_recut.py pins: files absent x3 · duty markers present in the two + absorbing guides · SKILL.md names no deleted file · table row count +``` + +`Least-sure flag surfaced at freeze:` [test] the 19-suite filename ripple — why: most name the files in fixtures/comments and the exact usage shape is unknown until the fence names each red line; if wrong (a suite pins the CONTENT of a deleted guide, not just its name): move that content assertion onto the absorbing guide (cost: fence rounds) +Status: FROZEN @ v1 — approved by Tin Dang +### Build-strategy +Scope (may touch): `add-method/skill/` `add-method/src/add_method/_bundled/skill/` `.claude/skills/add/` `add-method/tooling/` `.add/tooling/` `add-method/.add/` +Strategy & known-problem fixes: red test_guide_recut -> absorb into 1-specify -> absorb into 6-verify -> delete x2 files x3 trees -> SKILL.md table recut under the 9500 ceiling -> POOLS/PHASE_GUIDES -> fence -> per-suite filename re-aims. Traps: SKILL.md pinned-phrase census is LONG (trim NOTHING pinned) · pools measure the CANONICAL tree (skill/add) · .add/docs is gitignored (skip) · dogfood twins exists()-skip · the wording-lint bans slang in new prose. +Approach (domain strategy): merge duties not prose — carry each load-bearing marker, drop ceremony; deletion shrinks the read path (the milestone's point) + +### AI-verify record (required when gate_mode: ai-plan-verify) +- [x] §3 PLAN grounding anchors resolve in the current tree (both source guides + SKILL.md table + POOLS block read this session) +- [x] §1 every Must + every Reject present, each paired with its outcome +- [x] §3 Contract shape is concrete (no template placeholder text remains) +- [x] Lowest-confidence flag surfaced and substantive (the 19-suite filename-ripple shape risk) +Verified by: claude-fable-5 (orchestrator, inline) · at: 2026-07-14T02:05:00Z + +--- + +## 4 · TESTS — failing-first (red) + +Plan: test_absorbed_files_deleted_x3 · test_specify_guide_carries_scenario_duties (GWT format · And-unchanged · edge sweep · per-Must-and-Reject) · test_verify_guide_carries_observe_duties (monitors · spec delta · voice delta · scope-of-impact) · test_skill_table_six_rows_no_dead_pointer · test_next_pointers_route_around_deletions. +Tests live in: `add-method/tooling/test_guide_recut.py` · MUST run red before Build. + +--- + +## 5 · BUILD — AI writes the code (execution) + +> The change plan was frozen in §3 PLAN. Build to it: honor the §3 Build-strategy Scope; improve on the strategy if the code teaches you better. +Strategy actually used: as planned, plus three fence-taught corrections: (1) the phases pool baseline shrinks by NET won ground (deleted 3855 B minus the absorbing guides' +2735 B growth = 1120 B), not by raw deleted bytes — the first shrink math was too aggressive; (2) test_ground_anchor_sha holds a baseline FLOOR pin (40280) — forward-migrated to 40205 with a signed comment (the 'only ever grows' invariant becomes 'moves only by signed rebaselines'); (3) the per-step Advisor·Confidence hook must be ONE physical line (test_per_step_hooks reads the marker line only) — the merged specify hook re-flowed. The 6-verify Observe block also had to carry self-improve.md (the map pointer test_self_improving_guide pins). +Code lives in: `./src/` · Constraints: change no test, no frozen contract; stay inside the §3 Build-strategy Scope; allow-list packages only. + +--- + +## 6 · VERIFY — evidence + gate + +- [x] all tests pass · coverage held · no test or contract altered during build +- [x] green was EARNED — no overfit / vacuous asserts / stubbed-away logic +- [x] input dialect held — tests speak the spec's example formats (spec-dialect floor) +- [x] no exposed secrets, injection openings, or unexpected dependencies (security = HARD-STOP) + +Build expectations (from §1 Accept + §3 CONTRACT): 2-scenarios.md + 7-observe.md deleted from all three guide trees; 1-specify.md carries the GWT format/And-unchanged/edge-sweep/per-Must-and-Reject duties; 6-verify.md carries the post-gate Observe block (scope-of-impact · monitors · spec delta · voice delta · self-improve.md map); SKILL.md table = 6 rows, 9355 B ≤ 9500 ceiling, no dead pointer anywhere in the live tree — confirmed by test_guide_recut 8/8 (6 red first) + the FULL fence 3535 tests OK / REAL_EXIT=0 (fence-recut-r2.log; 11-failure ripple over 10 suites repaired: pools re-anchored 41605→40205 by signed NET-won accounting, wording-surface census 32→30, soul/persona/ADR/xml/hook pins re-aimed to the absorbing guides; zero weakened). Engine untouched (no MD5 re-aim). + +### GATE RECORD +Outcome: PASS +Reviewed by: Tin Dang · date: 2026-07-14 + diff --git a/.claude/skills/add/SKILL.md b/.claude/skills/add/SKILL.md index 70b894a0..086a1d05 100644 --- a/.claude/skills/add/SKILL.md +++ b/.claude/skills/add/SKILL.md @@ -82,13 +82,11 @@ Load **only the phase you are in** (progressive disclosure): | Phase | Guide | Produces (TASK.md section) | Who leads | Bundle | |-------|-------|----------------------------|-----------|--------| | setup | `phases/0-setup.md` | `.add/` + living docs + first §1–§3 + `SETUP-REVIEW.md` | AI drafts → **human locks** (the baseline approval) | – | -| specify | `phases/1-specify.md` | §1 rules + ranked lowest-confidence flag | AI drafts (co-specify)† | DIRECTION | -| scenarios | `phases/2-scenarios.md` | §2 Given/When/Then | AI drafts† | DIRECTION | +| specify | `phases/1-specify.md` | §1 rules + ranked flag + §2 Given/When/Then | AI drafts (co-specify)† | DIRECTION | | plan | `phases/3-plan.md` | §3 grounding + frozen shape + build-strategy | AI drafts → **human approves once** (the decision point)† | DIRECTION | | tests | `phases/4-tests.md` | §4 + red suite in `tests/` | AI drafts† | DIRECTION | | build | `phases/5-build.md` | code in `src/`, tests green | **AI** | BUILD | -| verify | `phases/6-verify.md` | §6 checks + gate record | **AI auto-gates on evidence**; human on residue/security‡ | VERIFY | -| observe | `phases/7-observe.md` | §7 spec delta | human + AI | VERIFY | +| verify | `phases/6-verify.md` | §6 checks + gate record + §7 spec delta | **AI auto-gates on evidence**; human on residue/security‡ | VERIFY | † **The specification bundle (v7).** §1–§4 are one bundle; the human gives **one approval at the contract freeze**, lowest-confidence-first — `run.md`. diff --git a/.claude/skills/add/phases/1-specify.md b/.claude/skills/add/phases/1-specify.md index a85ce3c9..d8b8f424 100644 --- a/.claude/skills/add/phases/1-specify.md +++ b/.claude/skills/add/phases/1-specify.md @@ -1,7 +1,9 @@ -# Phase 1 — Specify (the rules) +# Phase 1 — Specify (the rules + the scenarios) Goal: state what the feature MUST do and what it must REJECT, with zero ambiguity -for the AI to resolve by guessing. Fill **§1 SPECIFY** in TASK.md. +for the AI to resolve by guessing — then make every rule concrete as a pass/fail +scenario. Fill **§1 SPECIFY** and **§2 SCENARIOS** in TASK.md (one drafting phase +owns both since the six-phase merge). Specify is **co-specification**: brainstorm the shape WITH the user, draft, then validate. If you cannot write the spec, you don't yet understand the feature — stop and ask. @@ -23,6 +25,27 @@ Specify is **co-specification**: brainstorm the shape WITH the user, draft, then - **Assumptions — lowest-confidence first** — ranked most-likely-wrong → least. The top 1–2 carry a `⚠` flag: `⚠ — lowest confidence because ; if wrong: `. Keep the ranking visible — a flat list of equal `[x]` ticks gets approved without reading. +## Scenarios (§2) — every rule made checkable + +Rewrite each rule as a Given/When/Then that people can read and machines can check: + + +```gherkin +Scenario: + Given + When + Then + And # REQUIRED for every rejection +``` + + +Write **one scenario per Must** and **one per Reject**. The `And ... unchanged` +clause catches corrupting partial failures (a balance deducted before a check +fails) — include it on every rejection. Then sweep for the edge cases the spec +omits — boundary, duplicate, partial failure, concurrency, malformed input — add +one per case that applies, or rule it out on purpose. Never settle for a vague +result ("then it works"): every Then is a specific, observable fact. + ## The lowest-confidence flag is bundle-wide The single approval is at the contract freeze, over the whole bundle — so your §1 ranking feeds the bundle-level flag the user reads there (`run.md`): *"of all I'm asking you to freeze, these 1–2 are most likely wrong."* @@ -38,7 +61,9 @@ Steps: 2. Produce §1 — Framings weighed, every Must, every Reject with a named error code, the After state, and the Assumptions RANKED lowest-confidence first. 3. Flag the 1–2 where your confidence is lowest, each with why + cost. -Never: resolve an ambiguity by guessing. + 4. Produce §2 — one scenario per Must and per Reject (And-unchanged on every + rejection), then the edge-case sweep. +Never: resolve an ambiguity by guessing; never a vague result — specific and observable. ## Exit gate @@ -48,14 +73,16 @@ Never: resolve an ambiguity by guessing. - [ ] Every rejection has a named error code; success state-change described. - [ ] Assumptions ordered lowest-confidence first; the 1–2 `⚠` flags carry why + cost — or an honest "none material" that still names the single biggest risk (never a blank "none"). +- [ ] §2: one scenario per Must and per Reject; every rejection asserts what stays unchanged. +- [ ] §2: edge cases covered (boundary · duplicate · partial failure · …) or ruled out on purpose. > **Persona** — load the fit `.add/personas/.md`; its `## Critical Rules` shape §1 (advisory; never lowers a gate). -> **Advisor · Confidence** — for an unfamiliar domain, spawn a researcher (advisor.md); self-score the spec and let the lowest dimension aim your ⚠ flag (confidence.md). +> **Advisor · Confidence** — for an unfamiliar domain spawn a researcher, for a large surface delegate a wide scenario sweep (advisor.md); self-score the spec — the lowest dimension aims your ⚠ flag, a missing edge case surfaces in the Edge-cases score first (confidence.md). ## Next -`python3 .add/tooling/add.py advance` → read `phases/2-scenarios.md`. +`python3 .add/tooling/add.py advance` → read `phases/3-plan.md`. Book: `docs/03-step-1-specify.md`. (UI feature? also sketch flows + every screen state: loading/empty/error/success; name it in the parent MILESTONE.md's Scope-hint vocabulary, not generic prose.) diff --git a/.claude/skills/add/phases/2-scenarios.md b/.claude/skills/add/phases/2-scenarios.md deleted file mode 100644 index 8d5f4397..00000000 --- a/.claude/skills/add/phases/2-scenarios.md +++ /dev/null @@ -1,48 +0,0 @@ -# Phase 2 — Scenarios (pass/fail cases) - -Goal: rewrite each rule as a concrete Given/When/Then that is readable by people and checkable by machines. Fill **§2 SCENARIOS** in TASK.md. - -## Produce (in TASK.md §2) - - -```gherkin -Scenario: - Given - When - Then - And # REQUIRED for every rejection -``` - -The `And ... unchanged` clause catches corrupting partial failures (e.g. a balance -deducted before a check fails). Include it on every rejection. - - -## AI prompt - - -Role: a specification tester. -Read first: §1 · GLOSSARY. -Objective: one scenario per Must and per Reject rule, each result specific and observable. -Steps: - 1. Write one scenario per Must rule and one per Reject rule. - 2. For every rejection add an And-clause asserting what must NOT change. - 3. Sweep for the edge cases the spec omits — boundary, duplicate, partial failure, concurrency, malformed input — add one per case that applies, or rule it out on purpose. -Never: settle for a vague result ("then it works") — results must be specific and observable. - - -## Exit gate - - -- [ ] One scenario per Must rule. -- [ ] One scenario per Reject rule. -- [ ] Edge cases the spec omits are covered (boundary · duplicate · partial failure · …) or ruled out on purpose. -- [ ] Each result is a specific, observable fact. -- [ ] Every rejection asserts what stays unchanged. - - -> **Advisor · Confidence** — delegate a wide scenario sweep when the surface is large (advisor.md); score Edge cases honestly — a missing case surfaces there first (confidence.md). - -## Next - -`python3 .add/tooling/add.py advance` → read `phases/3-plan.md`. -Book: `docs/04-step-2-scenarios.md`. diff --git a/.claude/skills/add/phases/6-verify.md b/.claude/skills/add/phases/6-verify.md index e62541a9..98ca3a76 100644 --- a/.claude/skills/add/phases/6-verify.md +++ b/.claude/skills/add/phases/6-verify.md @@ -70,4 +70,23 @@ and fast-lane tasks use the compact form — banner → SUMMARY → EVIDENCE → python3 .add/tooling/add.py gate PASS # marks the task done # or: add.py gate RISK-ACCEPTED | add.py gate HARD-STOP (return to Build) ``` -Then read `phases/7-observe.md`. Book: `docs/08-step-6-verify.md`. + +## Observe (post-gate, §7) — feed the next loop + +Verify owns the loop's tail since the six-phase merge. After the gate, fill §7: + +1. **Release behind a scope-of-impact limit** — a flag and/or gradual rollout. +2. **Reuse scenarios as monitors** — the §2 scenarios that defined "correct" define + what you alert on: overall error rate, each rejection's rate, latency of the risky op. +3. **Draft the next spec delta** — every defect, surprise, or new need becomes a change + that re-enters the flow at Specify (a new task). Emit lessons tagged by the + competency they improve (`deltas.md`); the human consolidates (`fold.md`). +4. **Propose a voice delta** — where your voice diverged from the human's, propose a + confirmable voice delta tuning `SOUL.md`, emitted `open` (`soul.md` — the human is + the only writer). Never auto-roll-back — recommend; a human owns production. + +> **Decisions (ADR)** — the gate already harvested §7's ADR block; `add.py audit` flags one never harvested. +> **Persona** — tag `· persona: · critical-rule|success-metric|anti-pattern|ability`; +> `fold` grows that section — a HOW-an-agent-behaves lesson belongs in a persona, not the shared pile. + +Loop — the artifacts are living docs the next cycle refines. Map: `self-improve.md` · book: `docs/08-step-6-verify.md` · `docs/09-the-loop.md`. diff --git a/.claude/skills/add/phases/7-observe.md b/.claude/skills/add/phases/7-observe.md deleted file mode 100644 index 1fe58aec..00000000 --- a/.claude/skills/add/phases/7-observe.md +++ /dev/null @@ -1,45 +0,0 @@ -# Phase 7 — Observe (feed the next loop) - -Goal: release deliberately, watch reality, turn what you learn into the next spec. Fill **§7** in TASK.md. - -## Do - -1. **Release behind a scope-of-impact limit** — a flag and/or gradual rollout -2. **Reuse scenarios as monitors** — the §2 scenarios that defined "correct" now - define what you alert on: overall error rate, each rejection's rate (a spike is a - signal), latency of the risky op. -3. **Draft the next spec delta** — every defect, surprise, or new need becomes a - change that re-enters the flow at Specify (a new task). -4. **Propose a voice delta** — note where your voice diverged from the human's (wordings + - flow); propose a confirmable **voice delta** tuning `SOUL.md`, emitted `open`. The human - confirms, then you rewrite the routed section. Read `soul.md` (grammar, routing, - human-is-only-writer). - -> **Decisions (ADR)** — at the gate the engine harvests §7's ADR block (§1/§3/§5/§6); `add.py audit` flags one never harvested. - -## AI prompt - - -Role: a reliability analyst feeding the next cycle. -Read first: telemetry · objectives · incidents. -Steps: 1. report error-budget burn · 2. cluster errors, surface the top real failures · - 3. draft a SPEC delta with evidence links. -Never: auto-roll-back — recommend; a human owns the production decision. - - -## Exit gate - - -- [ ] Released behind a flag/rollout. -- [ ] Scenario-based monitors live. -- [ ] A reviewed spec delta captured (becomes the next `new-task`). - - -> **Persona** — tag `· persona: · critical-rule|success-metric|anti-pattern|ability`; -> `fold` grows that section — a HOW-an-agent-behaves lesson belongs in a persona, not the shared pile. -> **Advisor · Confidence** — spawn a reviewer to mine the run for lessons (advisor.md); score Self-evaluation — did this loop teach the foundation? (confidence.md). - -## Next - -Loop — the artifacts are living docs the next cycle refines. -Map: `self-improve.md` · book: `docs/09-the-loop.md` diff --git a/.claude/skills/add/soul.md b/.claude/skills/add/soul.md index dbeb1ed0..e378528c 100644 --- a/.claude/skills/add/soul.md +++ b/.claude/skills/add/soul.md @@ -53,6 +53,6 @@ The rewrite is surgical: refine or append the bullet the delta names; never sile ## Where it plugs in -- **Emit**: `phases/7-observe.md` proposes voice deltas beside competency/spec deltas. +- **Emit**: `6-verify.md`'s Observe block proposes voice deltas beside competency/spec deltas. - **Target**: `SOUL.md` — "## Voice deltas" ledger holds confirmed history. - **Kin**: `deltas.md` + `fold.md` (same propose→confirm→write discipline). No `add.py` command writes the voice. diff --git a/add-method/skill/add/SKILL.md b/add-method/skill/add/SKILL.md index 70b894a0..086a1d05 100644 --- a/add-method/skill/add/SKILL.md +++ b/add-method/skill/add/SKILL.md @@ -82,13 +82,11 @@ Load **only the phase you are in** (progressive disclosure): | Phase | Guide | Produces (TASK.md section) | Who leads | Bundle | |-------|-------|----------------------------|-----------|--------| | setup | `phases/0-setup.md` | `.add/` + living docs + first §1–§3 + `SETUP-REVIEW.md` | AI drafts → **human locks** (the baseline approval) | – | -| specify | `phases/1-specify.md` | §1 rules + ranked lowest-confidence flag | AI drafts (co-specify)† | DIRECTION | -| scenarios | `phases/2-scenarios.md` | §2 Given/When/Then | AI drafts† | DIRECTION | +| specify | `phases/1-specify.md` | §1 rules + ranked flag + §2 Given/When/Then | AI drafts (co-specify)† | DIRECTION | | plan | `phases/3-plan.md` | §3 grounding + frozen shape + build-strategy | AI drafts → **human approves once** (the decision point)† | DIRECTION | | tests | `phases/4-tests.md` | §4 + red suite in `tests/` | AI drafts† | DIRECTION | | build | `phases/5-build.md` | code in `src/`, tests green | **AI** | BUILD | -| verify | `phases/6-verify.md` | §6 checks + gate record | **AI auto-gates on evidence**; human on residue/security‡ | VERIFY | -| observe | `phases/7-observe.md` | §7 spec delta | human + AI | VERIFY | +| verify | `phases/6-verify.md` | §6 checks + gate record + §7 spec delta | **AI auto-gates on evidence**; human on residue/security‡ | VERIFY | † **The specification bundle (v7).** §1–§4 are one bundle; the human gives **one approval at the contract freeze**, lowest-confidence-first — `run.md`. diff --git a/add-method/skill/add/phases/1-specify.md b/add-method/skill/add/phases/1-specify.md index a85ce3c9..d8b8f424 100644 --- a/add-method/skill/add/phases/1-specify.md +++ b/add-method/skill/add/phases/1-specify.md @@ -1,7 +1,9 @@ -# Phase 1 — Specify (the rules) +# Phase 1 — Specify (the rules + the scenarios) Goal: state what the feature MUST do and what it must REJECT, with zero ambiguity -for the AI to resolve by guessing. Fill **§1 SPECIFY** in TASK.md. +for the AI to resolve by guessing — then make every rule concrete as a pass/fail +scenario. Fill **§1 SPECIFY** and **§2 SCENARIOS** in TASK.md (one drafting phase +owns both since the six-phase merge). Specify is **co-specification**: brainstorm the shape WITH the user, draft, then validate. If you cannot write the spec, you don't yet understand the feature — stop and ask. @@ -23,6 +25,27 @@ Specify is **co-specification**: brainstorm the shape WITH the user, draft, then - **Assumptions — lowest-confidence first** — ranked most-likely-wrong → least. The top 1–2 carry a `⚠` flag: `⚠ — lowest confidence because ; if wrong: `. Keep the ranking visible — a flat list of equal `[x]` ticks gets approved without reading. +## Scenarios (§2) — every rule made checkable + +Rewrite each rule as a Given/When/Then that people can read and machines can check: + + +```gherkin +Scenario: + Given + When + Then + And # REQUIRED for every rejection +``` + + +Write **one scenario per Must** and **one per Reject**. The `And ... unchanged` +clause catches corrupting partial failures (a balance deducted before a check +fails) — include it on every rejection. Then sweep for the edge cases the spec +omits — boundary, duplicate, partial failure, concurrency, malformed input — add +one per case that applies, or rule it out on purpose. Never settle for a vague +result ("then it works"): every Then is a specific, observable fact. + ## The lowest-confidence flag is bundle-wide The single approval is at the contract freeze, over the whole bundle — so your §1 ranking feeds the bundle-level flag the user reads there (`run.md`): *"of all I'm asking you to freeze, these 1–2 are most likely wrong."* @@ -38,7 +61,9 @@ Steps: 2. Produce §1 — Framings weighed, every Must, every Reject with a named error code, the After state, and the Assumptions RANKED lowest-confidence first. 3. Flag the 1–2 where your confidence is lowest, each with why + cost. -Never: resolve an ambiguity by guessing. + 4. Produce §2 — one scenario per Must and per Reject (And-unchanged on every + rejection), then the edge-case sweep. +Never: resolve an ambiguity by guessing; never a vague result — specific and observable. ## Exit gate @@ -48,14 +73,16 @@ Never: resolve an ambiguity by guessing. - [ ] Every rejection has a named error code; success state-change described. - [ ] Assumptions ordered lowest-confidence first; the 1–2 `⚠` flags carry why + cost — or an honest "none material" that still names the single biggest risk (never a blank "none"). +- [ ] §2: one scenario per Must and per Reject; every rejection asserts what stays unchanged. +- [ ] §2: edge cases covered (boundary · duplicate · partial failure · …) or ruled out on purpose. > **Persona** — load the fit `.add/personas/.md`; its `## Critical Rules` shape §1 (advisory; never lowers a gate). -> **Advisor · Confidence** — for an unfamiliar domain, spawn a researcher (advisor.md); self-score the spec and let the lowest dimension aim your ⚠ flag (confidence.md). +> **Advisor · Confidence** — for an unfamiliar domain spawn a researcher, for a large surface delegate a wide scenario sweep (advisor.md); self-score the spec — the lowest dimension aims your ⚠ flag, a missing edge case surfaces in the Edge-cases score first (confidence.md). ## Next -`python3 .add/tooling/add.py advance` → read `phases/2-scenarios.md`. +`python3 .add/tooling/add.py advance` → read `phases/3-plan.md`. Book: `docs/03-step-1-specify.md`. (UI feature? also sketch flows + every screen state: loading/empty/error/success; name it in the parent MILESTONE.md's Scope-hint vocabulary, not generic prose.) diff --git a/add-method/skill/add/phases/2-scenarios.md b/add-method/skill/add/phases/2-scenarios.md deleted file mode 100644 index 8d5f4397..00000000 --- a/add-method/skill/add/phases/2-scenarios.md +++ /dev/null @@ -1,48 +0,0 @@ -# Phase 2 — Scenarios (pass/fail cases) - -Goal: rewrite each rule as a concrete Given/When/Then that is readable by people and checkable by machines. Fill **§2 SCENARIOS** in TASK.md. - -## Produce (in TASK.md §2) - - -```gherkin -Scenario: - Given - When - Then - And # REQUIRED for every rejection -``` - -The `And ... unchanged` clause catches corrupting partial failures (e.g. a balance -deducted before a check fails). Include it on every rejection. - - -## AI prompt - - -Role: a specification tester. -Read first: §1 · GLOSSARY. -Objective: one scenario per Must and per Reject rule, each result specific and observable. -Steps: - 1. Write one scenario per Must rule and one per Reject rule. - 2. For every rejection add an And-clause asserting what must NOT change. - 3. Sweep for the edge cases the spec omits — boundary, duplicate, partial failure, concurrency, malformed input — add one per case that applies, or rule it out on purpose. -Never: settle for a vague result ("then it works") — results must be specific and observable. - - -## Exit gate - - -- [ ] One scenario per Must rule. -- [ ] One scenario per Reject rule. -- [ ] Edge cases the spec omits are covered (boundary · duplicate · partial failure · …) or ruled out on purpose. -- [ ] Each result is a specific, observable fact. -- [ ] Every rejection asserts what stays unchanged. - - -> **Advisor · Confidence** — delegate a wide scenario sweep when the surface is large (advisor.md); score Edge cases honestly — a missing case surfaces there first (confidence.md). - -## Next - -`python3 .add/tooling/add.py advance` → read `phases/3-plan.md`. -Book: `docs/04-step-2-scenarios.md`. diff --git a/add-method/skill/add/phases/6-verify.md b/add-method/skill/add/phases/6-verify.md index e62541a9..98ca3a76 100644 --- a/add-method/skill/add/phases/6-verify.md +++ b/add-method/skill/add/phases/6-verify.md @@ -70,4 +70,23 @@ and fast-lane tasks use the compact form — banner → SUMMARY → EVIDENCE → python3 .add/tooling/add.py gate PASS # marks the task done # or: add.py gate RISK-ACCEPTED | add.py gate HARD-STOP (return to Build) ``` -Then read `phases/7-observe.md`. Book: `docs/08-step-6-verify.md`. + +## Observe (post-gate, §7) — feed the next loop + +Verify owns the loop's tail since the six-phase merge. After the gate, fill §7: + +1. **Release behind a scope-of-impact limit** — a flag and/or gradual rollout. +2. **Reuse scenarios as monitors** — the §2 scenarios that defined "correct" define + what you alert on: overall error rate, each rejection's rate, latency of the risky op. +3. **Draft the next spec delta** — every defect, surprise, or new need becomes a change + that re-enters the flow at Specify (a new task). Emit lessons tagged by the + competency they improve (`deltas.md`); the human consolidates (`fold.md`). +4. **Propose a voice delta** — where your voice diverged from the human's, propose a + confirmable voice delta tuning `SOUL.md`, emitted `open` (`soul.md` — the human is + the only writer). Never auto-roll-back — recommend; a human owns production. + +> **Decisions (ADR)** — the gate already harvested §7's ADR block; `add.py audit` flags one never harvested. +> **Persona** — tag `· persona: · critical-rule|success-metric|anti-pattern|ability`; +> `fold` grows that section — a HOW-an-agent-behaves lesson belongs in a persona, not the shared pile. + +Loop — the artifacts are living docs the next cycle refines. Map: `self-improve.md` · book: `docs/08-step-6-verify.md` · `docs/09-the-loop.md`. diff --git a/add-method/skill/add/phases/7-observe.md b/add-method/skill/add/phases/7-observe.md deleted file mode 100644 index 1fe58aec..00000000 --- a/add-method/skill/add/phases/7-observe.md +++ /dev/null @@ -1,45 +0,0 @@ -# Phase 7 — Observe (feed the next loop) - -Goal: release deliberately, watch reality, turn what you learn into the next spec. Fill **§7** in TASK.md. - -## Do - -1. **Release behind a scope-of-impact limit** — a flag and/or gradual rollout -2. **Reuse scenarios as monitors** — the §2 scenarios that defined "correct" now - define what you alert on: overall error rate, each rejection's rate (a spike is a - signal), latency of the risky op. -3. **Draft the next spec delta** — every defect, surprise, or new need becomes a - change that re-enters the flow at Specify (a new task). -4. **Propose a voice delta** — note where your voice diverged from the human's (wordings + - flow); propose a confirmable **voice delta** tuning `SOUL.md`, emitted `open`. The human - confirms, then you rewrite the routed section. Read `soul.md` (grammar, routing, - human-is-only-writer). - -> **Decisions (ADR)** — at the gate the engine harvests §7's ADR block (§1/§3/§5/§6); `add.py audit` flags one never harvested. - -## AI prompt - - -Role: a reliability analyst feeding the next cycle. -Read first: telemetry · objectives · incidents. -Steps: 1. report error-budget burn · 2. cluster errors, surface the top real failures · - 3. draft a SPEC delta with evidence links. -Never: auto-roll-back — recommend; a human owns the production decision. - - -## Exit gate - - -- [ ] Released behind a flag/rollout. -- [ ] Scenario-based monitors live. -- [ ] A reviewed spec delta captured (becomes the next `new-task`). - - -> **Persona** — tag `· persona: · critical-rule|success-metric|anti-pattern|ability`; -> `fold` grows that section — a HOW-an-agent-behaves lesson belongs in a persona, not the shared pile. -> **Advisor · Confidence** — spawn a reviewer to mine the run for lessons (advisor.md); score Self-evaluation — did this loop teach the foundation? (confidence.md). - -## Next - -Loop — the artifacts are living docs the next cycle refines. -Map: `self-improve.md` · book: `docs/09-the-loop.md` diff --git a/add-method/skill/add/soul.md b/add-method/skill/add/soul.md index dbeb1ed0..e378528c 100644 --- a/add-method/skill/add/soul.md +++ b/add-method/skill/add/soul.md @@ -53,6 +53,6 @@ The rewrite is surgical: refine or append the bullet the delta names; never sile ## Where it plugs in -- **Emit**: `phases/7-observe.md` proposes voice deltas beside competency/spec deltas. +- **Emit**: `6-verify.md`'s Observe block proposes voice deltas beside competency/spec deltas. - **Target**: `SOUL.md` — "## Voice deltas" ledger holds confirmed history. - **Kin**: `deltas.md` + `fold.md` (same propose→confirm→write discipline). No `add.py` command writes the voice. diff --git a/add-method/src/add_method/_bundled/skill/add/SKILL.md b/add-method/src/add_method/_bundled/skill/add/SKILL.md index 70b894a0..086a1d05 100644 --- a/add-method/src/add_method/_bundled/skill/add/SKILL.md +++ b/add-method/src/add_method/_bundled/skill/add/SKILL.md @@ -82,13 +82,11 @@ Load **only the phase you are in** (progressive disclosure): | Phase | Guide | Produces (TASK.md section) | Who leads | Bundle | |-------|-------|----------------------------|-----------|--------| | setup | `phases/0-setup.md` | `.add/` + living docs + first §1–§3 + `SETUP-REVIEW.md` | AI drafts → **human locks** (the baseline approval) | – | -| specify | `phases/1-specify.md` | §1 rules + ranked lowest-confidence flag | AI drafts (co-specify)† | DIRECTION | -| scenarios | `phases/2-scenarios.md` | §2 Given/When/Then | AI drafts† | DIRECTION | +| specify | `phases/1-specify.md` | §1 rules + ranked flag + §2 Given/When/Then | AI drafts (co-specify)† | DIRECTION | | plan | `phases/3-plan.md` | §3 grounding + frozen shape + build-strategy | AI drafts → **human approves once** (the decision point)† | DIRECTION | | tests | `phases/4-tests.md` | §4 + red suite in `tests/` | AI drafts† | DIRECTION | | build | `phases/5-build.md` | code in `src/`, tests green | **AI** | BUILD | -| verify | `phases/6-verify.md` | §6 checks + gate record | **AI auto-gates on evidence**; human on residue/security‡ | VERIFY | -| observe | `phases/7-observe.md` | §7 spec delta | human + AI | VERIFY | +| verify | `phases/6-verify.md` | §6 checks + gate record + §7 spec delta | **AI auto-gates on evidence**; human on residue/security‡ | VERIFY | † **The specification bundle (v7).** §1–§4 are one bundle; the human gives **one approval at the contract freeze**, lowest-confidence-first — `run.md`. diff --git a/add-method/src/add_method/_bundled/skill/add/phases/1-specify.md b/add-method/src/add_method/_bundled/skill/add/phases/1-specify.md index a85ce3c9..d8b8f424 100644 --- a/add-method/src/add_method/_bundled/skill/add/phases/1-specify.md +++ b/add-method/src/add_method/_bundled/skill/add/phases/1-specify.md @@ -1,7 +1,9 @@ -# Phase 1 — Specify (the rules) +# Phase 1 — Specify (the rules + the scenarios) Goal: state what the feature MUST do and what it must REJECT, with zero ambiguity -for the AI to resolve by guessing. Fill **§1 SPECIFY** in TASK.md. +for the AI to resolve by guessing — then make every rule concrete as a pass/fail +scenario. Fill **§1 SPECIFY** and **§2 SCENARIOS** in TASK.md (one drafting phase +owns both since the six-phase merge). Specify is **co-specification**: brainstorm the shape WITH the user, draft, then validate. If you cannot write the spec, you don't yet understand the feature — stop and ask. @@ -23,6 +25,27 @@ Specify is **co-specification**: brainstorm the shape WITH the user, draft, then - **Assumptions — lowest-confidence first** — ranked most-likely-wrong → least. The top 1–2 carry a `⚠` flag: `⚠ — lowest confidence because ; if wrong: `. Keep the ranking visible — a flat list of equal `[x]` ticks gets approved without reading. +## Scenarios (§2) — every rule made checkable + +Rewrite each rule as a Given/When/Then that people can read and machines can check: + + +```gherkin +Scenario: + Given + When + Then + And # REQUIRED for every rejection +``` + + +Write **one scenario per Must** and **one per Reject**. The `And ... unchanged` +clause catches corrupting partial failures (a balance deducted before a check +fails) — include it on every rejection. Then sweep for the edge cases the spec +omits — boundary, duplicate, partial failure, concurrency, malformed input — add +one per case that applies, or rule it out on purpose. Never settle for a vague +result ("then it works"): every Then is a specific, observable fact. + ## The lowest-confidence flag is bundle-wide The single approval is at the contract freeze, over the whole bundle — so your §1 ranking feeds the bundle-level flag the user reads there (`run.md`): *"of all I'm asking you to freeze, these 1–2 are most likely wrong."* @@ -38,7 +61,9 @@ Steps: 2. Produce §1 — Framings weighed, every Must, every Reject with a named error code, the After state, and the Assumptions RANKED lowest-confidence first. 3. Flag the 1–2 where your confidence is lowest, each with why + cost. -Never: resolve an ambiguity by guessing. + 4. Produce §2 — one scenario per Must and per Reject (And-unchanged on every + rejection), then the edge-case sweep. +Never: resolve an ambiguity by guessing; never a vague result — specific and observable. ## Exit gate @@ -48,14 +73,16 @@ Never: resolve an ambiguity by guessing. - [ ] Every rejection has a named error code; success state-change described. - [ ] Assumptions ordered lowest-confidence first; the 1–2 `⚠` flags carry why + cost — or an honest "none material" that still names the single biggest risk (never a blank "none"). +- [ ] §2: one scenario per Must and per Reject; every rejection asserts what stays unchanged. +- [ ] §2: edge cases covered (boundary · duplicate · partial failure · …) or ruled out on purpose. > **Persona** — load the fit `.add/personas/.md`; its `## Critical Rules` shape §1 (advisory; never lowers a gate). -> **Advisor · Confidence** — for an unfamiliar domain, spawn a researcher (advisor.md); self-score the spec and let the lowest dimension aim your ⚠ flag (confidence.md). +> **Advisor · Confidence** — for an unfamiliar domain spawn a researcher, for a large surface delegate a wide scenario sweep (advisor.md); self-score the spec — the lowest dimension aims your ⚠ flag, a missing edge case surfaces in the Edge-cases score first (confidence.md). ## Next -`python3 .add/tooling/add.py advance` → read `phases/2-scenarios.md`. +`python3 .add/tooling/add.py advance` → read `phases/3-plan.md`. Book: `docs/03-step-1-specify.md`. (UI feature? also sketch flows + every screen state: loading/empty/error/success; name it in the parent MILESTONE.md's Scope-hint vocabulary, not generic prose.) diff --git a/add-method/src/add_method/_bundled/skill/add/phases/2-scenarios.md b/add-method/src/add_method/_bundled/skill/add/phases/2-scenarios.md deleted file mode 100644 index 8d5f4397..00000000 --- a/add-method/src/add_method/_bundled/skill/add/phases/2-scenarios.md +++ /dev/null @@ -1,48 +0,0 @@ -# Phase 2 — Scenarios (pass/fail cases) - -Goal: rewrite each rule as a concrete Given/When/Then that is readable by people and checkable by machines. Fill **§2 SCENARIOS** in TASK.md. - -## Produce (in TASK.md §2) - - -```gherkin -Scenario: - Given - When - Then - And # REQUIRED for every rejection -``` - -The `And ... unchanged` clause catches corrupting partial failures (e.g. a balance -deducted before a check fails). Include it on every rejection. - - -## AI prompt - - -Role: a specification tester. -Read first: §1 · GLOSSARY. -Objective: one scenario per Must and per Reject rule, each result specific and observable. -Steps: - 1. Write one scenario per Must rule and one per Reject rule. - 2. For every rejection add an And-clause asserting what must NOT change. - 3. Sweep for the edge cases the spec omits — boundary, duplicate, partial failure, concurrency, malformed input — add one per case that applies, or rule it out on purpose. -Never: settle for a vague result ("then it works") — results must be specific and observable. - - -## Exit gate - - -- [ ] One scenario per Must rule. -- [ ] One scenario per Reject rule. -- [ ] Edge cases the spec omits are covered (boundary · duplicate · partial failure · …) or ruled out on purpose. -- [ ] Each result is a specific, observable fact. -- [ ] Every rejection asserts what stays unchanged. - - -> **Advisor · Confidence** — delegate a wide scenario sweep when the surface is large (advisor.md); score Edge cases honestly — a missing case surfaces there first (confidence.md). - -## Next - -`python3 .add/tooling/add.py advance` → read `phases/3-plan.md`. -Book: `docs/04-step-2-scenarios.md`. diff --git a/add-method/src/add_method/_bundled/skill/add/phases/6-verify.md b/add-method/src/add_method/_bundled/skill/add/phases/6-verify.md index e62541a9..98ca3a76 100644 --- a/add-method/src/add_method/_bundled/skill/add/phases/6-verify.md +++ b/add-method/src/add_method/_bundled/skill/add/phases/6-verify.md @@ -70,4 +70,23 @@ and fast-lane tasks use the compact form — banner → SUMMARY → EVIDENCE → python3 .add/tooling/add.py gate PASS # marks the task done # or: add.py gate RISK-ACCEPTED | add.py gate HARD-STOP (return to Build) ``` -Then read `phases/7-observe.md`. Book: `docs/08-step-6-verify.md`. + +## Observe (post-gate, §7) — feed the next loop + +Verify owns the loop's tail since the six-phase merge. After the gate, fill §7: + +1. **Release behind a scope-of-impact limit** — a flag and/or gradual rollout. +2. **Reuse scenarios as monitors** — the §2 scenarios that defined "correct" define + what you alert on: overall error rate, each rejection's rate, latency of the risky op. +3. **Draft the next spec delta** — every defect, surprise, or new need becomes a change + that re-enters the flow at Specify (a new task). Emit lessons tagged by the + competency they improve (`deltas.md`); the human consolidates (`fold.md`). +4. **Propose a voice delta** — where your voice diverged from the human's, propose a + confirmable voice delta tuning `SOUL.md`, emitted `open` (`soul.md` — the human is + the only writer). Never auto-roll-back — recommend; a human owns production. + +> **Decisions (ADR)** — the gate already harvested §7's ADR block; `add.py audit` flags one never harvested. +> **Persona** — tag `· persona: · critical-rule|success-metric|anti-pattern|ability`; +> `fold` grows that section — a HOW-an-agent-behaves lesson belongs in a persona, not the shared pile. + +Loop — the artifacts are living docs the next cycle refines. Map: `self-improve.md` · book: `docs/08-step-6-verify.md` · `docs/09-the-loop.md`. diff --git a/add-method/src/add_method/_bundled/skill/add/phases/7-observe.md b/add-method/src/add_method/_bundled/skill/add/phases/7-observe.md deleted file mode 100644 index 1fe58aec..00000000 --- a/add-method/src/add_method/_bundled/skill/add/phases/7-observe.md +++ /dev/null @@ -1,45 +0,0 @@ -# Phase 7 — Observe (feed the next loop) - -Goal: release deliberately, watch reality, turn what you learn into the next spec. Fill **§7** in TASK.md. - -## Do - -1. **Release behind a scope-of-impact limit** — a flag and/or gradual rollout -2. **Reuse scenarios as monitors** — the §2 scenarios that defined "correct" now - define what you alert on: overall error rate, each rejection's rate (a spike is a - signal), latency of the risky op. -3. **Draft the next spec delta** — every defect, surprise, or new need becomes a - change that re-enters the flow at Specify (a new task). -4. **Propose a voice delta** — note where your voice diverged from the human's (wordings + - flow); propose a confirmable **voice delta** tuning `SOUL.md`, emitted `open`. The human - confirms, then you rewrite the routed section. Read `soul.md` (grammar, routing, - human-is-only-writer). - -> **Decisions (ADR)** — at the gate the engine harvests §7's ADR block (§1/§3/§5/§6); `add.py audit` flags one never harvested. - -## AI prompt - - -Role: a reliability analyst feeding the next cycle. -Read first: telemetry · objectives · incidents. -Steps: 1. report error-budget burn · 2. cluster errors, surface the top real failures · - 3. draft a SPEC delta with evidence links. -Never: auto-roll-back — recommend; a human owns the production decision. - - -## Exit gate - - -- [ ] Released behind a flag/rollout. -- [ ] Scenario-based monitors live. -- [ ] A reviewed spec delta captured (becomes the next `new-task`). - - -> **Persona** — tag `· persona: · critical-rule|success-metric|anti-pattern|ability`; -> `fold` grows that section — a HOW-an-agent-behaves lesson belongs in a persona, not the shared pile. -> **Advisor · Confidence** — spawn a reviewer to mine the run for lessons (advisor.md); score Self-evaluation — did this loop teach the foundation? (confidence.md). - -## Next - -Loop — the artifacts are living docs the next cycle refines. -Map: `self-improve.md` · book: `docs/09-the-loop.md` diff --git a/add-method/src/add_method/_bundled/skill/add/soul.md b/add-method/src/add_method/_bundled/skill/add/soul.md index dbeb1ed0..e378528c 100644 --- a/add-method/src/add_method/_bundled/skill/add/soul.md +++ b/add-method/src/add_method/_bundled/skill/add/soul.md @@ -53,6 +53,6 @@ The rewrite is surgical: refine or append the bullet the delta names; never sile ## Where it plugs in -- **Emit**: `phases/7-observe.md` proposes voice deltas beside competency/spec deltas. +- **Emit**: `6-verify.md`'s Observe block proposes voice deltas beside competency/spec deltas. - **Target**: `SOUL.md` — "## Voice deltas" ledger holds confirmed history. - **Kin**: `deltas.md` + `fold.md` (same propose→confirm→write discipline). No `add.py` command writes the voice. diff --git a/add-method/tooling/test_adr_audit.py b/add-method/tooling/test_adr_audit.py index e1212f5a..c2bed18c 100644 --- a/add-method/tooling/test_adr_audit.py +++ b/add-method/tooling/test_adr_audit.py @@ -31,9 +31,9 @@ # the 3 git-tracked mirror trees per surface. Skill guide: canonical · .claude dogfood · bundle. # Book/glossary: canonical · repo-ROOT mirror (test_book_parity) · bundle — NOT .add/docs/, which is # a gitignored local install artifact absent in CI. -OBSERVE_GUIDE = (ADD_METHOD / "skill/add/phases/7-observe.md", - REPO / ".claude/skills/add/phases/7-observe.md", - ADD_METHOD / "src/add_method/_bundled/skill/add/phases/7-observe.md") +OBSERVE_GUIDE = (ADD_METHOD / "skill/add/phases/6-verify.md", + REPO / ".claude/skills/add/phases/6-verify.md", + ADD_METHOD / "src/add_method/_bundled/skill/add/phases/6-verify.md") BOOK_LOOP = (ADD_METHOD / "docs/09-the-loop.md", REPO / "09-the-loop.md", ADD_METHOD / "src/add_method/_bundled/docs/09-the-loop.md") diff --git a/add-method/tooling/test_domain_test_mapping.py b/add-method/tooling/test_domain_test_mapping.py index f56bce3c..b462cc03 100644 --- a/add-method/tooling/test_domain_test_mapping.py +++ b/add-method/tooling/test_domain_test_mapping.py @@ -25,8 +25,8 @@ # 41190 → 41605 @ strategy-facet-block (contract FROZEN @ v1, M6-signed rebaseline — see the # ledger entry in test_skill_lean.py): the duplicate migrates FORWARD in lockstep; the # domain-form addition this file guards remains absorbed (its own net ≤0B claim unchanged). -POOL_GUIDES = ["0-setup.md", "1-specify.md", "2-scenarios.md", "3-plan.md", - "4-tests.md", "5-build.md", "6-verify.md", "7-observe.md"] +POOL_GUIDES = ["0-setup.md", "1-specify.md", "3-plan.md", + "4-tests.md", "5-build.md", "6-verify.md"] POOL_CEILING = int(41605 * 0.80) diff --git a/add-method/tooling/test_fold_persona_sections.py b/add-method/tooling/test_fold_persona_sections.py index 581570e9..734f4d2d 100644 --- a/add-method/tooling/test_fold_persona_sections.py +++ b/add-method/tooling/test_fold_persona_sections.py @@ -139,12 +139,12 @@ class ProseNamesAllFour(unittest.TestCase): def test_guides_and_book(self): # M3 deltas = (ADD_METHOD / "skill" / "add" / "deltas.md").read_text(encoding="utf-8") fold = (ADD_METHOD / "skill" / "add" / "fold.md").read_text(encoding="utf-8") - observe = (ADD_METHOD / "skill" / "add" / "phases" / "7-observe.md").read_text(encoding="utf-8") + observe = (ADD_METHOD / "skill" / "add" / "phases" / "6-verify.md").read_text(encoding="utf-8") book = (ADD_METHOD / "docs" / "18-personas.md").read_text(encoding="utf-8") for hint in ("critical-rule", "success-metric", "anti-pattern", "ability"): self.assertIn(hint, deltas, f"deltas.md grammar misses {hint}") self.assertIn(hint, fold, f"fold.md misses {hint}") - self.assertIn("anti-pattern", observe, "7-observe.md persona footnote misses the new hints") + self.assertIn("anti-pattern", observe, "6-verify.md persona footnote misses the new hints (guide-recut moved it)") self.assertIn("Anti-patterns", book, "18-personas.md must name the grown sections") self.assertIn("Abilities", book) diff --git a/add-method/tooling/test_ground_anchor_sha.py b/add-method/tooling/test_ground_anchor_sha.py index 25eb4f90..fcb500a0 100644 --- a/add-method/tooling/test_ground_anchor_sha.py +++ b/add-method/tooling/test_ground_anchor_sha.py @@ -43,8 +43,8 @@ _CANON_SKILL = HERE.parent / "skill" / "add" PHASES_POOL = [ "phases/0-setup.md", "phases/1-specify.md", - "phases/2-scenarios.md", "phases/3-plan.md", "phases/4-tests.md", - "phases/5-build.md", "phases/6-verify.md", "phases/7-observe.md", + "phases/3-plan.md", "phases/4-tests.md", + "phases/5-build.md", "phases/6-verify.md", ] @@ -176,8 +176,13 @@ def test_phases_pool_within_budget(self): # M4, R:lean_pool_r # forward-migrated (never git-mv'd) to 40339 by the later merge-reconciliation task (PR #120 × # this branch, human-approved) that kept PR #120's persona-template-depth phase-guide prose; # see test_skill_lean.py's own comment trail for that rebaseline's accounting. - self.assertGreaterEqual(phases["baseline"], 40280, - "the phases baseline only ever grows (human-approved rebaselines)") + # floor forward-migrated 40280 → 40205 @ guide-recut (six-phase-loop 3/6, contract + # FROZEN @ v1): the phase merges DELETED two guides (net won 1120 B), the first + # legitimate shrink — "only ever grows" held while the guide set was fixed; the new + # invariant is "never shrinks EXCEPT by a signed deletion accounting" (see + # test_skill_lean.py's POOLS comment trail for this shrink's arithmetic). + self.assertGreaterEqual(phases["baseline"], 40205, + "the phases baseline moves only by signed rebaselines") if __name__ == "__main__": diff --git a/add-method/tooling/test_guide_recut.py b/add-method/tooling/test_guide_recut.py new file mode 100644 index 00000000..d7742bcf --- /dev/null +++ b/add-method/tooling/test_guide_recut.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Red/green tests for guide-recut (six-phase-loop 3/6, frozen v1): the phase-guide +tree re-cuts to the merged 6-phase loop — 1-specify absorbs 2-scenarios' duties, +6-verify absorbs 7-observe's, the two absorbed files are DELETED from all three +guide trees, and SKILL.md's phase table shows the 6-row loop with no dead pointer. + +Run: python3 -m unittest test_guide_recut -v +""" +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +PKG_ROOT = HERE.parent +REPO_ROOT = PKG_ROOT.parent + +GUIDE_TREES = ( + PKG_ROOT / "skill" / "add", # canonical + PKG_ROOT / "src" / "add_method" / "_bundled" / "skill" / "add", # bundled twin + REPO_ROOT / ".claude" / "skills" / "add", # live-skill twin +) +_CANON = GUIDE_TREES[0] +_DELETED = ("phases/2-scenarios.md", "phases/7-observe.md") + +# One marker per load-bearing duty the merge must carry (§1 Must list). +_SPECIFY_DUTY_MARKERS = [ + "Given ", # the gherkin output format survives + "And ", # the And-unchanged clause per rejection + "one scenario per Must", # per-Must AND per-Reject coverage duty + "partial failure", # the edge-case sweep vocabulary +] +_VERIFY_DUTY_MARKERS = [ + "scope-of-impact", # release behind a flag/rollout + "monitors", # scenarios-as-monitors + "spec delta", # the next loop's entry + "voice delta", # the SOUL.md route +] + + +class DeletionTest(unittest.TestCase): + def test_absorbed_files_deleted_x3(self): # M3 + leftovers = [str(tree / rel) for tree in GUIDE_TREES for rel in _DELETED + if (tree / rel).exists()] + self.assertEqual(leftovers, [], + f"absorbed guides must be deleted from every tree: {leftovers}") + + def test_absorbing_guides_synced_x3(self): # M1 + M2 (parity) + for rel in ("phases/1-specify.md", "phases/6-verify.md"): + canon = (_CANON / rel).read_bytes() + for tree in GUIDE_TREES[1:]: + self.assertEqual((tree / rel).read_bytes(), canon, + f"twin drifted: {tree / rel}") + + +class AbsorbedDutiesTest(unittest.TestCase): + def test_specify_guide_carries_scenario_duties(self): # M1 + R2 + text = (_CANON / "phases" / "1-specify.md").read_text(encoding="utf-8") + missing = [m for m in _SPECIFY_DUTY_MARKERS if m not in text] + self.assertEqual(missing, [], + f"1-specify.md must carry 2-scenarios' duties; missing: {missing}") + + def test_verify_guide_carries_observe_duties(self): # M2 + R2 + text = (_CANON / "phases" / "6-verify.md").read_text(encoding="utf-8") + missing = [m for m in _VERIFY_DUTY_MARKERS if m not in text] + self.assertEqual(missing, [], + f"6-verify.md must carry 7-observe's duties; missing: {missing}") + + +class RoutingTest(unittest.TestCase): + def test_no_live_guide_names_a_deleted_file(self): # R1 + M5 + offenders = [] + for tree in GUIDE_TREES: + for f in sorted(tree.rglob("*.md")): + body = f.read_text(encoding="utf-8", errors="replace") + for rel in ("2-scenarios.md", "7-observe.md"): + if rel in body: + offenders.append(f"{f}:{rel}") + self.assertEqual(offenders, [], + f"live skill prose still routes to a deleted guide: {offenders}") + + def test_specify_next_routes_to_plan(self): # M1 (Accept) + text = (_CANON / "phases" / "1-specify.md").read_text(encoding="utf-8") + self.assertIn("phases/3-plan.md", text, + "specify's Next pointer must route straight to plan") + + def test_skill_table_shows_the_six_phase_loop(self): # M4 (Accept) + text = (_CANON / "SKILL.md").read_text(encoding="utf-8") + rows = [ln for ln in text.splitlines() + if ln.startswith("|") and "`phases/" in ln] + self.assertEqual(len(rows), 6, + f"the phase table must hold exactly 6 guide rows: {rows}") + joined = "\n".join(rows) + for gone in ("2-scenarios", "7-observe"): + self.assertNotIn(gone, joined, f"dead row survives: {gone}") + spec_row = next(r for r in rows if "1-specify" in r) + self.assertIn("§2", spec_row, "the specify row must own §2 too") + ver_row = next(r for r in rows if "6-verify" in r) + self.assertIn("§7", ver_row, "the verify row must own §7 too") + + def test_skill_size_ceiling_held(self): # ceiling honors + size = len((_CANON / "SKILL.md").read_bytes()) + self.assertLessEqual(size, 9500, + f"SKILL.md is {size}B; the orient-split ceiling binds this edit") + + +if __name__ == "__main__": + unittest.main() diff --git a/add-method/tooling/test_per_step_hooks.py b/add-method/tooling/test_per_step_hooks.py index d7edc266..7d8447e3 100644 --- a/add-method/tooling/test_per_step_hooks.py +++ b/add-method/tooling/test_per_step_hooks.py @@ -1,6 +1,6 @@ """test_per_step_hooks — guards the Advisor+Confidence hook woven into every phase guide. -Each of the 8 phase guides (0-ground … 7-observe) must carry ONE thin, phase-appropriate +Each phase guide (0-setup … 6-verify, the recut 6-phase loop) must carry ONE thin, phase-appropriate blockquote hook — marked `Advisor · Confidence` — that names BOTH advisor.md and confidence.md, so an agent self-scores and knows when to delegate in the idiom of the phase it is in. SKILL.md must cross-ref both docs so they are discoverable. The hooks must be mutually distinct (not @@ -20,8 +20,8 @@ _SKILL = _TOOLING.parent / "skill" / "add" / "SKILL.md" _PHASE_FILES = ( - "1-specify.md", "2-scenarios.md", "3-plan.md", - "4-tests.md", "5-build.md", "6-verify.md", "7-observe.md", + "1-specify.md", "3-plan.md", + "4-tests.md", "5-build.md", "6-verify.md", ) _MARKER = "Advisor · Confidence" @@ -71,10 +71,11 @@ def test_skill_cross_ref(self) -> None: self.assertIn("confidence.md", self.skill, "SKILL.md must cross-ref confidence.md") def test_wording_surface_count_unchanged(self) -> None: - self.assertEqual(len(_wl.surface_files()), 32, + self.assertEqual(len(_wl.surface_files()), 30, "editing guides must not change the wording-lint surface count " - "(32: +beyond.md @ skill-orient-split; " + "(30: +beyond.md @ skill-orient-split; " "0-ground.md + 3-contract.md merged into 3-plan.md @ guides-and-skill, net -1; " + "2-scenarios.md + 7-observe.md deleted @ guide-recut, net -2; " "+fast-lane.md @ fast-lane, +components.md @ component-method-docs, " "+sensitivity.md @ sensitivity-glossary, +self-improve.md @ self-improving-guide)") diff --git a/add-method/tooling/test_phase_bundles.py b/add-method/tooling/test_phase_bundles.py index 14bac96b..8701ba93 100644 --- a/add-method/tooling/test_phase_bundles.py +++ b/add-method/tooling/test_phase_bundles.py @@ -301,7 +301,7 @@ def test_phase_table_has_bundle_column_populated(self): cells = [c.strip() for c in ln.strip("|").split("|")] rows[cells[0]] = cells expected_bundle = { - "specify": "DIRECTION", "scenarios": "DIRECTION", + "specify": "DIRECTION", "plan": "DIRECTION", "tests": "DIRECTION", "build": "BUILD", "verify": "VERIFY", } diff --git a/add-method/tooling/test_rewrite_guides.py b/add-method/tooling/test_rewrite_guides.py index bdc87060..dbf8c2b3 100644 --- a/add-method/tooling/test_rewrite_guides.py +++ b/add-method/tooling/test_rewrite_guides.py @@ -60,11 +60,9 @@ # Files carrying a `Never:` prompt-field today — each must still carry one after the rewrite. _NEVER_FIELD_FILES = ( _SKILL / "phases" / "1-specify.md", - _SKILL / "phases" / "2-scenarios.md", _SKILL / "phases" / "3-plan.md", _SKILL / "phases" / "4-tests.md", _SKILL / "phases" / "5-build.md", - _SKILL / "phases" / "7-observe.md", _DOCS / "appendix-b-prompts.md", ) diff --git a/add-method/tooling/test_self_improving_guide.py b/add-method/tooling/test_self_improving_guide.py index 5de8bdad..6415df81 100644 --- a/add-method/tooling/test_self_improving_guide.py +++ b/add-method/tooling/test_self_improving_guide.py @@ -4,7 +4,7 @@ `skill/add/self-improve.md` (NEW, unpooled — tree census only) maps the four self-improving artifacts (foundation · personas · SOUL.md · next scope) with each one's emit grammar and consolidator, routes all 5 domains, names how the 8 steps feed observe, - and POINTS at the mechanics guides without duplicating their rules. 7-observe.md points + and POINTS at the mechanics guides without duplicating their rules. 6-verify.md points at it. Whole-tree + phases budgets hold; 3 skill trees stay byte-identical. Run: python3 -m unittest test_self_improving_guide -v """ @@ -62,8 +62,9 @@ def test_no_duplicated_reject_codes(self): # M3 class ObservePointsAtMap(unittest.TestCase): def test_observe_next_points_here(self): # M4 - t = (TREES[0] / "phases" / "7-observe.md").read_text(encoding="utf-8") - self.assertIn("self-improve.md", t, "7-observe.md must point at the map") + # guide-recut: the Observe duties live in 6-verify.md's post-gate block now + t = (TREES[0] / "phases" / "6-verify.md").read_text(encoding="utf-8") + self.assertIn("self-improve.md", t, "6-verify.md must point at the map") class BudgetsAndParity(unittest.TestCase): @@ -77,7 +78,7 @@ def test_tree_and_phases_budgets_hold(self): # R1+R2 self.assertLessEqual(n, int(pool["baseline"] * pool["ratio"]), "phases pool bust") def test_three_tree_parity(self): # R3 - for name in ("self-improve.md", "phases/7-observe.md"): + for name in ("self-improve.md", "phases/6-verify.md"): digests = {hashlib.md5((tree / name).read_bytes()).hexdigest() for tree in TREES} self.assertEqual(len(digests), 1, f"{name} drifted across skill trees") diff --git a/add-method/tooling/test_skill_lean.py b/add-method/tooling/test_skill_lean.py index c468c4c5..f68fd521 100644 --- a/add-method/tooling/test_skill_lean.py +++ b/add-method/tooling/test_skill_lean.py @@ -228,10 +228,16 @@ # after in-file compression absorbed ~100 B (declaration sentence, small-batches line, persona # overlay lead) — +332 B residual human-approved surface (32943 → 33275 actual). RATIO 0.80 kept # EXACTLY; baseline grows by surface ÷ ratio (+⌈332/0.80⌉=415). The won ground is untouched. - {"name": "phases", "ratio": 0.80, "baseline": 41605, + # phases 41605 → 40205 @ guide-recut (six-phase-loop 3/6, contract FROZEN @ v1 — the + # INVERSE of the rebaseline-for-new-surface method, same signing discipline): the phase + # merges deleted 2-scenarios.md (1809 B) + 7-observe.md (2046 B) after their load-bearing + # duties moved into 1-specify.md (+1468 B) / 6-verify.md (+1267 B) — NET won ground + # 1120 B the ceiling locks in: baseline SHRINKS by net ÷ ratio (−⌈1120/0.80⌉=1400); + # ratio 0.80 kept EXACTLY. + {"name": "phases", "ratio": 0.80, "baseline": 40205, "guides": ["phases/0-setup.md", "phases/1-specify.md", - "phases/2-scenarios.md", "phases/3-plan.md", "phases/4-tests.md", - "phases/5-build.md", "phases/6-verify.md", "phases/7-observe.md"]}, + "phases/3-plan.md", "phases/4-tests.md", + "phases/5-build.md", "phases/6-verify.md"]}, # reference 66345 → 70282 @ sensitivity-glossary (same "rebaseline for genuinely-new surface" method): # a NEW load-on-demand guide (`sensitivity.md`, 2677 B — how the AI maintains the project's extensible # risk-class vocabulary) joins the reference pool. RATIO 0.68 kept EXACTLY; baseline grows by new-surface @@ -292,8 +298,8 @@ # Routing rows the SKILL.md phase table MUST keep (one guide per phase). PHASE_GUIDES = [ "phases/0-setup.md", "phases/1-specify.md", - "phases/2-scenarios.md", "phases/3-plan.md", "phases/4-tests.md", - "phases/5-build.md", "phases/6-verify.md", "phases/7-observe.md", + "phases/3-plan.md", "phases/4-tests.md", + "phases/5-build.md", "phases/6-verify.md", ] # Load-on-demand pointers SKILL.md must keep naming. diff --git a/add-method/tooling/test_soul_self_improve.py b/add-method/tooling/test_soul_self_improve.py index 6182d71b..fb241ede 100644 --- a/add-method/tooling/test_soul_self_improve.py +++ b/add-method/tooling/test_soul_self_improve.py @@ -4,7 +4,7 @@ A new skill doc `soul.md` defines the VOICE DELTA loop — grammar, the open→confirmed lifecycle, the human-is-only-writer rule, the routing into SOUL.md's sections, and the -reject codes — mirroring deltas.md + fold.md but folding into SOUL.md. phases/7-observe.md +reject codes — mirroring deltas.md + fold.md but folding into SOUL.md. phases/6-verify.md emits a voice delta; SKILL.md + SOUL.md.tmpl point at the loop. Docs-only, no engine change. Run: python3 -m unittest test_soul_self_improve -v @@ -70,9 +70,10 @@ def test_reject_codes(self): "soul.md must name an unroutable-voice-delta reject code") def test_observe_emits_voice_delta(self): - obs = _read(CANONICAL, "phases/7-observe.md") + # guide-recut: the Observe duties live in 6-verify.md's post-gate block now + obs = _read(CANONICAL, "phases/6-verify.md") self.assertIn("voice delta", obs.lower(), - "7-observe.md must gain a voice-delta emit step") + "6-verify.md must carry the voice-delta emit step") self.assertIn("soul.md", obs, "the emit step must point at soul.md") def test_skill_points_at_soul_md(self): @@ -86,7 +87,7 @@ def test_soul_template_points_at_loop(self): self.assertIn("soul.md", body[i:], "the Voice deltas section must point at soul.md") def test_three_trees_identical(self): - for rel in ("soul.md", "phases/7-observe.md", "SKILL.md"): + for rel in ("soul.md", "phases/6-verify.md", "SKILL.md"): digests = {_md5(t / rel) for t in (CANONICAL, BUNDLED, DOGFOOD)} self.assertEqual(len(digests), 1, f"{rel} diverged across the 3 skill trees") diff --git a/add-method/tooling/test_wording_lint.py b/add-method/tooling/test_wording_lint.py index 00d56765..0278cd72 100644 --- a/add-method/tooling/test_wording_lint.py +++ b/add-method/tooling/test_wording_lint.py @@ -166,7 +166,7 @@ def test_enforced_banned_green_over_live_surface(self) -> None: self.assertEqual([f for f in live if f.code == "banned_idiom_present"], []) def test_surface_files_cover_the_contract(self) -> None: - """The surface is skill/add (30 files: +beyond.md @ skill-orient-split, +loop.md @ v20, +graduate.md @ v22, +confidence.md +advisor.md @ advisor-context, +compact-foundation.md @ foundation-compaction, +soul.md @ soul-self-improve, +design.md @ udd-design-loop, +release.md @ release-altitude, +phases/fast-lane.md @ fast-lane, +components.md @ component-method-docs) + docs/appendix-b-prompts.md, per §3.""" + """The surface is skill/add (29 files: +beyond.md @ skill-orient-split, +loop.md @ v20, +graduate.md @ v22, +confidence.md +advisor.md @ advisor-context, +compact-foundation.md @ foundation-compaction, +soul.md @ soul-self-improve, +design.md @ udd-design-loop, +release.md @ release-altitude, +phases/fast-lane.md @ fast-lane, +components.md @ component-method-docs) + docs/appendix-b-prompts.md, per §3.""" files = wl.surface_files() names = {p.name for p in files} self.assertIn("SKILL.md", names) @@ -183,8 +183,9 @@ def test_surface_files_cover_the_contract(self) -> None: self.assertIn("sensitivity.md", names) self.assertIn("appendix-b-prompts.md", names) self.assertIn("self-improve.md", names) - self.assertEqual(len(files), 32, f"expected 31 skill files + appendix-b, got {len(files)} " - "(guides-and-skill merged 0-ground.md + 3-contract.md into 3-plan.md, net -1)") + self.assertEqual(len(files), 30, f"expected 29 skill files + appendix-b, got {len(files)} " + "(guide-recut deleted 2-scenarios.md + 7-observe.md — duties absorbed " + "into 1-specify.md / 6-verify.md, net -2)") if __name__ == "__main__": diff --git a/add-method/tooling/test_xml_convention.py b/add-method/tooling/test_xml_convention.py index be59da62..ca378e9e 100644 --- a/add-method/tooling/test_xml_convention.py +++ b/add-method/tooling/test_xml_convention.py @@ -86,9 +86,6 @@ def _strip_code_fences(text: str) -> str: "## 5 · After the lock", "## Next", )}, - "2-scenarios.md": { - "prompt": True, "output_format": True, - "narrative": ("## Next",)}, "3-plan.md": { "prompt": True, "output_format": True, "narrative": ("## The freeze review checklist", "## Next")}, @@ -106,9 +103,6 @@ def _strip_code_fences(text: str) -> str: "## Part four — was the green earned?", "## Record exactly one outcome (no silent pass)", )}, - "7-observe.md": { - "prompt": True, "output_format": False, - "narrative": ("## Do", "## Next")}, } From 3fdeb67a8a8d2ac88780dbb35f41530f98a3713f Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 02:02:49 +0700 Subject: [PATCH 5/9] =?UTF-8?q?feat(method):=20bundle-disclosure=20?= =?UTF-8?q?=E2=80=94=20phase=20guides=20load=20inside=20the=20roster=20age?= =?UTF-8?q?nts=20(six-phase-loop=204/6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user's directive made mechanism: when the orchestrator delegates a phase, the roster bundle agent loads its OWN phase guides at spawn — the orchestrator reads only SKILL.md. The inline lane stays first-class (load the one phase guide yourself, unchanged). - add-design.md: NEW "Load your bundle guides" section (phases/0-setup.md on fresh projects + phases/1-specify.md + phases/3-plan.md); span prose re-cut to setup · specify · plan (grounding/scenarios/contract live inside them); description spawn steps -> SETUP, SPECIFY, or PLAN; Return enum follows - add-build.md: loads phases/4-tests.md + phases/5-build.md at spawn - add-verify.md: loads phases/6-verify.md (carries the post-gate Observe block); owns verify end-to-end — OBSERVE is no longer a spawn step; Return enum verify-only - SKILL.md flow lead-in states the split in one sentence: inline -> load only the phase you are in; delegating -> the Bundle-column agent loads its own guides, you read ONLY this file. 9442 B, under the orient-split 9500 ceiling. Synced x3 skill trees; agents synced canonical -> _bundled + .claude installed copies. Tests: NEW test_bundle_disclosure.py (6: per-agent guide census · no deleted guide or retired step name — path-qualified, docs chapters keep their names · disclosure sentence · ceiling · x2 parity). Full fence 3541/3541 OK. Engine untouched — no MD5 re-aim. refs: six-phase-loop milestone, stacked branch feat/six-phase-loop author: Tin Dang --- .add/state.json | 52 ++++++++- .add/tasks/bundle-disclosure/TASK.md | 107 ++++++++++++++++++ .claude/agents/add-build.md | 3 + .claude/agents/add-design.md | 11 +- .claude/agents/add-verify.md | 9 +- .claude/skills/add/SKILL.md | 2 +- add-method/agents/add-build.md | 3 + add-method/agents/add-design.md | 11 +- add-method/agents/add-verify.md | 9 +- add-method/skill/add/SKILL.md | 2 +- .../add_method/_bundled/agents/add-build.md | 3 + .../add_method/_bundled/agents/add-design.md | 11 +- .../add_method/_bundled/agents/add-verify.md | 9 +- .../add_method/_bundled/skill/add/SKILL.md | 2 +- add-method/tooling/test_bundle_disclosure.py | 95 ++++++++++++++++ 15 files changed, 302 insertions(+), 27 deletions(-) create mode 100644 .add/tasks/bundle-disclosure/TASK.md create mode 100644 add-method/tooling/test_bundle_disclosure.py diff --git a/.add/state.json b/.add/state.json index 8b8fa45c..c92f55d2 100644 --- a/.add/state.json +++ b/.add/state.json @@ -1,7 +1,7 @@ { "project": "AIDD / ADD Methodology", "stage": "mvp", - "active_task": "guide-recut", + "active_task": "bundle-disclosure", "tasks": { "add-check": { "title": "add.py check: validate .add project integrity", @@ -9181,10 +9181,56 @@ "email": "tindang.ht97@gmail.com", "source": "git" } + }, + "bundle-disclosure": { + "title": "Disclose phase guides into the bundle agents", + "phase": "done", + "gate": "PASS", + "milestone": "six-phase-loop", + "depends_on": [], + "created": "2026-07-13T18:54:40+00:00", + "updated": "2026-07-13T19:02:26+00:00", + "fast": true, + "freeze": { + "version": "v1", + "frozen_at": "2026-07-13T18:55:28+00:00", + "approved_by": "Tin Dang", + "actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } + }, + "flag_verified": true, + "tripwire": { + "contract_md5": "be05d2e5ff4c0858e5c3e8c7106edb71", + "tests": { + "add-method/tooling/test_bundle_disclosure.py": "4c7636a224273a1342c78245d124a5d6" + } + }, + "scope": { + "declared": [ + "add-method/agents/", + "add-method/src/add_method/_bundled/agents/", + ".claude/agents/", + "add-method/skill/", + "add-method/src/add_method/_bundled/skill/", + ".claude/skills/add/", + "add-method/tooling/", + ".add/tooling/", + "add-method/.add/" + ], + "snapshot_md5": "c5492e4f812ac8c567b99b3f353b4adf" + }, + "gate_actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } } }, "created": "2026-05-28T15:39:04+00:00", - "updated": "2026-07-13T18:52:57+00:00", + "updated": "2026-07-13T19:02:26+00:00", "milestones": { "v1-1": { "title": "v1.1 \u2014 adoption & ergonomics", @@ -10446,7 +10492,7 @@ "plan-legibility": "relations-surface", "ceremony-to-effort": "fold-draft-at-close", "call-floor": "skill-orient-split", - "six-phase-loop": "guide-recut" + "six-phase-loop": "bundle-disclosure" }, "todos": [ { diff --git a/.add/tasks/bundle-disclosure/TASK.md b/.add/tasks/bundle-disclosure/TASK.md new file mode 100644 index 00000000..a8c8d0cf --- /dev/null +++ b/.add/tasks/bundle-disclosure/TASK.md @@ -0,0 +1,107 @@ +# TASK: Disclose phase guides into the bundle agents + +slug: bundle-disclosure · created: 2026-07-14 · stage: mvp +milestone: six-phase-loop +autonomy: auto +phase: done +fast: true + +> Fast lane — one small task, minimal sections, filled top-to-bottom. The trust floor still +> holds: a FROZEN §3 contract · ≥1 red test before build · a recorded §6 gate (security = HARD-STOP). +> The acceptance scenario collapses into §1 `Accept:`; the observe note is one optional line at the gate. + +--- + +## 1 · SPECIFY — the rules + +> Project the expectations from the milestone Ground + this request — light, not re-invented. +Feature: bundle-disclosure — phase guides disclose INTO the roster bundle agents (six-phase-loop 4/6, the user's directive: "instead load all step's skill in main agent, we can disclose it into subagents"): each roster agent loads its own bundle guides at spawn; the orchestrator reads SKILL.md only when delegating; the inline lane stays first-class (work inline -> load the one phase guide yourself, unchanged) +Must: + - add-design.md instructs loading `phases/0-setup.md` (fresh project) + `phases/1-specify.md` + `phases/3-plan.md` at spawn, and its span prose re-cuts to the merged reality (setup · specify · plan — grounding/scenarios/contract live inside them, not as step names) + - add-build.md instructs loading `phases/4-tests.md` + `phases/5-build.md` at spawn + - add-verify.md instructs loading `phases/6-verify.md` at spawn, and its prose re-cuts (it owns verify end-to-end incl the post-gate Observe duties; OBSERVE is no longer a spawn step) + - SKILL.md states the disclosure split in one sentence: delegated -> the roster agent loads its own bundle guides (the orchestrator reads SKILL.md only); inline -> load the one phase guide yourself (unchanged, first-class); ≤9500 B held + - agents sync: canonical `add-method/agents/` -> `_bundled/agents/` byte-identical (the existing roster pin) + the installed `.claude/agents/` copies refreshed +Reject: + - a roster agent naming a deleted guide (2-scenarios.md/7-observe.md) or a retired step name (GROUND/SCENARIOS/CONTRACT/OBSERVE) as a spawn step -> the doc-truth test goes red + - the orchestrator instructed to pre-read a phase guide before delegating -> the disclosure sentence pin goes red +Accept: Given a delegated phase, When the orchestrator spawns the roster agent named by the SKILL.md table, Then the agent's own file names the exact bundle guide(s) it loads and the orchestrator needed only SKILL.md. +Boundary: none — no external input (agent prose + one SKILL.md sentence; the engine is untouched) +Assumptions: ⚠ agent prose edits ripple into surface registries (method-ergonomics precedent: "new agent prose ripples into 3 surface registries") — why: roster descriptions are pinned by CLAUDE.md sync-guidelines/roster tests; if wrong (more registries pin the old span wording): each is a fence-named doc-truth ripple (cost: fence rounds) + +--- + +## 3 · PLAN — the change plan: ground · contract · build-strategy + +### Grounding +Touches (files · symbols): add-method/agents/add-design.md · add-build.md · add-verify.md (span prose + NEW bundle-guides instruction) · add-method/src/add_method/_bundled/agents/ (byte-parity twin) · .claude/agents/ (installed copies) · add-method/skill/add/SKILL.md (one disclosure sentence, ≤9500 ceiling) ×3 skill trees · roster surface registries (CLAUDE.md block? test_roster_shipped, fence-named) +Context (working folder): engine untouched (no MD5 re-aim); test_roster_shipped pins bundled↔canonical byte parity; test_skill_orient_split pins the 9500 ceiling; SKILL.md pinned-phrase census binds any trim +Honors (patterns / conventions): inline-over-heavy-spawns (the inline lane stays first-class, never demoted) · don't override an agent's native Return contract (extend only) · progressive disclosure (the milestone's thesis) +Anchors the contract cites: add-design.md · add-build.md · add-verify.md · SKILL.md flow section +Ground SHA: 7ffdd2d — stamped by freeze + +### Contract + +``` +add-design.md: span -> "setup · specify · plan"; + "## Load your bundle guides" + (phases/0-setup.md fresh-project · phases/1-specify.md · phases/3-plan.md, + from the project's skill tree, at spawn, before the persona); description + spawn steps -> "SETUP, SPECIFY, or PLAN step" +add-build.md: + same section (phases/4-tests.md · phases/5-build.md); + description spawn steps stay "TESTS or BUILD" +add-verify.md: span -> verify end-to-end incl post-gate Observe duties (§7); + + same section (phases/6-verify.md); description -> "Spawn at the VERIFY + step" (no OBSERVE step name) +SKILL.md (flow section): one added sentence — delegating? the roster agent + loads its own bundle guides; you read ONLY SKILL.md. Inline? load the one + phase guide yourself (unchanged). <= 9500 B +sync: agents canonical -> _bundled byte-identical + .claude/agents refresh; + SKILL.md x3 trees +NEW test_bundle_disclosure.py pins: per-agent guide census · no retired step + name in descriptions · the SKILL.md disclosure sentence · parity · ceiling +``` + +`Least-sure flag surfaced at freeze:` [contract] the SKILL.md sentence placement under the 145-byte headroom — why: the ceiling is 9500 and the table section already carries pinned phrases on both sides; if wrong (no room without touching a pinned phrase): trim the equally-unpinned "Load only the phase you are in" lead-in and fold the sentence into it (cost: one lean-fence round) +Status: FROZEN @ v1 — approved by Tin Dang +### Build-strategy +Scope (may touch): `add-method/agents/` `add-method/src/add_method/_bundled/agents/` `.claude/agents/` `add-method/skill/` `add-method/src/add_method/_bundled/skill/` `.claude/skills/add/` `add-method/tooling/` `.add/tooling/` `add-method/.add/` +Strategy & known-problem fixes: red test_bundle_disclosure -> add-design re-cut -> add-build add-section -> add-verify re-cut -> SKILL.md sentence under ceiling -> sync agents + skill trees -> fence. Traps: bundled agents byte-parity pin · the pinned-phrase census on SKILL.md (trim nothing pinned) · agent Return contracts NEVER change (extend-only rule) · description lines feed the Agent-tool picker (keep capability wording, change only step names). +Approach (domain strategy): disclose don't duplicate — agents point at the SAME guide files the inline lane reads; one source of truth per phase + +### AI-verify record (required when gate_mode: ai-plan-verify) +- [x] §3 PLAN grounding anchors resolve in the current tree (all three agent files + SKILL.md flow section read this session) +- [x] §1 every Must + every Reject present, each paired with its outcome +- [x] §3 Contract shape is concrete (no template placeholder text remains) +- [x] Lowest-confidence flag surfaced and substantive (the 145-byte SKILL.md headroom risk) +Verified by: claude-fable-5 (orchestrator, inline) · at: 2026-07-14T02:30:00Z + +--- + +## 4 · TESTS — failing-first (red) + +Plan: test_each_agent_names_its_bundle_guides · test_no_agent_names_a_deleted_guide_or_retired_step · test_skill_states_the_disclosure_split · test_agents_bundled_parity_after_recut · test_skill_ceiling_held. +Tests live in: `add-method/tooling/test_bundle_disclosure.py` · MUST run red before Build. + +--- + +## 5 · BUILD — AI writes the code (execution) + +> The change plan was frozen in §3 PLAN. Build to it: honor the §3 Build-strategy Scope; improve on the strategy if the code teaches you better. +Strategy actually used: as planned, plus two step-back corrections: the deleted-guide pin had to be PATH-qualified (`phases/2-scenarios.md`) because the docs CHAPTERS keep the old names (docs/04-step-2-scenarios.md lives on — a book pointer is not a guide pointer); and both re-cut agents' native Return enums updated to the merged phases (add-design setup|specify|plan · add-verify verify) — the agent's own file in a sanctioned task, not a spawn-prompt override. +Code lives in: `./src/` · Constraints: change no test, no frozen contract; stay inside the §3 Build-strategy Scope; allow-list packages only. + +--- + +## 6 · VERIFY — evidence + gate + +- [x] all tests pass · coverage held · no test or contract altered during build +- [x] green was EARNED — no overfit / vacuous asserts / stubbed-away logic +- [x] input dialect held — tests speak the spec's example formats (spec-dialect floor) +- [x] no exposed secrets, injection openings, or unexpected dependencies (security = HARD-STOP) + +Build expectations (from §1 Accept + §3 CONTRACT): each roster bundle agent's file names the exact guides it loads at spawn (add-design: 0-setup+1-specify+3-plan · add-build: 4-tests+5-build · add-verify: 6-verify incl the Observe block); no agent names a deleted guide or retired spawn step; SKILL.md states the disclosure split (9442 B ≤ 9500); agents byte-identical canonical↔bundled + .claude refreshed — confirmed by test_bundle_disclosure 6/6 (3 red first) + the FULL fence 3541 tests OK / REAL_EXIT=0 (fence-bd-r1.log, zero ripple beyond the task suite itself, zero tests weakened). Engine untouched (no MD5 re-aim). + +### GATE RECORD +Outcome: PASS +Reviewed by: Tin Dang · date: 2026-07-14 + diff --git a/.claude/agents/add-build.md b/.claude/agents/add-build.md index 3897195b..25112ac1 100644 --- a/.claude/agents/add-build.md +++ b/.claude/agents/add-build.md @@ -7,6 +7,9 @@ color: green You are the **build** specialist in ADD's phase-agent roster — a test author and builder who drives red to green the honest way. You cover two phases: tests (turn each scenario plus the frozen contract into one executable test, confirm the suite fails for the right reason) and build (implement until every test passes, without changing a test or the frozen contract). +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/4-tests.md` and `phases/5-build.md`. They are the method depth for your span; the orchestrator does not pre-read them for you. + ## Become the persona Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic build engineer, correctness over speed — the generic body never blocks. diff --git a/.claude/agents/add-design.md b/.claude/agents/add-design.md index 3e87d8be..4e8af671 100644 --- a/.claude/agents/add-design.md +++ b/.claude/agents/add-design.md @@ -1,16 +1,19 @@ --- name: add-design -description: The ADD design specialist — drafts the whole direction span (foundation/setup, the real-code ground map, the rules, concrete scenarios, and the frozen contract) to the one human freeze decision. Spawn at the SETUP, GROUND, SPECIFY, SCENARIOS, or CONTRACT step. Recommended tier — top (ambiguity here costs every later phase). +description: The ADD design specialist — drafts the whole direction span (foundation/setup, the real-code ground map, the rules, concrete scenarios, and the frozen contract) to the one human freeze decision. Spawn at the SETUP, SPECIFY, or PLAN step. Recommended tier — top (ambiguity here costs every later phase). model: inherit color: blue --- -You are the **design** specialist in ADD's phase-agent roster — an interface architect and domain analyst who fixes direction before any code is written. You cover five phases in one continuous span — setup, ground, specify, scenarios, contract — carrying a task (or, for a fresh project, the whole foundation) from a blank page to a frozen, testable shape. Below the freeze, code is disposable; above it, the shape does not move — and that freeze is never yours to declare. +You are the **design** specialist in ADD's phase-agent roster — an interface architect and domain analyst who fixes direction before any code is written. You cover the direction span — setup · specify · plan (grounding, the scenarios, and the contract live INSIDE those phases since the six-phase merge) — carrying a task (or, for a fresh project, the whole foundation) from a blank page to a frozen, testable shape. Below the freeze, code is disposable; above it, the shape does not move — and that freeze is never yours to declare. + +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/1-specify.md` and `phases/3-plan.md` — plus `phases/0-setup.md` on a fresh project. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic domain-analyst/architect, correctness over speed — the generic body never blocks. -## What you own (the design span: setup → ground → specify → scenarios → contract) +## What you own (the design span: setup → specify → plan) - **Setup** (fresh project only) — point ADD at the repo and draft the whole foundation yourself: brownfield is mapped silently from code, greenfield runs the short 4-lens interview (Domain · Spec · Users · Decisions), lowest-confidence-first. Seed `.add/personas/` from PROJECT.md plus the vendored `.add/personas-teacher/` library, never clobbering an existing file. - **Ground** — before specifying, gather the REAL working folder a task touches: files, symbols, signatures (cite the symbol, never a bare line number — symbols survive, line numbers rot), conventions to honor, the issues/risks that feed Specify, and the anchors the contract will cite. Record the SHA grounded against. - **Specify** — co-specify with the user in three moves: Diverge (surface 2-3 framings + open questions), Converge (draft every Must and every Reject, each rejection paired with a named error code, plus the After state), Validate (present the ranked lowest-confidence assumptions; the user confirms or corrects). If you cannot write the spec, you do not yet understand the feature — stop and ask. @@ -27,6 +30,6 @@ Treat any Strategy you draft as a PREFERRED plan for whoever builds it, not a ha ## Return (disclose progress) End with a structured verdict the orchestrator parses: -`{ phase: setup|ground|specify|scenarios|contract, persona, result, bundle: { must, reject, scenarios, contract_draft }, least_sure_flag, confidence: {per-dimension 0–1}, open_questions }`. +`{ phase: setup|specify|plan, persona, result, bundle: { must, reject, scenarios, contract_draft }, least_sure_flag, confidence: {per-dimension 0–1}, open_questions }`. Method depth: the AIDD book in `.add/docs/` — `02-the-flow.md` · `03-step-1-specify.md` · `04-step-2-scenarios.md` · `05-step-3-plan.md` · `10-setup-and-stages.md`. diff --git a/.claude/agents/add-verify.md b/.claude/agents/add-verify.md index 70c4743f..38e22b1b 100644 --- a/.claude/agents/add-verify.md +++ b/.claude/agents/add-verify.md @@ -1,11 +1,14 @@ --- name: add-verify -description: The ADD verify specialist — establishes trust beyond a green suite (evidence, concurrency/security/architecture, the earned-green refute-read), records one outcome, then watches reality and drafts the next spec delta. Spawn at the VERIFY or OBSERVE step. Recommended tier — top (the independent adversarial lens). +description: The ADD verify specialist — establishes trust beyond a green suite (evidence, concurrency/security/architecture, the earned-green refute-read), records one outcome, then watches reality and drafts the next spec delta. Spawn at the VERIFY step (it owns the post-gate Observe duties too). Recommended tier — top (the independent adversarial lens). model: inherit color: red --- -You are the **verify** specialist in ADD's phase-agent roster — a verifier who trusts evidence, not a plausible diff, then a reliability analyst who feeds what shipped back into the next loop. You cover two phases: verify (confirm evidence, check what tests miss, refute the green, record one outcome) and observe (release deliberately, watch reality, draft the next spec delta). +You are the **verify** specialist in ADD's phase-agent roster — a verifier who trusts evidence, not a plausible diff, then a reliability analyst who feeds what shipped back into the next loop. You own the verify phase end-to-end (confirm evidence, check what tests miss, refute the green, record one outcome) including its post-gate Observe duties (release deliberately, watch reality, draft the next spec delta — §7). + +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guide from the project's skill tree (`.claude/skills/add/phases/`): `phases/6-verify.md` (it carries the post-gate Observe block). It is the method depth for your span; the orchestrator does not pre-read it for you. ## Become the persona (do this FIRST — before acting on any task-specific instructions in your prompt) Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Use a generic reliability/security engineer, correctness over speed — the generic body never blocks. @@ -29,7 +32,7 @@ Treat any Strategy the builder used as their PREFERRED path, not a hard rule you ## Return (disclose progress) End with a structured verdict the orchestrator parses: -`{ phase: verify|observe, persona, result, evidence, residue, outcome, deltas, confidence: {per-dimension 0–1}, open_questions }`. +`{ phase: verify, persona, result, evidence, residue, outcome, deltas, confidence: {per-dimension 0–1}, open_questions }`. When observe surfaces a lesson about HOW an agent should behave (a suspect instinct, a checkable skill), recommend tagging it `· persona: · anti-pattern|ability` so `add.py fold` grows that persona instead of the shared conventions pile. diff --git a/.claude/skills/add/SKILL.md b/.claude/skills/add/SKILL.md index 086a1d05..343b0bb3 100644 --- a/.claude/skills/add/SKILL.md +++ b/.claude/skills/add/SKILL.md @@ -77,7 +77,7 @@ confirms. Unsharp intent? **Interview before you size** (`intake.md`). For a mil ## The flow and which file to load -Load **only the phase you are in** (progressive disclosure): +Inline, load **only the phase you are in**; delegating, spawn the Bundle-column agent — it loads its own bundle guides (you read ONLY this file): | Phase | Guide | Produces (TASK.md section) | Who leads | Bundle | |-------|-------|----------------------------|-----------|--------| diff --git a/add-method/agents/add-build.md b/add-method/agents/add-build.md index 3897195b..25112ac1 100644 --- a/add-method/agents/add-build.md +++ b/add-method/agents/add-build.md @@ -7,6 +7,9 @@ color: green You are the **build** specialist in ADD's phase-agent roster — a test author and builder who drives red to green the honest way. You cover two phases: tests (turn each scenario plus the frozen contract into one executable test, confirm the suite fails for the right reason) and build (implement until every test passes, without changing a test or the frozen contract). +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/4-tests.md` and `phases/5-build.md`. They are the method depth for your span; the orchestrator does not pre-read them for you. + ## Become the persona Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic build engineer, correctness over speed — the generic body never blocks. diff --git a/add-method/agents/add-design.md b/add-method/agents/add-design.md index 3e87d8be..4e8af671 100644 --- a/add-method/agents/add-design.md +++ b/add-method/agents/add-design.md @@ -1,16 +1,19 @@ --- name: add-design -description: The ADD design specialist — drafts the whole direction span (foundation/setup, the real-code ground map, the rules, concrete scenarios, and the frozen contract) to the one human freeze decision. Spawn at the SETUP, GROUND, SPECIFY, SCENARIOS, or CONTRACT step. Recommended tier — top (ambiguity here costs every later phase). +description: The ADD design specialist — drafts the whole direction span (foundation/setup, the real-code ground map, the rules, concrete scenarios, and the frozen contract) to the one human freeze decision. Spawn at the SETUP, SPECIFY, or PLAN step. Recommended tier — top (ambiguity here costs every later phase). model: inherit color: blue --- -You are the **design** specialist in ADD's phase-agent roster — an interface architect and domain analyst who fixes direction before any code is written. You cover five phases in one continuous span — setup, ground, specify, scenarios, contract — carrying a task (or, for a fresh project, the whole foundation) from a blank page to a frozen, testable shape. Below the freeze, code is disposable; above it, the shape does not move — and that freeze is never yours to declare. +You are the **design** specialist in ADD's phase-agent roster — an interface architect and domain analyst who fixes direction before any code is written. You cover the direction span — setup · specify · plan (grounding, the scenarios, and the contract live INSIDE those phases since the six-phase merge) — carrying a task (or, for a fresh project, the whole foundation) from a blank page to a frozen, testable shape. Below the freeze, code is disposable; above it, the shape does not move — and that freeze is never yours to declare. + +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/1-specify.md` and `phases/3-plan.md` — plus `phases/0-setup.md` on a fresh project. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic domain-analyst/architect, correctness over speed — the generic body never blocks. -## What you own (the design span: setup → ground → specify → scenarios → contract) +## What you own (the design span: setup → specify → plan) - **Setup** (fresh project only) — point ADD at the repo and draft the whole foundation yourself: brownfield is mapped silently from code, greenfield runs the short 4-lens interview (Domain · Spec · Users · Decisions), lowest-confidence-first. Seed `.add/personas/` from PROJECT.md plus the vendored `.add/personas-teacher/` library, never clobbering an existing file. - **Ground** — before specifying, gather the REAL working folder a task touches: files, symbols, signatures (cite the symbol, never a bare line number — symbols survive, line numbers rot), conventions to honor, the issues/risks that feed Specify, and the anchors the contract will cite. Record the SHA grounded against. - **Specify** — co-specify with the user in three moves: Diverge (surface 2-3 framings + open questions), Converge (draft every Must and every Reject, each rejection paired with a named error code, plus the After state), Validate (present the ranked lowest-confidence assumptions; the user confirms or corrects). If you cannot write the spec, you do not yet understand the feature — stop and ask. @@ -27,6 +30,6 @@ Treat any Strategy you draft as a PREFERRED plan for whoever builds it, not a ha ## Return (disclose progress) End with a structured verdict the orchestrator parses: -`{ phase: setup|ground|specify|scenarios|contract, persona, result, bundle: { must, reject, scenarios, contract_draft }, least_sure_flag, confidence: {per-dimension 0–1}, open_questions }`. +`{ phase: setup|specify|plan, persona, result, bundle: { must, reject, scenarios, contract_draft }, least_sure_flag, confidence: {per-dimension 0–1}, open_questions }`. Method depth: the AIDD book in `.add/docs/` — `02-the-flow.md` · `03-step-1-specify.md` · `04-step-2-scenarios.md` · `05-step-3-plan.md` · `10-setup-and-stages.md`. diff --git a/add-method/agents/add-verify.md b/add-method/agents/add-verify.md index 70c4743f..38e22b1b 100644 --- a/add-method/agents/add-verify.md +++ b/add-method/agents/add-verify.md @@ -1,11 +1,14 @@ --- name: add-verify -description: The ADD verify specialist — establishes trust beyond a green suite (evidence, concurrency/security/architecture, the earned-green refute-read), records one outcome, then watches reality and drafts the next spec delta. Spawn at the VERIFY or OBSERVE step. Recommended tier — top (the independent adversarial lens). +description: The ADD verify specialist — establishes trust beyond a green suite (evidence, concurrency/security/architecture, the earned-green refute-read), records one outcome, then watches reality and drafts the next spec delta. Spawn at the VERIFY step (it owns the post-gate Observe duties too). Recommended tier — top (the independent adversarial lens). model: inherit color: red --- -You are the **verify** specialist in ADD's phase-agent roster — a verifier who trusts evidence, not a plausible diff, then a reliability analyst who feeds what shipped back into the next loop. You cover two phases: verify (confirm evidence, check what tests miss, refute the green, record one outcome) and observe (release deliberately, watch reality, draft the next spec delta). +You are the **verify** specialist in ADD's phase-agent roster — a verifier who trusts evidence, not a plausible diff, then a reliability analyst who feeds what shipped back into the next loop. You own the verify phase end-to-end (confirm evidence, check what tests miss, refute the green, record one outcome) including its post-gate Observe duties (release deliberately, watch reality, draft the next spec delta — §7). + +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guide from the project's skill tree (`.claude/skills/add/phases/`): `phases/6-verify.md` (it carries the post-gate Observe block). It is the method depth for your span; the orchestrator does not pre-read it for you. ## Become the persona (do this FIRST — before acting on any task-specific instructions in your prompt) Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Use a generic reliability/security engineer, correctness over speed — the generic body never blocks. @@ -29,7 +32,7 @@ Treat any Strategy the builder used as their PREFERRED path, not a hard rule you ## Return (disclose progress) End with a structured verdict the orchestrator parses: -`{ phase: verify|observe, persona, result, evidence, residue, outcome, deltas, confidence: {per-dimension 0–1}, open_questions }`. +`{ phase: verify, persona, result, evidence, residue, outcome, deltas, confidence: {per-dimension 0–1}, open_questions }`. When observe surfaces a lesson about HOW an agent should behave (a suspect instinct, a checkable skill), recommend tagging it `· persona: · anti-pattern|ability` so `add.py fold` grows that persona instead of the shared conventions pile. diff --git a/add-method/skill/add/SKILL.md b/add-method/skill/add/SKILL.md index 086a1d05..343b0bb3 100644 --- a/add-method/skill/add/SKILL.md +++ b/add-method/skill/add/SKILL.md @@ -77,7 +77,7 @@ confirms. Unsharp intent? **Interview before you size** (`intake.md`). For a mil ## The flow and which file to load -Load **only the phase you are in** (progressive disclosure): +Inline, load **only the phase you are in**; delegating, spawn the Bundle-column agent — it loads its own bundle guides (you read ONLY this file): | Phase | Guide | Produces (TASK.md section) | Who leads | Bundle | |-------|-------|----------------------------|-----------|--------| diff --git a/add-method/src/add_method/_bundled/agents/add-build.md b/add-method/src/add_method/_bundled/agents/add-build.md index 3897195b..25112ac1 100644 --- a/add-method/src/add_method/_bundled/agents/add-build.md +++ b/add-method/src/add_method/_bundled/agents/add-build.md @@ -7,6 +7,9 @@ color: green You are the **build** specialist in ADD's phase-agent roster — a test author and builder who drives red to green the honest way. You cover two phases: tests (turn each scenario plus the frozen contract into one executable test, confirm the suite fails for the right reason) and build (implement until every test passes, without changing a test or the frozen contract). +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/4-tests.md` and `phases/5-build.md`. They are the method depth for your span; the orchestrator does not pre-read them for you. + ## Become the persona Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic build engineer, correctness over speed — the generic body never blocks. diff --git a/add-method/src/add_method/_bundled/agents/add-design.md b/add-method/src/add_method/_bundled/agents/add-design.md index 3e87d8be..4e8af671 100644 --- a/add-method/src/add_method/_bundled/agents/add-design.md +++ b/add-method/src/add_method/_bundled/agents/add-design.md @@ -1,16 +1,19 @@ --- name: add-design -description: The ADD design specialist — drafts the whole direction span (foundation/setup, the real-code ground map, the rules, concrete scenarios, and the frozen contract) to the one human freeze decision. Spawn at the SETUP, GROUND, SPECIFY, SCENARIOS, or CONTRACT step. Recommended tier — top (ambiguity here costs every later phase). +description: The ADD design specialist — drafts the whole direction span (foundation/setup, the real-code ground map, the rules, concrete scenarios, and the frozen contract) to the one human freeze decision. Spawn at the SETUP, SPECIFY, or PLAN step. Recommended tier — top (ambiguity here costs every later phase). model: inherit color: blue --- -You are the **design** specialist in ADD's phase-agent roster — an interface architect and domain analyst who fixes direction before any code is written. You cover five phases in one continuous span — setup, ground, specify, scenarios, contract — carrying a task (or, for a fresh project, the whole foundation) from a blank page to a frozen, testable shape. Below the freeze, code is disposable; above it, the shape does not move — and that freeze is never yours to declare. +You are the **design** specialist in ADD's phase-agent roster — an interface architect and domain analyst who fixes direction before any code is written. You cover the direction span — setup · specify · plan (grounding, the scenarios, and the contract live INSIDE those phases since the six-phase merge) — carrying a task (or, for a fresh project, the whole foundation) from a blank page to a frozen, testable shape. Below the freeze, code is disposable; above it, the shape does not move — and that freeze is never yours to declare. + +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/1-specify.md` and `phases/3-plan.md` — plus `phases/0-setup.md` on a fresh project. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic domain-analyst/architect, correctness over speed — the generic body never blocks. -## What you own (the design span: setup → ground → specify → scenarios → contract) +## What you own (the design span: setup → specify → plan) - **Setup** (fresh project only) — point ADD at the repo and draft the whole foundation yourself: brownfield is mapped silently from code, greenfield runs the short 4-lens interview (Domain · Spec · Users · Decisions), lowest-confidence-first. Seed `.add/personas/` from PROJECT.md plus the vendored `.add/personas-teacher/` library, never clobbering an existing file. - **Ground** — before specifying, gather the REAL working folder a task touches: files, symbols, signatures (cite the symbol, never a bare line number — symbols survive, line numbers rot), conventions to honor, the issues/risks that feed Specify, and the anchors the contract will cite. Record the SHA grounded against. - **Specify** — co-specify with the user in three moves: Diverge (surface 2-3 framings + open questions), Converge (draft every Must and every Reject, each rejection paired with a named error code, plus the After state), Validate (present the ranked lowest-confidence assumptions; the user confirms or corrects). If you cannot write the spec, you do not yet understand the feature — stop and ask. @@ -27,6 +30,6 @@ Treat any Strategy you draft as a PREFERRED plan for whoever builds it, not a ha ## Return (disclose progress) End with a structured verdict the orchestrator parses: -`{ phase: setup|ground|specify|scenarios|contract, persona, result, bundle: { must, reject, scenarios, contract_draft }, least_sure_flag, confidence: {per-dimension 0–1}, open_questions }`. +`{ phase: setup|specify|plan, persona, result, bundle: { must, reject, scenarios, contract_draft }, least_sure_flag, confidence: {per-dimension 0–1}, open_questions }`. Method depth: the AIDD book in `.add/docs/` — `02-the-flow.md` · `03-step-1-specify.md` · `04-step-2-scenarios.md` · `05-step-3-plan.md` · `10-setup-and-stages.md`. diff --git a/add-method/src/add_method/_bundled/agents/add-verify.md b/add-method/src/add_method/_bundled/agents/add-verify.md index 70c4743f..38e22b1b 100644 --- a/add-method/src/add_method/_bundled/agents/add-verify.md +++ b/add-method/src/add_method/_bundled/agents/add-verify.md @@ -1,11 +1,14 @@ --- name: add-verify -description: The ADD verify specialist — establishes trust beyond a green suite (evidence, concurrency/security/architecture, the earned-green refute-read), records one outcome, then watches reality and drafts the next spec delta. Spawn at the VERIFY or OBSERVE step. Recommended tier — top (the independent adversarial lens). +description: The ADD verify specialist — establishes trust beyond a green suite (evidence, concurrency/security/architecture, the earned-green refute-read), records one outcome, then watches reality and drafts the next spec delta. Spawn at the VERIFY step (it owns the post-gate Observe duties too). Recommended tier — top (the independent adversarial lens). model: inherit color: red --- -You are the **verify** specialist in ADD's phase-agent roster — a verifier who trusts evidence, not a plausible diff, then a reliability analyst who feeds what shipped back into the next loop. You cover two phases: verify (confirm evidence, check what tests miss, refute the green, record one outcome) and observe (release deliberately, watch reality, draft the next spec delta). +You are the **verify** specialist in ADD's phase-agent roster — a verifier who trusts evidence, not a plausible diff, then a reliability analyst who feeds what shipped back into the next loop. You own the verify phase end-to-end (confirm evidence, check what tests miss, refute the green, record one outcome) including its post-gate Observe duties (release deliberately, watch reality, draft the next spec delta — §7). + +## Load your bundle guides (disclosure — the orchestrator reads only SKILL.md) +At spawn, read YOUR phase guide from the project's skill tree (`.claude/skills/add/phases/`): `phases/6-verify.md` (it carries the post-gate Observe block). It is the method depth for your span; the orchestrator does not pre-read it for you. ## Become the persona (do this FIRST — before acting on any task-specific instructions in your prompt) Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Use a generic reliability/security engineer, correctness over speed — the generic body never blocks. @@ -29,7 +32,7 @@ Treat any Strategy the builder used as their PREFERRED path, not a hard rule you ## Return (disclose progress) End with a structured verdict the orchestrator parses: -`{ phase: verify|observe, persona, result, evidence, residue, outcome, deltas, confidence: {per-dimension 0–1}, open_questions }`. +`{ phase: verify, persona, result, evidence, residue, outcome, deltas, confidence: {per-dimension 0–1}, open_questions }`. When observe surfaces a lesson about HOW an agent should behave (a suspect instinct, a checkable skill), recommend tagging it `· persona: · anti-pattern|ability` so `add.py fold` grows that persona instead of the shared conventions pile. diff --git a/add-method/src/add_method/_bundled/skill/add/SKILL.md b/add-method/src/add_method/_bundled/skill/add/SKILL.md index 086a1d05..343b0bb3 100644 --- a/add-method/src/add_method/_bundled/skill/add/SKILL.md +++ b/add-method/src/add_method/_bundled/skill/add/SKILL.md @@ -77,7 +77,7 @@ confirms. Unsharp intent? **Interview before you size** (`intake.md`). For a mil ## The flow and which file to load -Load **only the phase you are in** (progressive disclosure): +Inline, load **only the phase you are in**; delegating, spawn the Bundle-column agent — it loads its own bundle guides (you read ONLY this file): | Phase | Guide | Produces (TASK.md section) | Who leads | Bundle | |-------|-------|----------------------------|-----------|--------| diff --git a/add-method/tooling/test_bundle_disclosure.py b/add-method/tooling/test_bundle_disclosure.py new file mode 100644 index 00000000..0d1b1fad --- /dev/null +++ b/add-method/tooling/test_bundle_disclosure.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Red/green tests for bundle-disclosure (six-phase-loop 4/6, frozen v1): phase +guides disclose INTO the roster bundle agents — each agent's own file names the +bundle guide(s) it loads at spawn, the orchestrator reads only SKILL.md when +delegating, and no agent names a deleted guide or a retired step name. The +inline lane stays first-class (load the one phase guide yourself, unchanged). + +Run: python3 -m unittest test_bundle_disclosure -v +""" +import hashlib +import re +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +PKG_ROOT = HERE.parent +REPO_ROOT = PKG_ROOT.parent + +_AGENTS = PKG_ROOT / "agents" +_BUNDLED_AGENTS = PKG_ROOT / "src" / "add_method" / "_bundled" / "agents" +_SKILL = PKG_ROOT / "skill" / "add" / "SKILL.md" + +# agent -> the exact bundle guides its file must instruct loading (M1-M3) +_BUNDLE_GUIDES = { + "add-design.md": ["phases/0-setup.md", "phases/1-specify.md", "phases/3-plan.md"], + "add-build.md": ["phases/4-tests.md", "phases/5-build.md"], + "add-verify.md": ["phases/6-verify.md"], +} +# step names retired by the six-phase merges — none may survive as a SPAWN STEP +# in a description line (capability prose may still use the lowercase words). +_RETIRED_STEPS = ("GROUND", "SCENARIOS", "CONTRACT", "OBSERVE") +# path-qualified: the DOCS chapters (docs/04-step-2-scenarios.md …) live on; only +# the skill-tree guides were deleted. +_DELETED_GUIDES = ("phases/2-scenarios.md", "phases/7-observe.md") + + +def _md5(p: Path) -> str: + return hashlib.md5(p.read_bytes()).hexdigest() + + +class BundleGuidesTest(unittest.TestCase): + def test_each_agent_names_its_bundle_guides(self): # M1 + M2 + M3 + Accept + for name, guides in _BUNDLE_GUIDES.items(): + text = (_AGENTS / name).read_text(encoding="utf-8") + missing = [g for g in guides if g not in text] + self.assertEqual(missing, [], + f"{name} must name the bundle guide(s) it loads at spawn; " + f"missing: {missing}") + + def test_no_agent_names_a_deleted_guide_or_retired_step(self): # R1 + for f in sorted(_AGENTS.glob("*.md")): + text = f.read_text(encoding="utf-8") + for g in _DELETED_GUIDES: + self.assertNotIn(g, text, f"{f.name} names the deleted guide {g}") + desc = next((ln for ln in text.splitlines() + if ln.startswith("description:")), "") + step_m = re.search(r"Spawn at the ([^.]*)\bstep", desc) + if step_m: + for retired in _RETIRED_STEPS: + self.assertNotIn(retired, step_m.group(1), + f"{f.name} still names retired step {retired} " + f"in its spawn line: {step_m.group(0)}") + + +class DisclosureSplitTest(unittest.TestCase): + def test_skill_states_the_disclosure_split(self): # M4 + R2 + text = _SKILL.read_text(encoding="utf-8") + self.assertIn("loads its own bundle guides", text, + "SKILL.md must state that a delegated roster agent loads " + "its own bundle guides (the orchestrator reads SKILL.md only)") + + def test_skill_ceiling_held(self): # M4 (ceiling honors) + size = len(_SKILL.read_bytes()) + self.assertLessEqual(size, 9500, + f"SKILL.md is {size}B; the orient-split ceiling binds") + + +class ParityTest(unittest.TestCase): + def test_agents_bundled_parity_after_recut(self): # M5 + for f in sorted(_AGENTS.glob("*.md")): + twin = _BUNDLED_AGENTS / f.name + self.assertTrue(twin.exists(), f"bundled twin missing: {twin}") + self.assertEqual(_md5(twin), _md5(f), f"bundled twin drifted: {f.name}") + + def test_installed_agents_refreshed(self): # M5 (.claude copies) + installed = REPO_ROOT / ".claude" / "agents" + for name in _BUNDLE_GUIDES: + f = installed / name + self.assertTrue(f.exists(), f"installed agent missing: {f}") + self.assertEqual(_md5(f), _md5(_AGENTS / name), + f"installed agent stale: {name}") + + +if __name__ == "__main__": + unittest.main() From 08e9ce149ca5c99c9eaa5cae708389ef03f26d5e Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 02:09:58 +0700 Subject: [PATCH 6/9] =?UTF-8?q?feat(method):=20persona-presets=20=E2=80=94?= =?UTF-8?q?=20per-phase=20teacher-grade=20fallback=20stances=20(six-phase-?= =?UTF-8?q?loop=205/6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interview decision made mechanism: per-phase expertise via PRESETS on the lean 5-agent roster, not a per-step agent rebuild. Each bundle agent's thin "use a generic engineer" fallback upgrades to named expert stances per owned phase, distilled from the vendored teacher library: - add-design.md: Preset (specify) — a 15-year domain analyst who asks rather than assumes, every Reject earns a named error code (setup rides this); Preset (plan) — a systems architect, contract-first, cites only anchors that exist, sizes the change's reach before freezing scope - add-build.md: Preset (tests) — red for the RIGHT reason, one test per scenario, behavior never internals; Preset (build) — the smallest honest diff in small reviewable batches, a test is never touched to go green - add-verify.md: Preset (verify) — adversarial reviewer then reliability analyst, refutes the green before trusting it, security always HARD-STOP, spans the post-gate Observe duties (§7) Presets are the FALLBACK tier only: project-persona routing stays first (the flow: selection sentence untouched); a preset never blocks and never lowers a gate. Synced canonical -> _bundled + .claude installed copies. Tests: NEW test_persona_presets.py (4: per-phase preset census · no bare generic fallback left · routing-first + never-lowers-a-gate survive · x2 parity). Full fence 3545/3545 OK. Engine untouched. refs: six-phase-loop milestone, stacked branch feat/six-phase-loop author: Tin Dang --- .add/state.json | 49 +++++++- .add/tasks/persona-presets/TASK.md | 111 ++++++++++++++++++ .claude/agents/add-build.md | 4 +- .claude/agents/add-design.md | 4 +- .claude/agents/add-verify.md | 3 +- add-method/agents/add-build.md | 4 +- add-method/agents/add-design.md | 4 +- add-method/agents/add-verify.md | 3 +- .../add_method/_bundled/agents/add-build.md | 4 +- .../add_method/_bundled/agents/add-design.md | 4 +- .../add_method/_bundled/agents/add-verify.md | 3 +- add-method/tooling/test_persona_presets.py | 70 +++++++++++ 12 files changed, 251 insertions(+), 12 deletions(-) create mode 100644 .add/tasks/persona-presets/TASK.md create mode 100644 add-method/tooling/test_persona_presets.py diff --git a/.add/state.json b/.add/state.json index c92f55d2..d3832b76 100644 --- a/.add/state.json +++ b/.add/state.json @@ -1,7 +1,7 @@ { "project": "AIDD / ADD Methodology", "stage": "mvp", - "active_task": "bundle-disclosure", + "active_task": "persona-presets", "tasks": { "add-check": { "title": "add.py check: validate .add project integrity", @@ -9227,10 +9227,53 @@ "email": "tindang.ht97@gmail.com", "source": "git" } + }, + "persona-presets": { + "title": "Per-phase teacher-grade persona presets on the roster", + "phase": "done", + "gate": "PASS", + "milestone": "six-phase-loop", + "depends_on": [], + "created": "2026-07-13T19:03:07+00:00", + "updated": "2026-07-13T19:09:34+00:00", + "fast": true, + "freeze": { + "version": "v1", + "frozen_at": "2026-07-13T19:03:52+00:00", + "approved_by": "Tin Dang", + "actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } + }, + "flag_verified": true, + "tripwire": { + "contract_md5": "1325f3f86bc2de21d27336d108bdf296", + "tests": { + "add-method/tooling/test_persona_presets.py": "d7f5dc87b81e92c9a9e025bada7c5b2a" + } + }, + "scope": { + "declared": [ + "add-method/agents/", + "add-method/src/add_method/_bundled/agents/", + ".claude/agents/", + "add-method/tooling/", + ".add/tooling/", + "add-method/.add/" + ], + "snapshot_md5": "9fa5813e1d277bae100312f22d6f0ee2" + }, + "gate_actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } } }, "created": "2026-05-28T15:39:04+00:00", - "updated": "2026-07-13T19:02:26+00:00", + "updated": "2026-07-13T19:09:34+00:00", "milestones": { "v1-1": { "title": "v1.1 \u2014 adoption & ergonomics", @@ -10492,7 +10535,7 @@ "plan-legibility": "relations-surface", "ceremony-to-effort": "fold-draft-at-close", "call-floor": "skill-orient-split", - "six-phase-loop": "bundle-disclosure" + "six-phase-loop": "persona-presets" }, "todos": [ { diff --git a/.add/tasks/persona-presets/TASK.md b/.add/tasks/persona-presets/TASK.md new file mode 100644 index 00000000..88c7f840 --- /dev/null +++ b/.add/tasks/persona-presets/TASK.md @@ -0,0 +1,111 @@ +# TASK: Per-phase teacher-grade persona presets on the roster + +slug: persona-presets · created: 2026-07-14 · stage: mvp +milestone: six-phase-loop +autonomy: auto +phase: done +fast: true + +> Fast lane — one small task, minimal sections, filled top-to-bottom. The trust floor still +> holds: a FROZEN §3 contract · ≥1 red test before build · a recorded §6 gate (security = HARD-STOP). +> The acceptance scenario collapses into §1 `Accept:`; the observe note is one optional line at the gate. + +--- + +## 1 · SPECIFY — the rules + +> Project the expectations from the milestone Ground + this request — light, not re-invented. +Feature: persona-presets — per-phase teacher-grade persona PRESETS on the lean roster (six-phase-loop 5/6, the user's interview decision: per-phase expertise via presets, NOT a per-step agent rebuild): each bundle agent's no-persona fallback upgrades from one thin generic line to a named expert stance PER OWNED PHASE, marked `Preset ():` +Must: + - add-design.md carries `Preset (specify):` and `Preset (plan):` lines (a setup preset rides the specify one) — each names a concrete expert stance + its working rule in one line + - add-build.md carries `Preset (tests):` and `Preset (build):` lines + - add-verify.md carries a `Preset (verify):` line that spans the gate AND the post-gate Observe duties + - the preset is the FALLBACK tier only: the project-persona selection stays first (the existing `flow:` routing prose is untouched above the presets); the preset never blocks and never lowers a gate + - agents sync canonical -> _bundled byte-identical + .claude refresh +Reject: + - a preset that overrides the project-persona routing (the `flow:` selection sentence removed or demoted) -> the routing-first pin goes red + - banned idiom in preset prose (the wording-lint sweeps agents? if not: the fence's own census) -> lint red +Accept: Given a spawn with no project persona seeded or matched, When the bundle agent reads its persona section, Then it finds a named per-phase expert stance (not a bare "generic engineer") for every phase it owns. +Boundary: none — no external input (agent prose only; engine untouched) +Assumptions: ⚠ the wording-lint surface may not cover agents/ (its census is skill/add + appendix) so banned-idiom discipline is manual here — why: POOLS/census read skill trees only; if wrong (agents ARE swept): the fence names the exact line (cost: one round) + +--- + +## 3 · PLAN — the change plan: ground · contract · build-strategy + +### Grounding +Touches (files · symbols): add-method/agents/add-design.md · add-build.md · add-verify.md ("Become the persona" sections, fallback sentence -> preset lines) · _bundled/agents twins · .claude/agents copies +Context (working folder): engine untouched; test_bundle_disclosure + test_roster_shipped parity pins bind; the vendored teacher library (.add/personas-teacher/) is the STANCE SOURCE the presets distill, not a runtime read +Honors (patterns / conventions): teacher-not-runtime personas (persona-learning-loop) · presets advisory, never lowers a gate · banned-slang list (no "blast radius", no "least-sure" as a bare token) +Anchors the contract cites: add-design.md · add-build.md · add-verify.md persona sections +Ground SHA: e3e0164 — stamped by freeze + +### Contract + +``` +Each "Become the persona" section's fallback sentence ("No persona seeded or +matched? Use a generic ...") is REPLACED by per-phase preset lines: + add-design.md: + Preset (specify): a domain analyst (15y) who asks rather than assumes — + every Reject earns a named error code; setup rides this stance + Preset (plan): a systems architect — contract-first, names the real + anchors, sizes the change's reach before freezing scope + add-build.md: + Preset (tests): a test engineer — red for the RIGHT reason, one test per + scenario, behavior not internals + Preset (build): an implementation engineer — smallest honest diff, small + reviewable batches, never touches a test + add-verify.md: + Preset (verify): an adversarial reviewer then reliability analyst — + refutes the green before trusting it, then watches reality (§7) +Common tail on each block: presets are the no-persona FALLBACK tier — + project personas route first; a preset never blocks and never lowers a gate. +NEW test_persona_presets.py pins: per-agent preset census (every owned phase) + · routing-first sentence survives · no bare "generic " fallback left + · parity x2 +``` + +`Least-sure flag surfaced at freeze:` [contract] the preset wording vs the wording-lint banned list — why: agent prose has tripped surface registries before (method-ergonomics lesson) and the banned list is only greppable, not enumerated here; if wrong: the fence names the exact banned token, reword (cost: one round) +Status: FROZEN @ v1 — approved by Tin Dang +### Build-strategy +Scope (may touch): `add-method/agents/` `add-method/src/add_method/_bundled/agents/` `.claude/agents/` `add-method/tooling/` `.add/tooling/` `add-method/.add/` +Strategy & known-problem fixes: red test_persona_presets -> replace the three fallback sentences with preset blocks -> sync x2 twins + .claude -> fence. Traps: add-verify's fallback sentence is LONG (severity-convention prose rides it — replace ONLY the trailing "No persona seeded or matched?" sentence) · parity pins re-check bytes. +Approach (domain strategy): distill the teacher library's stances into one-line presets; fallback tier only, routing untouched + +### AI-verify record (required when gate_mode: ai-plan-verify) +- [x] §3 PLAN grounding anchors resolve in the current tree (all three persona sections read this session) +- [x] §1 every Must + every Reject present, each paired with its outcome +- [x] §3 Contract shape is concrete (no template placeholder text remains) +- [x] Lowest-confidence flag surfaced and substantive (the banned-token wording risk) +Verified by: claude-fable-5 (orchestrator, inline) · at: 2026-07-14T02:50:00Z + +--- + +## 4 · TESTS — failing-first (red) + +Plan: test_each_agent_carries_a_preset_per_owned_phase · test_routing_first_sentence_survives · test_no_bare_generic_fallback_left · test_presets_synced_x2. +Tests live in: `add-method/tooling/test_persona_presets.py` · MUST run red before Build. + +--- + +## 5 · BUILD — AI writes the code (execution) + +> The change plan was frozen in §3 PLAN. Build to it: honor the §3 Build-strategy Scope; improve on the strategy if the code teaches you better. +Strategy actually used: as planned — the three fallback sentences swapped for preset blocks in place (add-verify's long severity-convention prose untouched above its trailing sentence, exactly as the strategy's trap note called); zero fence ripple. +Code lives in: `./src/` · Constraints: change no test, no frozen contract; stay inside the §3 Build-strategy Scope; allow-list packages only. + +--- + +## 6 · VERIFY — evidence + gate + +- [x] all tests pass · coverage held · no test or contract altered during build +- [x] green was EARNED — no overfit / vacuous asserts / stubbed-away logic +- [x] input dialect held — tests speak the spec's example formats (spec-dialect floor) +- [x] no exposed secrets, injection openings, or unexpected dependencies (security = HARD-STOP) + +Build expectations (from §1 Accept + §3 CONTRACT): with no project persona seeded/matched, each bundle agent finds a named per-phase expert stance (design: specify+plan · build: tests+build · verify: verify spanning §7) instead of a bare generic line; the routing-first sentence and the never-blocks/never-lowers floor survive verbatim — confirmed by test_persona_presets 4/4 (3 red first) + the FULL fence 3545 tests OK / REAL_EXIT=0 (fence-pp-r1.log, zero ripple, zero weakened). Engine untouched. + +### GATE RECORD +Outcome: PASS +Reviewed by: Tin Dang · date: 2026-07-14 + diff --git a/.claude/agents/add-build.md b/.claude/agents/add-build.md index 25112ac1..2d8b5d4a 100644 --- a/.claude/agents/add-build.md +++ b/.claude/agents/add-build.md @@ -11,7 +11,9 @@ You are the **build** specialist in ADD's phase-agent roster — a test author a At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/4-tests.md` and `phases/5-build.md`. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona -Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic build engineer, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Fall back to the per-phase presets — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (tests): a test engineer — red for the RIGHT reason, one test per scenario, asserts behavior never internals. +- Preset (build): an implementation engineer — the smallest honest diff in small reviewable batches; a test is never touched to go green. ## What you own (tests → build) - **Tests** — one executable test per scenario, asserting behavior not internals; contract-conformance tests for every shape and error code the frozen contract names; side-effect assertions on rejection paths; confirm the suite is RED for the right reason (missing implementation, not a broken harness) before build opens; record a coverage target. diff --git a/.claude/agents/add-design.md b/.claude/agents/add-design.md index 4e8af671..dcf51e2c 100644 --- a/.claude/agents/add-design.md +++ b/.claude/agents/add-design.md @@ -11,7 +11,9 @@ You are the **design** specialist in ADD's phase-agent roster — an interface a At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/1-specify.md` and `phases/3-plan.md` — plus `phases/0-setup.md` on a fresh project. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona -Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic domain-analyst/architect, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Fall back to the per-phase presets — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (specify): a 15-year domain analyst who asks rather than assumes — every Reject earns a named error code, every assumption is ranked before it is believed (setup rides this stance). +- Preset (plan): a systems architect — contract-first, cites only anchors that exist in the tree, and sizes the change's reach before freezing scope. ## What you own (the design span: setup → specify → plan) - **Setup** (fresh project only) — point ADD at the repo and draft the whole foundation yourself: brownfield is mapped silently from code, greenfield runs the short 4-lens interview (Domain · Spec · Users · Decisions), lowest-confidence-first. Seed `.add/personas/` from PROJECT.md plus the vendored `.add/personas-teacher/` library, never clobbering an existing file. diff --git a/.claude/agents/add-verify.md b/.claude/agents/add-verify.md index 38e22b1b..918dde81 100644 --- a/.claude/agents/add-verify.md +++ b/.claude/agents/add-verify.md @@ -11,7 +11,8 @@ You are the **verify** specialist in ADD's phase-agent roster — a verifier who At spawn, read YOUR phase guide from the project's skill tree (`.claude/skills/add/phases/`): `phases/6-verify.md` (it carries the post-gate Observe block). It is the method depth for your span; the orchestrator does not pre-read it for you. ## Become the persona (do this FIRST — before acting on any task-specific instructions in your prompt) -Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Use a generic reliability/security engineer, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Fall back to the preset — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (verify): an adversarial reviewer, then a reliability analyst — refutes the green before trusting it, treats security findings as HARD-STOP always, then watches reality and drafts the next spec delta (§7). ## What you own (verify → observe) - **Before build** — fill the Build expectations block (observable outcomes derived from the scenarios and the frozen contract); confirm each against real evidence at the gate, not just a green test. diff --git a/add-method/agents/add-build.md b/add-method/agents/add-build.md index 25112ac1..2d8b5d4a 100644 --- a/add-method/agents/add-build.md +++ b/add-method/agents/add-build.md @@ -11,7 +11,9 @@ You are the **build** specialist in ADD's phase-agent roster — a test author a At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/4-tests.md` and `phases/5-build.md`. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona -Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic build engineer, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Fall back to the per-phase presets — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (tests): a test engineer — red for the RIGHT reason, one test per scenario, asserts behavior never internals. +- Preset (build): an implementation engineer — the smallest honest diff in small reviewable batches; a test is never touched to go green. ## What you own (tests → build) - **Tests** — one executable test per scenario, asserting behavior not internals; contract-conformance tests for every shape and error code the frozen contract names; side-effect assertions on rejection paths; confirm the suite is RED for the right reason (missing implementation, not a broken harness) before build opens; record a coverage target. diff --git a/add-method/agents/add-design.md b/add-method/agents/add-design.md index 4e8af671..dcf51e2c 100644 --- a/add-method/agents/add-design.md +++ b/add-method/agents/add-design.md @@ -11,7 +11,9 @@ You are the **design** specialist in ADD's phase-agent roster — an interface a At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/1-specify.md` and `phases/3-plan.md` — plus `phases/0-setup.md` on a fresh project. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona -Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic domain-analyst/architect, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Fall back to the per-phase presets — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (specify): a 15-year domain analyst who asks rather than assumes — every Reject earns a named error code, every assumption is ranked before it is believed (setup rides this stance). +- Preset (plan): a systems architect — contract-first, cites only anchors that exist in the tree, and sizes the change's reach before freezing scope. ## What you own (the design span: setup → specify → plan) - **Setup** (fresh project only) — point ADD at the repo and draft the whole foundation yourself: brownfield is mapped silently from code, greenfield runs the short 4-lens interview (Domain · Spec · Users · Decisions), lowest-confidence-first. Seed `.add/personas/` from PROJECT.md plus the vendored `.add/personas-teacher/` library, never clobbering an existing file. diff --git a/add-method/agents/add-verify.md b/add-method/agents/add-verify.md index 38e22b1b..918dde81 100644 --- a/add-method/agents/add-verify.md +++ b/add-method/agents/add-verify.md @@ -11,7 +11,8 @@ You are the **verify** specialist in ADD's phase-agent roster — a verifier who At spawn, read YOUR phase guide from the project's skill tree (`.claude/skills/add/phases/`): `phases/6-verify.md` (it carries the post-gate Observe block). It is the method depth for your span; the orchestrator does not pre-read it for you. ## Become the persona (do this FIRST — before acting on any task-specific instructions in your prompt) -Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Use a generic reliability/security engineer, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Fall back to the preset — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (verify): an adversarial reviewer, then a reliability analyst — refutes the green before trusting it, treats security findings as HARD-STOP always, then watches reality and drafts the next spec delta (§7). ## What you own (verify → observe) - **Before build** — fill the Build expectations block (observable outcomes derived from the scenarios and the frozen contract); confirm each against real evidence at the gate, not just a green test. diff --git a/add-method/src/add_method/_bundled/agents/add-build.md b/add-method/src/add_method/_bundled/agents/add-build.md index 25112ac1..2d8b5d4a 100644 --- a/add-method/src/add_method/_bundled/agents/add-build.md +++ b/add-method/src/add_method/_bundled/agents/add-build.md @@ -11,7 +11,9 @@ You are the **build** specialist in ADD's phase-agent roster — a test author a At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/4-tests.md` and `phases/5-build.md`. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona -Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic build engineer, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: build` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: build-engineer / test-author); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Fall back to the per-phase presets — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (tests): a test engineer — red for the RIGHT reason, one test per scenario, asserts behavior never internals. +- Preset (build): an implementation engineer — the smallest honest diff in small reviewable batches; a test is never touched to go green. ## What you own (tests → build) - **Tests** — one executable test per scenario, asserting behavior not internals; contract-conformance tests for every shape and error code the frozen contract names; side-effect assertions on rejection paths; confirm the suite is RED for the right reason (missing implementation, not a broken harness) before build opens; record a coverage target. diff --git a/add-method/src/add_method/_bundled/agents/add-design.md b/add-method/src/add_method/_bundled/agents/add-design.md index 4e8af671..dcf51e2c 100644 --- a/add-method/src/add_method/_bundled/agents/add-design.md +++ b/add-method/src/add_method/_bundled/agents/add-design.md @@ -11,7 +11,9 @@ You are the **design** specialist in ADD's phase-agent roster — an interface a At spawn, read YOUR phase guides from the project's skill tree (`.claude/skills/add/phases/`): `phases/1-specify.md` and `phases/3-plan.md` — plus `phases/0-setup.md` on a fresh project. They are the method depth for your span; the orchestrator does not pre-read them for you. ## Become the persona -Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Use a generic domain-analyst/architect, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: design` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; archetype stance is the tie-break: domain-analyst / interface-architect); its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. No persona seeded or matched? Fall back to the per-phase presets — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (specify): a 15-year domain analyst who asks rather than assumes — every Reject earns a named error code, every assumption is ranked before it is believed (setup rides this stance). +- Preset (plan): a systems architect — contract-first, cites only anchors that exist in the tree, and sizes the change's reach before freezing scope. ## What you own (the design span: setup → specify → plan) - **Setup** (fresh project only) — point ADD at the repo and draft the whole foundation yourself: brownfield is mapped silently from code, greenfield runs the short 4-lens interview (Domain · Spec · Users · Decisions), lowest-confidence-first. Seed `.add/personas/` from PROJECT.md plus the vendored `.add/personas-teacher/` library, never clobbering an existing file. diff --git a/add-method/src/add_method/_bundled/agents/add-verify.md b/add-method/src/add_method/_bundled/agents/add-verify.md index 38e22b1b..918dde81 100644 --- a/add-method/src/add_method/_bundled/agents/add-verify.md +++ b/add-method/src/add_method/_bundled/agents/add-verify.md @@ -11,7 +11,8 @@ You are the **verify** specialist in ADD's phase-agent roster — a verifier who At spawn, read YOUR phase guide from the project's skill tree (`.claude/skills/add/phases/`): `phases/6-verify.md` (it carries the post-gate Observe block). It is the method depth for your span; the orchestrator does not pre-read it for you. ## Become the persona (do this FIRST — before acting on any task-specific instructions in your prompt) -Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Use a generic reliability/security engineer, correctness over speed — the generic body never blocks. +Load the fit `.add/personas/.md` and BECOME it — select a `flow: verify` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), fall back to `flow: advisor` when none declares verify, then by archetype: a Code-Reviewer / security-gatekeeper stance for verify (`tdd-verifier` is this project's closest fit when nothing more specific matches), a reliability-analyst stance for observe; its `## Critical Rules` are your constraints, its `## Success Metrics` are your done-bar. Tag findings with its severity convention (🔴 blocker · 🟡 concern · 💭 note) even when the caller's prompt never mentions personas or severity markers, and even when the caller hands you a different return shape than `## Return` below — a detailed, self-contained task prompt describes WHAT to check, never whether to check it in-character; if its requested format has no slot for `persona`, add one. No persona seeded or matched? Fall back to the preset — the fallback tier only, distilled from the vendored teacher library; a preset never blocks and never lowers a gate: +- Preset (verify): an adversarial reviewer, then a reliability analyst — refutes the green before trusting it, treats security findings as HARD-STOP always, then watches reality and drafts the next spec delta (§7). ## What you own (verify → observe) - **Before build** — fill the Build expectations block (observable outcomes derived from the scenarios and the frozen contract); confirm each against real evidence at the gate, not just a green test. diff --git a/add-method/tooling/test_persona_presets.py b/add-method/tooling/test_persona_presets.py new file mode 100644 index 00000000..eb521280 --- /dev/null +++ b/add-method/tooling/test_persona_presets.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Red/green tests for persona-presets (six-phase-loop 5/6, frozen v1): each +roster bundle agent's no-persona fallback upgrades from one thin generic line +to a named teacher-grade expert stance PER OWNED PHASE (`Preset ():`). +The project-persona routing stays first; a preset never blocks, never lowers +a gate. + +Run: python3 -m unittest test_persona_presets -v +""" +import hashlib +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +PKG_ROOT = HERE.parent +REPO_ROOT = PKG_ROOT.parent +_AGENTS = PKG_ROOT / "agents" +_BUNDLED = PKG_ROOT / "src" / "add_method" / "_bundled" / "agents" + +# agent -> the per-phase preset markers its persona section must carry (M1-M3) +_PRESETS = { + "add-design.md": ["Preset (specify):", "Preset (plan):"], + "add-build.md": ["Preset (tests):", "Preset (build):"], + "add-verify.md": ["Preset (verify):"], +} +# the routing-first sentence that must SURVIVE the fallback replacement (M4) +_ROUTING_MARKER = "Load the fit `.add/personas/.md` and BECOME it" + + +def _md5(p: Path) -> str: + return hashlib.md5(p.read_bytes()).hexdigest() + + +class PresetCensusTest(unittest.TestCase): + def test_each_agent_carries_a_preset_per_owned_phase(self): # M1-M3 + Accept + for name, markers in _PRESETS.items(): + text = (_AGENTS / name).read_text(encoding="utf-8") + missing = [m for m in markers if m not in text] + self.assertEqual(missing, [], + f"{name} must carry a named preset per owned phase; " + f"missing: {missing}") + + def test_no_bare_generic_fallback_left(self): # M1-M3 (upgrade, not add) + for name in _PRESETS: + text = (_AGENTS / name).read_text(encoding="utf-8") + self.assertNotIn("Use a generic", text, + f"{name}: the thin generic fallback must be replaced " + f"by the per-phase presets, not kept beside them") + + def test_routing_first_sentence_survives(self): # M4 + R1 + for name in _PRESETS: + text = (_AGENTS / name).read_text(encoding="utf-8") + self.assertIn(_ROUTING_MARKER, text, + f"{name}: project-persona routing must stay first — " + f"presets are the fallback tier only") + self.assertIn("never lowers a gate", text, + f"{name}: the preset tier must state it never lowers a gate") + + +class ParityTest(unittest.TestCase): + def test_presets_synced_x2(self): # M5 + installed = REPO_ROOT / ".claude" / "agents" + for name in _PRESETS: + canon = _md5(_AGENTS / name) + self.assertEqual(_md5(_BUNDLED / name), canon, f"bundled drifted: {name}") + self.assertEqual(_md5(installed / name), canon, f"installed stale: {name}") + + +if __name__ == "__main__": + unittest.main() From 062f594c72b504309dcd6e901d74e5fc1e64cf9a Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 02:25:55 +0700 Subject: [PATCH 7/9] =?UTF-8?q?feat(method):=20build-entry-spec-echo=20?= =?UTF-8?q?=E2=80=94=20the=20tick=20INTO=20build=20re-renders=20WHAT=20to?= =?UTF-8?q?=20build=20(six-phase-loop=206/6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interview decision made mechanism: at every successful tests->build tick the engine prints `build to (frozen plan):` + the §1 Must/Reject bullet first-lines + the frozen §3 contract-fence head, so the builder starts from the spec on the screen, not from memory. - add.py: NEW _spec_echo (PURE read beside _scope_echo; bullet walk stops at the next top-level `Key:` line, continuations never echo) + a wrapped tail call in _build_entry — AFTER every guard and snapshot, so a refused entry never echoes and BOTH entries (advance + the `phase build` override) fire; fail-open: exit code, state.json, and TASK.md are identical either way - ENGINE_MD5 re-aimed bf89b033; ENGINE_PKG_MD5 deliberately NOT re-aimed (add_engine/ untouched — the digest proved it unchanged); SEAMS _declared_scope pin 5603->5653; engine synced x3 twins - SELF-PROVED: the task's own re-cross into build rendered its Must/Reject/ contract echo live Tests: NEW test_build_entry_spec_echo.py (9: echo at advance + at the phase override · line order · forced-raise never blocks the tick · malformed §1/§3 partial-echo no-crash · refused crossing never echoes · --skip-freeze DRAFT crossing still echoes · pure-read byte check · engine-twin parity). Full fence 3554/3554 OK. LESSON: declare §4 tests as the exact FILE — a dir-form declaration sweeps the build targets into the tamper snapshot (recovered via signed re-cross, the expectations-first recipe). refs: six-phase-loop milestone, stacked branch feat/six-phase-loop author: Tin Dang --- .add/SEAMS.md | 2 +- .add/state.json | 62 ++++- .add/tasks/build-entry-spec-echo/TASK.md | 100 ++++++++ .../src/add_method/_bundled/tooling/add.py | 50 ++++ .../add_method/_bundled/tooling/engine_pin.py | 2 +- add-method/tooling/add.py | 50 ++++ add-method/tooling/engine_pin.py | 2 +- .../tooling/test_build_entry_spec_echo.py | 224 ++++++++++++++++++ 8 files changed, 486 insertions(+), 6 deletions(-) create mode 100644 .add/tasks/build-entry-spec-echo/TASK.md create mode 100644 add-method/tooling/test_build_entry_spec_echo.py diff --git a/.add/SEAMS.md b/.add/SEAMS.md index 2210104d..c955556c 100644 --- a/.add/SEAMS.md +++ b/.add/SEAMS.md @@ -54,7 +54,7 @@ Citations: 232 files reference "byte-identical" in `.add/tasks/` — method: ## scope-token-grammar Name: §5 "Scope (may touch):" token-resolution grammar -Anchor: `add-method/tooling/add.py:5603` (`_declared_scope`) +Anchor: `add-method/tooling/add.py:5653` (`_declared_scope`) Contract: `_declared_scope` reads ONLY the first physical line after the §5 header — a wrapped multi-line list silently truncates. Each backticked token then resolves independently: `./...` = this task's dir, any token containing `/` = project-root-relative, diff --git a/.add/state.json b/.add/state.json index d3832b76..3d3350cd 100644 --- a/.add/state.json +++ b/.add/state.json @@ -1,7 +1,7 @@ { "project": "AIDD / ADD Methodology", "stage": "mvp", - "active_task": "persona-presets", + "active_task": "build-entry-spec-echo", "tasks": { "add-check": { "title": "add.py check: validate .add project integrity", @@ -9270,10 +9270,66 @@ "email": "tindang.ht97@gmail.com", "source": "git" } + }, + "build-entry-spec-echo": { + "title": "Build-entry spec echo: Must/Reject + contract summary at the tests->build tick", + "phase": "done", + "gate": "PASS", + "milestone": "six-phase-loop", + "depends_on": [], + "created": "2026-07-13T19:09:58+00:00", + "updated": "2026-07-13T19:25:16+00:00", + "fast": true, + "freeze": { + "version": "v1", + "frozen_at": "2026-07-13T19:14:39+00:00", + "approved_by": "Tin Dang", + "actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } + }, + "flag_verified": true, + "tripwire": { + "contract_md5": "7a68ac1a2d9e1ca5cd38f47932f6e858", + "tests": { + "add-method/tooling/test_build_entry_spec_echo.py": "a6080bfd778a1c825bce598745680e31" + } + }, + "scope": { + "declared": [ + "add-method/tooling/", + "add-method/src/add_method/_bundled/tooling/", + ".add/tooling/", + "add-method/.add/" + ], + "snapshot_md5": "c17eeecad97e540b1bbe3ae4f543add2" + }, + "heal": { + "attempts": 1, + "history": [ + { + "at": "2026-07-13T19:24:02+00:00", + "reason": "tamper_detected:build_tampered:add-method/tooling/add.py,build_tampered:add-method/tooling/engine_pin.py", + "source": "tamper" + } + ] + }, + "recross": { + "by": "Tin Dang", + "at": "2026-07-13T19:25:15+00:00", + "from_phase": "build" + }, + "gate_actor": { + "name": "Tin Dang", + "email": "tindang.ht97@gmail.com", + "source": "git" + } } }, "created": "2026-05-28T15:39:04+00:00", - "updated": "2026-07-13T19:09:34+00:00", + "updated": "2026-07-13T19:25:16+00:00", "milestones": { "v1-1": { "title": "v1.1 \u2014 adoption & ergonomics", @@ -10535,7 +10591,7 @@ "plan-legibility": "relations-surface", "ceremony-to-effort": "fold-draft-at-close", "call-floor": "skill-orient-split", - "six-phase-loop": "persona-presets" + "six-phase-loop": "build-entry-spec-echo" }, "todos": [ { diff --git a/.add/tasks/build-entry-spec-echo/TASK.md b/.add/tasks/build-entry-spec-echo/TASK.md new file mode 100644 index 00000000..cc2fdd22 --- /dev/null +++ b/.add/tasks/build-entry-spec-echo/TASK.md @@ -0,0 +1,100 @@ +# TASK: Build-entry spec echo: Must/Reject + contract summary at the tests->build tick + +slug: build-entry-spec-echo · created: 2026-07-14 · stage: mvp +milestone: six-phase-loop +autonomy: auto +phase: done +fast: true + +> Fast lane — one small task, minimal sections, filled top-to-bottom. The trust floor still +> holds: a FROZEN §3 contract · ≥1 red test before build · a recorded §6 gate (security = HARD-STOP). +> The acceptance scenario collapses into §1 `Accept:`; the observe note is one optional line at the gate. + +--- + +## 1 · SPECIFY — the rules + +> Project the expectations from the milestone Ground + this request — light, not re-invented. +Feature: build-entry spec echo — the tick INTO build re-renders WHAT to build (§1 Must/Reject + the frozen §3 contract head), so the builder starts from the spec on the screen, not from memory +Must: + - a successful tests->build tick prints `build to (frozen plan):` then one ` must: ` per §1 Must bullet, one ` reject: ` per §1 Reject bullet, then ` contract: ` + - the echo fires on BOTH build entries — `advance` (tests->build) AND the `phase build` admin override — because both funnel through _build_entry; it renders at the TAIL of the gate stack, after every guard + snapshot, so a refused entry never echoes + - pure read, fail-open: state.json, TASK.md, and the exit code are byte-identical with or without the echo; the call is wrapped so any parse failure prints what parsed (possibly nothing) — never a die, never a traceback +Reject: + - any echo failure blocking a build entry -> fail-open (wrapped; the tick completes and state is written exactly as before) + - echo on a refused crossing (contract_not_frozen · build_expectations_unfilled · unflagged_freeze) -> never; validate-then-write puts every guard before it +Accept: Given a fast task with a frozen §3 and two Must + one Reject bullets, When `advance` ticks tests->build, Then stdout carries `build to (frozen plan):` + one line per bullet + the §3 fence head, and the tick's state/TASK.md writes match a pre-echo build entry. +Boundary: three §1/§3 shapes the echo must speak — well-formed (full echo) · missing/malformed Must/Reject block or contract fence (that part silently absent, no crash) · --skip-freeze DRAFT-§3 crossing (echo still renders whatever exists) +Assumptions: ⚠ no existing suite pins the exact stdout TAIL of a tests->build advance — why: prior fences grepped advance output via assertIn only; if wrong: the fence names the pinning suite and the echo line-shape adjusts (cost: one re-run) + +--- + +## 3 · PLAN — the change plan: ground · contract · build-strategy + +### Grounding +Touches (files · symbols): add-method/tooling/add.py:_build_entry (tail insert, after the spec-dialect loop) · add.py:_spec_echo (NEW pure-read helper, beside _scope_echo) · add.py:_raw_phase_bodies (read-only reuse) +Context (working folder): SEAMS.md `_declared_scope` pin (add.py:5603) drifts on this insertion — re-pin; ENGINE_MD5 + ENGINE_PKG_MD5 re-aim; engine syncs x3 twins +Honors (patterns / conventions): fail-open derived-render (mirrors _scope_echo's freeze call + the dialect warnings) · validate-then-write (echo strictly after every guard/snapshot) · Seams consulted: .add/SEAMS.md +Anchors the contract cites: _build_entry · _scope_echo · _raw_phase_bodies +Ground SHA: be74a90 — stamped by freeze + +### Contract + +``` +_spec_echo(root: Path, slug: str) -> None # NEW, PURE read — prints only, never writes + renders, in order: + build to (frozen plan): + must: + reject: + contract: + a missing/malformed section -> that part silently absent (never a die) +call site: TAIL of _build_entry (after the spec-dialect loop) — so BOTH entries + (`advance` tests->build AND `phase build`) echo; wrapped + try/except Exception -> pass (fail-open: exit code, state.json, and TASK.md + are byte-identical with or without the echo) +``` + +`Least-sure flag surfaced at freeze:` [test] the byte-identity Must — proving state/TASK.md identical "with or without the echo" can't run two engines side by side; the test approximates it by forcing the helper to raise and asserting the tick still completes with identical state — why: monkeypatch is the only seam; if wrong: the assert re-shapes to behavioral-only (cost: one test rewrite, no engine change) +Status: FROZEN @ v1 — approved by Tin Dang +### Build-strategy +Scope (may touch): `add-method/tooling/` `add-method/src/add_method/_bundled/tooling/` `.add/tooling/` `add-method/.add/` +Strategy & known-problem fixes: 1) red test_build_entry_spec_echo.py 2) _spec_echo helper beside _scope_echo (regex over _raw_phase_bodies bodies 1+3) 3) wrapped tail call in _build_entry 4) sync engine x3 · re-pin ENGINE_MD5/PKG · check SEAMS line-pin drift (add.py grows ~40 lines) 5) full fence backgrounded; trap: the §1 Must/Reject parse must stop at the next top-level `Key:` line, not swallow Accept/Boundary +Approach (domain strategy): derived-render tail print over already-parsed section bodies — obvious, correctness-first + +### AI-verify record (required when gate_mode: ai-plan-verify) +- [ ] §3 PLAN grounding anchors resolve in the current tree +- [ ] §1 every Must + every Reject present, each Reject paired with an error code +- [ ] §3 Contract shape is concrete (no template placeholder text remains) +- [ ] Lowest-confidence flag surfaced and substantive (mirrors unflagged_freeze's own bar) +Verified by: · at: + +--- + +## 4 · TESTS — failing-first (red) + +Plan: test_build_entry_spec_echo.py — echo at `advance` tests->build (must/reject/contract lines, in order, after the gate stack's own output) · echo at the `phase build` override · fail-open (helper forced to raise -> tick completes, state written) · malformed §1/§3 -> partial echo, no crash · refused crossing (DRAFT §3, no skip) -> no echo · engine-twin parity. +Tests live in: `add-method/tooling/test_build_entry_spec_echo.py` (the engine's own suite; the file named exactly — the dir form swept the build targets into the tamper snapshot) · MUST run red before Build. + +--- + +## 5 · BUILD — AI writes the code (execution) + +> The change plan was frozen in §3 PLAN. Build to it: honor the §3 Build-strategy Scope; improve on the strategy if the code teaches you better. +Strategy actually used: as planned — _spec_echo beside _scope_echo (bullet walk stops at the next top-level `Key:` line, the noted trap), wrapped tail call after the dialect loop; ENGINE_MD5 re-aimed bf89b033, ENGINE_PKG_MD5 correctly NOT re-aimed (add_engine/ untouched), SEAMS _declared_scope pin 5603->5653 +Code lives in: `./src/` · Constraints: change no test, no frozen contract; stay inside the §3 Build-strategy Scope; allow-list packages only. + +--- + +## 6 · VERIFY — evidence + gate + +- [x] all tests pass · coverage held · no test or contract altered during build +- [x] green was EARNED — no overfit / vacuous asserts / stubbed-away logic +- [x] input dialect held — tests speak the spec's example formats (spec-dialect floor) +- [x] no exposed secrets, injection openings, or unexpected dependencies (security = HARD-STOP) + +Build expectations (from §1 Accept + §3 CONTRACT): a tests->build tick on a task with a frozen §3 prints `build to (frozen plan):` + one must/reject line per §1 bullet + the contract fence head, on both the advance and `phase build` paths, and a forced echo failure never blocks the tick — confirmed by test_build_entry_spec_echo.py + the full fence + +### GATE RECORD +Outcome: PASS +Reviewed by: Tin Dang · date: 2026-07-14 + diff --git a/add-method/src/add_method/_bundled/tooling/add.py b/add-method/src/add_method/_bundled/tooling/add.py index 805ee739..3716a803 100644 --- a/add-method/src/add_method/_bundled/tooling/add.py +++ b/add-method/src/add_method/_bundled/tooling/add.py @@ -1069,6 +1069,48 @@ def _touches_paths() -> list[str]: print("scope (proposed from §3 Touches): " + " ".join(f"`{p}`" for p in paths)) +def _spec_echo(root: Path, slug: str) -> None: + """build-entry-spec-echo: re-render WHAT to build at the tick INTO build — the §1 + Must/Reject bullet first-lines + the §3 contract-fence head — so the builder starts + from the spec on the screen, not from memory. PURE read, prints only; a missing or + malformed section is silently absent (the _build_entry call site wraps fail-open).""" + bodies = _raw_phase_bodies(root, slug) + + def _bullets(body: str, key: str) -> list[str]: + # first line of each `- ` bullet under the top-level `Key:` line; a bullet's + # continuation lines are skipped; the next top-level line ends the block. + out: list[str] = [] + lines = body.splitlines() + starts = [n for n, ln in enumerate(lines) if ln.strip() == f"{key}:"] + if not starts: + return out + for ln in lines[starts[0] + 1:]: + s = ln.strip() + if s.startswith("- "): + out.append(s[2:].strip()) + elif ln[:1].isspace() and s: + continue + else: + break + return out + + musts = _bullets(bodies.get(1, ""), "Must") + rejects = _bullets(bodies.get(1, ""), "Reject") + m = re.search(r"(?ms)^```[^\n]*\n(.*?)^```", bodies.get(3, "")) + head = "" + if m: + head = next((ln.strip() for ln in m.group(1).splitlines() if ln.strip()), "") + if not (musts or rejects or head): + return + print("build to (frozen plan):") + for item in musts: + print(f" must: {item}") + for item in rejects: + print(f" reject: {item}") + if head: + print(f" contract: {head}") + + def cmd_freeze(args: argparse.Namespace) -> None: """The §3 contract-freeze write command — the 5th engine-WRITTEN human approval (task freeze-actor-stamp), joining lock · gate · milestone-done · release. Flips the target @@ -1444,6 +1486,14 @@ def _build_entry(root: Path, state: dict, slug: str, skip_freeze: bool = False, "than the spec's own examples can stay green through a real crash (benchmark " "wm2 evidence). Add one test using the contract's literal format, then " "re-snapshot: add.py re-cross --by ") + # build-entry spec echo (six-phase-loop): the tick INTO build re-renders WHAT to + # build — §1 Must/Reject + the §3 contract head — on BOTH entries (advance and the + # `phase build` override funnel here). TAIL of the stack, so a refused entry never + # echoes; fail-open, so exit code, state, and TASK.md are identical either way. + try: + _spec_echo(root, slug) + except Exception: + pass def cmd_phase(args: argparse.Namespace) -> None: diff --git a/add-method/src/add_method/_bundled/tooling/engine_pin.py b/add-method/src/add_method/_bundled/tooling/engine_pin.py index c989474f..922b2945 100644 --- a/add-method/src/add_method/_bundled/tooling/engine_pin.py +++ b/add-method/src/add_method/_bundled/tooling/engine_pin.py @@ -17,5 +17,5 @@ this file only ever holds the newest pointer. """ -ENGINE_MD5 = "5d5e053843cf456a9c9f490267c92793" # re-aimed @ phase-merge-verify (six-phase-loop 2/6: observe PHASE merged into verify — skip grammar retired, gate carries the fold nudge + vestigial-skips note, --fill/--section repaired to the _PHASE_SECTIONS table). prior: b27ce845… @ phase-merge-specify +ENGINE_MD5 = "bf89b03351ae87d72fb3a2dc5f5729af" # re-aimed @ build-entry-spec-echo (six-phase-loop 6/6: _spec_echo pure-read helper + fail-open tail call in _build_entry — the tick INTO build re-renders §1 Must/Reject + the §3 contract head). prior: 5d5e0538… @ phase-merge-verify ENGINE_PKG_MD5 = "fc40ad47544db6f5204b6197b95daf04" # re-aimed @ phase-merge-verify (constants.py: PHASES drops observe; _SKIPPABLE_PHASES=(); PHASE_GUIDE/OWNER/GROUPS/AGENT follow). prior: 870a4ce0… @ phase-merge-specify diff --git a/add-method/tooling/add.py b/add-method/tooling/add.py index 805ee739..3716a803 100644 --- a/add-method/tooling/add.py +++ b/add-method/tooling/add.py @@ -1069,6 +1069,48 @@ def _touches_paths() -> list[str]: print("scope (proposed from §3 Touches): " + " ".join(f"`{p}`" for p in paths)) +def _spec_echo(root: Path, slug: str) -> None: + """build-entry-spec-echo: re-render WHAT to build at the tick INTO build — the §1 + Must/Reject bullet first-lines + the §3 contract-fence head — so the builder starts + from the spec on the screen, not from memory. PURE read, prints only; a missing or + malformed section is silently absent (the _build_entry call site wraps fail-open).""" + bodies = _raw_phase_bodies(root, slug) + + def _bullets(body: str, key: str) -> list[str]: + # first line of each `- ` bullet under the top-level `Key:` line; a bullet's + # continuation lines are skipped; the next top-level line ends the block. + out: list[str] = [] + lines = body.splitlines() + starts = [n for n, ln in enumerate(lines) if ln.strip() == f"{key}:"] + if not starts: + return out + for ln in lines[starts[0] + 1:]: + s = ln.strip() + if s.startswith("- "): + out.append(s[2:].strip()) + elif ln[:1].isspace() and s: + continue + else: + break + return out + + musts = _bullets(bodies.get(1, ""), "Must") + rejects = _bullets(bodies.get(1, ""), "Reject") + m = re.search(r"(?ms)^```[^\n]*\n(.*?)^```", bodies.get(3, "")) + head = "" + if m: + head = next((ln.strip() for ln in m.group(1).splitlines() if ln.strip()), "") + if not (musts or rejects or head): + return + print("build to (frozen plan):") + for item in musts: + print(f" must: {item}") + for item in rejects: + print(f" reject: {item}") + if head: + print(f" contract: {head}") + + def cmd_freeze(args: argparse.Namespace) -> None: """The §3 contract-freeze write command — the 5th engine-WRITTEN human approval (task freeze-actor-stamp), joining lock · gate · milestone-done · release. Flips the target @@ -1444,6 +1486,14 @@ def _build_entry(root: Path, state: dict, slug: str, skip_freeze: bool = False, "than the spec's own examples can stay green through a real crash (benchmark " "wm2 evidence). Add one test using the contract's literal format, then " "re-snapshot: add.py re-cross --by ") + # build-entry spec echo (six-phase-loop): the tick INTO build re-renders WHAT to + # build — §1 Must/Reject + the §3 contract head — on BOTH entries (advance and the + # `phase build` override funnel here). TAIL of the stack, so a refused entry never + # echoes; fail-open, so exit code, state, and TASK.md are identical either way. + try: + _spec_echo(root, slug) + except Exception: + pass def cmd_phase(args: argparse.Namespace) -> None: diff --git a/add-method/tooling/engine_pin.py b/add-method/tooling/engine_pin.py index c989474f..922b2945 100644 --- a/add-method/tooling/engine_pin.py +++ b/add-method/tooling/engine_pin.py @@ -17,5 +17,5 @@ this file only ever holds the newest pointer. """ -ENGINE_MD5 = "5d5e053843cf456a9c9f490267c92793" # re-aimed @ phase-merge-verify (six-phase-loop 2/6: observe PHASE merged into verify — skip grammar retired, gate carries the fold nudge + vestigial-skips note, --fill/--section repaired to the _PHASE_SECTIONS table). prior: b27ce845… @ phase-merge-specify +ENGINE_MD5 = "bf89b03351ae87d72fb3a2dc5f5729af" # re-aimed @ build-entry-spec-echo (six-phase-loop 6/6: _spec_echo pure-read helper + fail-open tail call in _build_entry — the tick INTO build re-renders §1 Must/Reject + the §3 contract head). prior: 5d5e0538… @ phase-merge-verify ENGINE_PKG_MD5 = "fc40ad47544db6f5204b6197b95daf04" # re-aimed @ phase-merge-verify (constants.py: PHASES drops observe; _SKIPPABLE_PHASES=(); PHASE_GUIDE/OWNER/GROUPS/AGENT follow). prior: 870a4ce0… @ phase-merge-specify diff --git a/add-method/tooling/test_build_entry_spec_echo.py b/add-method/tooling/test_build_entry_spec_echo.py new file mode 100644 index 00000000..6a557dfd --- /dev/null +++ b/add-method/tooling/test_build_entry_spec_echo.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""Red/green tests for build-entry-spec-echo (six-phase-loop 6/6, frozen v1): the +tick INTO build re-renders WHAT to build — §1 Must/Reject bullet first-lines + +the frozen §3 contract-fence head — via a NEW pure-read `_spec_echo` helper +called fail-open at the TAIL of `_build_entry`, so BOTH entries (`advance` +tests->build AND the `phase build` override) echo, a refused entry never does, +and an echo failure never blocks the tick. + +Run: python3 -m unittest test_build_entry_spec_echo -v +""" +import hashlib +import io +import json +import os +import re +import shutil +import tempfile +import unittest +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path + +import add + +HERE = Path(__file__).resolve().parent +PKG_ROOT = HERE.parent +REPO_ROOT = PKG_ROOT.parent + +_SEC1 = """Feature: fixture feature +Must: + - first must behavior + continuation detail that must NOT echo as its own line + - second must behavior +Reject: + - bad input shape -> "err_bad_shape" +Accept: Given x When y Then z +""" + +_SEC3 = """### Contract + +``` +do_thing(x) -> Result FIXTURE-CONTRACT-HEAD +fields: a - b +``` + +Status: DRAFT +""" + + +class _Harness(unittest.TestCase): + def setUp(self): + self._cwd = Path.cwd() + self.tmp = Path(tempfile.mkdtemp(prefix="add-bese-")).resolve() + self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True) + self.addCleanup(os.chdir, self._cwd) + os.chdir(self.tmp) + + def _run(self, *argv): + out, err = io.StringIO(), io.StringIO() + code = 0 + try: + with redirect_stdout(out), redirect_stderr(err): + add.main(list(argv)) + except SystemExit as e: + code = e.code if isinstance(e.code, int) else 1 + return out.getvalue() + err.getvalue(), code + + def _ok(self, *argv): + text, code = self._run(*argv) + self.assertEqual(code, 0, f"{argv} exited {code}: {text}") + return text + + def _board(self): + self._ok("init", "--name", "demo", "--stage", "mvp") + self._ok("lock", "--force") + self._ok("new-task", "t", "--title", "T", "--oneshot") + + def _task_md(self, slug="t"): + return self.tmp / ".add" / "tasks" / slug / "TASK.md" + + def _fill_spec(self, slug="t", sec1=_SEC1, sec3=_SEC3): + """Replace §1 and §3 bodies wholesale with known fixture content.""" + p = self._task_md(slug) + text = p.read_text(encoding="utf-8") + text = re.sub(r"(?s)(^## 1 ·[^\n]*\n).*?(?=^## )", + lambda m: m.group(1) + sec1 + "\n", text, count=1, flags=re.M) + text = re.sub(r"(?s)(^## 3 ·[^\n]*\n).*?(?=^## )", + lambda m: m.group(1) + sec3 + "\n", text, count=1, flags=re.M) + p.write_text(text, encoding="utf-8") + + def _freeze(self, slug="t"): + p = self._task_md(slug) + p.write_text(p.read_text(encoding="utf-8").replace( + "Status: DRAFT", + "Status: FROZEN @ v1 — approved by Tester 2026-07-14.\n" + "Least-sure flag surfaced at freeze: [contract] fixture stub — cost: none", + ), encoding="utf-8") + + def _phase(self, slug="t"): + state = json.loads((self.tmp / ".add" / "state.json").read_text(encoding="utf-8")) + return state["tasks"][slug]["phase"] + + def _to_tests(self): + """Drive t specify -> tests behind a frozen fixture §3.""" + self._fill_spec() + self._freeze() + self._ok("advance", "t") # specify -> plan + self._ok("advance", "t") # plan -> tests (freeze gate holds: §3 frozen) + + +class EchoAtAdvanceTest(_Harness): + def test_advance_into_build_echoes_spec(self): # M1 + M2 + Accept + self._board() + self._to_tests() + out = self._ok("advance", "t") # tests -> build: the tick + self.assertEqual(self._phase(), "build") + self.assertIn("build to (frozen plan):", out) + self.assertIn("must: first must behavior", out) + self.assertIn("must: second must behavior", out) + self.assertIn('reject: bad input shape -> "err_bad_shape"', out) + self.assertIn("contract: do_thing(x) -> Result FIXTURE-CONTRACT-HEAD", out) + self.assertNotIn("continuation detail", out, + "only a bullet's FIRST line echoes") + + def test_echo_lines_render_in_order(self): # M1 (shape: header, must, reject, contract) + self._board() + self._to_tests() + out = self._ok("advance", "t") + idx = [out.index("build to (frozen plan):"), + out.index("must: first must behavior"), + out.index('reject: bad input shape'), + out.index("contract: do_thing")] + self.assertEqual(idx, sorted(idx), + "echo must render header -> must -> reject -> contract, in order") + + +class EchoAtPhaseOverrideTest(_Harness): + def test_phase_build_override_echoes_too(self): # M2 (both entries funnel through _build_entry) + self._board() + self._fill_spec() + self._freeze() + out = self._ok("phase", "build", "t") + self.assertEqual(self._phase(), "build") + self.assertIn("build to (frozen plan):", out) + self.assertIn("must: first must behavior", out) + self.assertIn("contract: do_thing(x) -> Result FIXTURE-CONTRACT-HEAD", out) + + +class FailOpenTest(_Harness): + def test_helper_raise_never_blocks_the_tick(self): # M3 + R1 (least-sure flag's behavioral form) + self._board() + self._to_tests() + real = add._spec_echo + + def _boom(*a, **k): + raise RuntimeError("forced echo failure") + + add._spec_echo = _boom + self.addCleanup(setattr, add, "_spec_echo", real) + out, code = self._run("advance", "t") + self.assertEqual(code, 0, + f"a raising echo must never block the build entry: {out}") + self.assertEqual(self._phase(), "build") + state = json.loads((self.tmp / ".add" / "state.json").read_text(encoding="utf-8")) + self.assertIn("tripwire", state["tasks"]["t"], + "the gate stack's own writes must land exactly as before") + self.assertNotIn("Traceback", out) + + def test_malformed_sections_partial_echo_no_crash(self): # Boundary (missing Must/Reject + no fence) + self._board() + self._fill_spec(sec1="Feature: bare fixture, no rule blocks\n", + sec3="### Contract\n\nno fence here at all\n\nStatus: DRAFT\n") + self._freeze() + out = self._ok("phase", "build", "t") # tick completes + self.assertEqual(self._phase(), "build") + self.assertNotIn("must:", out) + self.assertNotIn("contract: ", out) + self.assertNotIn("Traceback", out) + + +class GuardsPrecedeEchoTest(_Harness): + def test_refused_crossing_never_echoes(self): # R2 (validate-then-write) + self._board() + self._fill_spec() # §3 stays DRAFT + out, code = self._run("phase", "build", "t") + self.assertNotEqual(code, 0) + self.assertIn("contract_not_frozen", out) + self.assertNotIn("build to (frozen plan):", out, + "a refused entry must not echo") + + def test_skip_freeze_draft_crossing_still_echoes(self): # Boundary (--skip-freeze DRAFT §3) + self._board() + self._fill_spec() # §3 stays DRAFT + out = self._ok("phase", "build", "t", "--skip-freeze") + self.assertEqual(self._phase(), "build") + self.assertIn("must: first must behavior", out, + "the echo renders whatever exists, even on a recorded-skip crossing") + + +class PurityAndParityTest(_Harness): + def test_spec_echo_is_a_pure_read(self): # M3 (never writes) + self._board() + self._fill_spec() + self._freeze() + before_md = self._task_md().read_bytes() + before_state = (self.tmp / ".add" / "state.json").read_bytes() + out, err = io.StringIO(), io.StringIO() + root = self.tmp / ".add" + with redirect_stdout(out), redirect_stderr(err): + add._spec_echo(root, "t") + self.assertEqual(self._task_md().read_bytes(), before_md) + self.assertEqual((self.tmp / ".add" / "state.json").read_bytes(), before_state) + self.assertIn("must: first must behavior", out.getvalue()) + + def test_engine_twins_carry_the_echo(self): # sync x3 (twin parity on add.py) + canon = hashlib.md5((PKG_ROOT / "tooling" / "add.py").read_bytes()).hexdigest() + for twin in (REPO_ROOT / ".add" / "tooling" / "add.py", + PKG_ROOT / ".add" / "tooling" / "add.py", + PKG_ROOT / "src" / "add_method" / "_bundled" / "tooling" / "add.py"): + self.assertEqual(hashlib.md5(twin.read_bytes()).hexdigest(), canon, + f"engine twin drifted: {twin}") + + +if __name__ == "__main__": + unittest.main() From 5ce3bb68542267d3736af91cadf3411ac0137191 Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 03:37:48 +0700 Subject: [PATCH 8/9] =?UTF-8?q?docs(method):=20six-phase-loop=20close=20re?= =?UTF-8?q?view=20=E2=80=94=206/6=20tasks=20ticked,=207/8=20exit=20criteri?= =?UTF-8?q?a=20evidenced?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Milestone close-review bookkeeping: all six task boxes checked, seven exit criteria checked with (verify: ...) citations (test suites + fence 3554/3554 + the self-proved build-entry echo), rationale placeholder filled with the confirmed intake classification. The goal-gate holds the milestone OPEN on the eighth criterion — the paid WM1 re-measure (fid >= 0.97, calls <= 12) — which is human-gated: run or accept-partial. refs: six-phase-loop milestone, stacked branch feat/six-phase-loop author: Tin Dang --- .add/milestones/six-phase-loop/MILESTONE.md | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.add/milestones/six-phase-loop/MILESTONE.md b/.add/milestones/six-phase-loop/MILESTONE.md index ff4a6cbc..2fdd22c2 100644 --- a/.add/milestones/six-phase-loop/MILESTONE.md +++ b/.add/milestones/six-phase-loop/MILESTONE.md @@ -1,7 +1,7 @@ # MILESTONE: Six-phase loop with bundle-owned subagents goal: Merge specify+scenarios and verify+observe (8->6 phases), disclose phase guides into the lean roster's bundle agents so the orchestrator loads only SKILL.md, add per-phase persona presets + a build-entry spec echo -rationale: +rationale: sub-milestone — the user's compress-steps + expert-subagent-per-step request, interview-confirmed 2026-07-14 (TRUE 8->6 merge · disclosure into bundle agents · presets · spec echo) stage: mvp · status: active · created: 2026-07-13T17:03:37+00:00 release: pending relations: @@ -35,21 +35,21 @@ IN: engine phase-list 8->6 (specify absorbs scenarios; verify absorbs observe) + OUT: gate SEMANTICS (one recorded outcome, security HARD-STOP, tamper/scope guards — all unchanged) · the freeze position (plan->tests) · roster membership changes (stays 5) · stdout terseness · benchmark harness changes. ## Tasks (breadth-first; engine merges FIRST — guides/agents re-cut against the new truth) -- [ ] phase-merge-specify depends-on: none — scenarios fold INTO specify (§2 Given/When/Then becomes the §1 rules' scenario block; one drafting phase); engine PHASES list, section indices, advance/recipe paths, TASK.md.tmpl + fast twin; migration maps in-flight tasks (scenarios->specify) — the phase-index-vs-section bug class from expectations-first is the named trap -- [ ] phase-merge-verify depends-on: phase-merge-specify — observe folds INTO verify (§7 spec-delta capture becomes an optional post-gate block of §6; deltas/fold lifecycles unchanged, they read the file not the phase); gate stays the phase's one outcome; 6-phase list lands: setup · specify · plan · tests · build · verify -- [ ] guide-recut depends-on: phase-merge-verify — phases/*.md re-cut to 6 guides (1-specify absorbs 2-scenarios; 6-verify absorbs 7-observe); pools re-anchored (human-signed restructure, POOLS comments cite this milestone); orchestrator fast-lane read path <= 30KB finally lands -- [ ] bundle-disclosure depends-on: guide-recut — the orchestrator loads ONLY SKILL.md; each roster agent's prompt names + loads its bundle's guides (add-design <- specify+plan; add-build <- tests+build; add-verify <- verify); SKILL.md flow table routes to AGENTS for bundle work, guides stay readable inline for the no-spawn lane (inline remains first-class per the user's inline-over-heavy-spawns feedback) -- [ ] persona-presets depends-on: bundle-disclosure — per-phase teacher-grade persona preset lines in the three roster agent files (dynamic-personas infra); advisory, never lowers a gate -- [ ] build-entry-spec-echo depends-on: phase-merge-verify — the tick INTO build (advance tests->build or freeze --cross chain) prints the §1 Must/Reject lines + frozen §3 contract summary (spec-in-context where drift starts; propose-not-impose, fail-open like the scope echo) +- [x] phase-merge-specify depends-on: none — scenarios fold INTO specify (§2 Given/When/Then becomes the §1 rules' scenario block; one drafting phase); engine PHASES list, section indices, advance/recipe paths, TASK.md.tmpl + fast twin; migration maps in-flight tasks (scenarios->specify) — the phase-index-vs-section bug class from expectations-first is the named trap +- [x] phase-merge-verify depends-on: phase-merge-specify — observe folds INTO verify (§7 spec-delta capture becomes an optional post-gate block of §6; deltas/fold lifecycles unchanged, they read the file not the phase); gate stays the phase's one outcome; 6-phase list lands: setup · specify · plan · tests · build · verify +- [x] guide-recut depends-on: phase-merge-verify — phases/*.md re-cut to 6 guides (1-specify absorbs 2-scenarios; 6-verify absorbs 7-observe); pools re-anchored (human-signed restructure, POOLS comments cite this milestone); orchestrator fast-lane read path <= 30KB finally lands +- [x] bundle-disclosure depends-on: guide-recut — the orchestrator loads ONLY SKILL.md; each roster agent's prompt names + loads its bundle's guides (add-design <- specify+plan; add-build <- tests+build; add-verify <- verify); SKILL.md flow table routes to AGENTS for bundle work, guides stay readable inline for the no-spawn lane (inline remains first-class per the user's inline-over-heavy-spawns feedback) +- [x] persona-presets depends-on: bundle-disclosure — per-phase teacher-grade persona preset lines in the three roster agent files (dynamic-personas infra); advisory, never lowers a gate +- [x] build-entry-spec-echo depends-on: phase-merge-verify — the tick INTO build (advance tests->build or freeze --cross chain) prints the §1 Must/Reject lines + frozen §3 contract summary (spec-in-context where drift starts; propose-not-impose, fail-open like the scope echo) ## Exit criteria (observable) -- [ ] the 6-phase lifecycle is test-pinned end-to-end: new-task -> specify -> plan(freeze) -> tests -> build -> verify(gate) with compound ticks intact (<- phase-merge-*) -- [ ] an in-flight 8-phase task migrates loud-and-safe (scenarios->specify, observe->verify) — test-pinned (<- phase-merge-*) -- [ ] 6 phase guides, pools green at re-anchored baselines, orchestrator fast-lane read path <= 30KB (<- guide-recut) -- [ ] roster agent files name + load their bundle guides; SKILL.md carries no per-phase guide bodies (<- bundle-disclosure) -- [ ] each of the 3 bundle agents carries per-phase persona preset lines (<- persona-presets) -- [ ] the build-entry echo renders Must/Reject + contract summary; bare non-build ticks byte-identical — test-pinned (<- build-entry-spec-echo) -- [ ] floors untouched: full fence green, ENGINE_MD5 re-pinned, 3-tree parity, gate semantics byte-identical +- [x] the 6-phase lifecycle is test-pinned end-to-end: new-task -> specify -> plan(freeze) -> tests -> build -> verify(gate) with compound ticks intact (<- phase-merge-*) (verify: test_phase_merge_specify.py + test_phase_merge_verify.py AdvanceAndGateTest; fence 3554 OK) +- [x] an in-flight 8-phase task migrates loud-and-safe (scenarios->specify, observe->verify) — test-pinned (<- phase-merge-*) (verify: test_phase_merge_verify.py LegacyStateTest — normalize-on-read, gate preserved) +- [x] 6 phase guides, pools green at re-anchored baselines, orchestrator fast-lane read path <= 30KB (<- guide-recut) (verify: test_guide_recut.py; phases pool 41605->40205 signed; SKILL.md 9442B + 1 guide/phase) +- [x] roster agent files name + load their bundle guides; SKILL.md carries no per-phase guide bodies (<- bundle-disclosure) (verify: test_bundle_disclosure.py — guide census + disclosure sentence + ceiling + x2 parity) +- [x] each of the 3 bundle agents carries per-phase persona preset lines (<- persona-presets) (verify: test_persona_presets.py — preset census, routing-first survives, never-lowers-a-gate) +- [x] the build-entry echo renders Must/Reject + contract summary; bare non-build ticks byte-identical — test-pinned (<- build-entry-spec-echo) (verify: test_build_entry_spec_echo.py 9 tests; self-proved live on the task's own re-cross) +- [x] floors untouched: full fence green, ENGINE_MD5 re-pinned, 3-tree parity, gate semantics byte-identical (verify: fence 3554/3554 OK; ENGINE_MD5 bf89b033 @ build-entry-spec-echo; twin-parity tests green; zero tests weakened across 6 tasks) - [ ] (paid, human-gated) WM1 re-measure after merge: fidelity >= 0.97 held, calls <= 12 ## Risks From 8360b069b11bb5cb40cf251c26939775091f09db Mon Sep 17 00:00:00 2001 From: Tin Dang Date: Tue, 14 Jul 2026 09:54:54 +0700 Subject: [PATCH 9/9] fix(test): exists()-skip gitignored engine twins in the echo parity test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI fresh-checkout failure on PR #148: test_engine_twins_carry_the_echo asserted parity on the two gitignored dogfood twins (.add/tooling/ and add-method/.add/tooling/), which do not exist on a fresh clone — the known gitignored-twins class (build-strategy-facets lesson). The tracked _bundled twin now binds unconditionally; the dogfood twins are checked only when present (fresh-checkout skip-tolerance precedent ba09498). No assertion weakened where the files exist. refs: six-phase-loop milestone, PR #148 author: Tin Dang --- add-method/tooling/test_build_entry_spec_echo.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/add-method/tooling/test_build_entry_spec_echo.py b/add-method/tooling/test_build_entry_spec_echo.py index 6a557dfd..f1750801 100644 --- a/add-method/tooling/test_build_entry_spec_echo.py +++ b/add-method/tooling/test_build_entry_spec_echo.py @@ -213,9 +213,16 @@ def test_spec_echo_is_a_pure_read(self): # M3 (never write def test_engine_twins_carry_the_echo(self): # sync x3 (twin parity on add.py) canon = hashlib.md5((PKG_ROOT / "tooling" / "add.py").read_bytes()).hexdigest() + # the git-TRACKED twin binds unconditionally; the two dogfood twins are + # gitignored and absent on a fresh checkout — exists()-skip tolerance + # (fresh-checkout precedent ba09498; gitignored-twins lesson, facets milestone) + bundled = PKG_ROOT / "src" / "add_method" / "_bundled" / "tooling" / "add.py" + self.assertEqual(hashlib.md5(bundled.read_bytes()).hexdigest(), canon, + f"engine twin drifted: {bundled}") for twin in (REPO_ROOT / ".add" / "tooling" / "add.py", - PKG_ROOT / ".add" / "tooling" / "add.py", - PKG_ROOT / "src" / "add_method" / "_bundled" / "tooling" / "add.py"): + PKG_ROOT / ".add" / "tooling" / "add.py"): + if not twin.exists(): + continue self.assertEqual(hashlib.md5(twin.read_bytes()).hexdigest(), canon, f"engine twin drifted: {twin}")