@pilotspace/add 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to the ADD method (`@pilotspace/add` on npm,
4
4
  `pilotspace-add` on PyPI) are documented here. The format follows
5
5
  [Keep a Changelog](https://keepachangelog.com/); versions follow semver.
6
6
 
7
+ ## [2.4.0] — 2026-07-24
8
+
9
+ Minor: the **strategy-intake** milestone closes — a fitting **persona** becomes ADD's
10
+ adaptive project-management brain — plus the follow-on cleanup that completes the 2.3.0
11
+ **scenarios fold** and adds a CI sweep of every shipped surface. The method changes are all
12
+ skill/agent-surface prose with **zero `add.py` engine change** (the engine records the
13
+ `## Strategy` slot and the gate, never drives the loop or gates on the strategy); this
14
+ release also carries one **concurrency fix** to the installer's lock (see *Fixed*).
15
+
16
+ ### Persona-as-PM strategy loop — `strategy.md` (new guide)
17
+ - **DISCUSS → OPTIMIZE → CONVERGE** — a new `strategy.md` guide drives a persona-framed
18
+ loop that fills a milestone's `## Strategy` slot with a sequenced, optimized task DAG,
19
+ converging on the *existing* six-dimension confidence self-score (no new bar invented).
20
+ - **`add-advisor` refute at CONVERGE** — a high-uncertainty milestone spawns the advisor
21
+ in refute mode to break the strategy before it's recorded; advisory (it cannot block),
22
+ reusing the existing refute mode. The advisor's `direction` beat now names a milestone
23
+ strategy as a refutable artifact alongside a task bundle.
24
+ - **risk-proportional depth ladder** — one legible rule: micro/`--tiny` skips the loop at
25
+ zero added per-turn cost · multi-task low-uncertainty runs the loop · high-uncertainty
26
+ adds the refute. It is the skill's judgment, never an engine gate; strategy stays SOFT
27
+ and security stays HARD-STOP.
28
+ - **persona-at-intake** — `intake.md` now loads the **fitting persona** before it sizes a
29
+ request (match-else-seed, advisory), so the persona that owns the intake report also
30
+ shapes the sizing.
31
+
32
+ ### Fold completion + shipped-surface sweep
33
+ - the **scenarios fold** finishes — four remaining shipped residuals of the §2→§4 retirement
34
+ are fixed, each defect class pinned by a new guard.
35
+ - a **shipped-surface** CI sweep derives the published file set from the packaging manifests
36
+ and fails on any dead chapter/section reference across every shipped surface.
37
+ - CI/deps: `actions/setup-python` 6→7, `actions/setup-node` 6→7, `@clack/prompts` 1.6.0→1.7.0.
38
+
39
+ ### Fixed
40
+ - **stale-lock reclaim could unlink a LIVE lock** (mutual-exclusion violation). The reclaim
41
+ path gated its unlink on inode identity alone, assuming a fresh replacement lock always
42
+ gets a new inode. Linux (ext4/tmpfs) **reuses freed inode numbers**, so a delayed racer
43
+ could match the crashed generation's inode against a *live* holder's replacement lock and
44
+ delete it — putting two processes inside the critical section at once. Reclaim now requires
45
+ the file to be **both** the observed inode **and** still stale, so a live or
46
+ heartbeat-refreshed lock is never mistaken for a dead one. Fixed at all four reclaim sites
47
+ in `_installer.py` and mirrored into the `bin/cli.js` npm twin, which carried the identical
48
+ guard. `O_EXCL`/`wx` remains the sole mutual-exclusion primitive and a genuinely crashed
49
+ lock still self-heals. Surfaced as an intermittent `peak=2` on the Linux CI runner only.
50
+
7
51
  ## [2.3.0] — 2026-07-24
8
52
 
9
53
  Minor: three waves — a **signal graph** view over the task DAG, the **§2
@@ -200,7 +200,7 @@ python3 .add/tooling/add.py new-task transfer --title "Transfer money between my
200
200
  > any build. Plain `init` skips that gate, which is fine here: by hand, the
201
201
  > human IS the one driving every step.
202
202
 
203
- This scaffolds `.add/tasks/transfer/PLAN.md` — **one file holding all seven phase
203
+ This scaffolds `.add/tasks/transfer/PLAN.md` — **one file holding all six phase
204
204
  sections** — plus empty `tests/` and `src/` folders, and makes it the active task
205
205
  at phase `specify`. Open it in your editor; you'll fill it top to bottom.
206
206
 
@@ -234,21 +234,6 @@ Confirm every assumption (no FX, no daily limit in v1). Then:
234
234
  python3 .add/tooling/add.py advance
235
235
  ```
236
236
 
237
- ### Phase 2 — Scenarios (https://pilotspace.github.io/ADD/04-step-2-scenarios/)
238
-
239
- In **§2**, turn each rule into a Given/When/Then. For every rejection, assert what
240
- must stay **unchanged**:
241
-
242
- ```gherkin
243
- Scenario: insufficient funds
244
- Given A has 20, mine
245
- When I transfer 50 from A to B
246
- Then it is rejected "insufficient_funds"
247
- And no balance changes
248
- ```
249
-
250
- Then `python3 .add/tooling/add.py advance`.
251
-
252
237
  ### Phase 3 — Contract (https://pilotspace.github.io/ADD/05-step-3-plan/)
253
238
 
254
239
  In **§3**, fix the external shape and **freeze** it (`Status: FROZEN @ v1`):
@@ -264,9 +249,24 @@ A frozen contract is the decision point that makes the AI build safe. Then advan
264
249
 
265
250
  ### Phase 4 — Tests, red first (https://pilotspace.github.io/ADD/06-step-4-tests/)
266
251
 
267
- Write one test per scenario into `.add/tasks/transfer/tests/`, then **run them and
268
- confirm they FAIL** there's no code yet. A test that passes now is testing
269
- nothing. This is red/green TDD: red before green. Then advance.
252
+ **§4 · TESTS & SCENARIOS** is where pass/fail cases live with the tests that
253
+ prove them. Write one test per §1 Must/Reject. For every rejection, also assert
254
+ what must stay **unchanged**.
255
+
256
+ A readable case, for a human who needs one:
257
+
258
+ ```gherkin
259
+ Scenario: insufficient funds
260
+ Given A has 20, mine
261
+ When I transfer 50 from A to B
262
+ Then it is rejected "insufficient_funds"
263
+ And no balance changes
264
+ ```
265
+
266
+ The test plan is the canonical encoding — a Given/When/Then block is for a human
267
+ reader, never ceremony. Write the tests into `.add/tasks/transfer/tests/`, then
268
+ **run them and confirm they FAIL** — there's no code yet. A test that passes now
269
+ is testing nothing. This is red/green TDD: red before green. Then advance.
270
270
 
271
271
  ### Phase 5 — Build (https://pilotspace.github.io/ADD/07-step-5-build/)
272
272
 
@@ -25,8 +25,9 @@ beat keeps moving. Personas carry the expertise; you carry independent, first-pr
25
25
  when uncertain — catch the plausible-but-wrong before the human or the gate does.
26
26
 
27
27
  Every mode serves EVERY beat — the spawn names the beat + mode, and you calibrate to it:
28
- **direction** (propose the bundle plan · refute the draft so the human freezes the stronger
29
- shape), **build** (decide approach forks mid-flight · pressure-test a strategy or a support-
28
+ **direction** (propose the bundle plan · refute the draft a task bundle OR a high-uncertainty
29
+ milestone **strategy** so the human freezes the stronger shape), **build** (decide approach
30
+ forks mid-flight · pressure-test a strategy or a support-
30
31
  worker slice partition against the frozen contract), **verify** (refute the earned-green ·
31
32
  judge whether the evidence supports the verdict). You never need the beat to be direction
32
33
  to be useful; you never need it to be verify to be skeptical.
package/bin/cli.js CHANGED
@@ -1209,6 +1209,28 @@ function agedReclaimTickets(dir, lockFileName, staleAfterSeconds, nowMs) {
1209
1209
  }).sort();
1210
1210
  }
1211
1211
 
1212
+ // True iff `p` is STILL the exact stale generation identified by `observedIno` AND still stale —
1213
+ // the guard that gates a reclaim UNLINK. Mirror of _installer.py:_still_stale_generation.
1214
+ //
1215
+ // Inode NUMBER alone is NOT a stable identity across a delete+recreate: Linux (ext4/tmpfs)
1216
+ // aggressively REUSES freed inode numbers, so a live holder's fresh replacement lock can reuse the
1217
+ // crashed file's inode. A reclaimer that trusts inode identity alone then unlinks a LIVE lock whose
1218
+ // inode merely coincides with the crashed generation it observed — two holders end up inside the
1219
+ // critical section at once (the peak=2 double-hold; reproduced on Linux CI, never on macOS APFS,
1220
+ // which does not reuse inodes in a short window).
1221
+ //
1222
+ // Re-verifying that the CURRENT file is itself still stale (mtime age > `staleAfterSeconds`)
1223
+ // distinguishes a live reused-inode holder (age ~0, or heartbeat-refreshed) from the crashed
1224
+ // generation we meant to reclaim. The "wx" create remains the SOLE mutual-exclusion primitive;
1225
+ // this only prevents a live file being mistaken for a dead one.
1226
+ function stillStaleGeneration(p, observedIno, staleAfterSeconds, nowMs) {
1227
+ const now = nowMs === undefined ? Date.now() : nowMs;
1228
+ let cur = null;
1229
+ try { cur = fs.statSync(p); }
1230
+ catch (_e) { return false; } // vanished — nothing to reclaim
1231
+ return cur.ino === observedIno && (now - cur.mtimeMs) / 1000 > staleAfterSeconds;
1232
+ }
1233
+
1212
1234
  // prune-data: reclaim ORPHANED snapshots under <home>/data. An orphan is a <home>/data/<key>
1213
1235
  // whose key is owned by NO LIVE registry entry (LIVE = a registered path that still EXISTS on
1214
1236
  // disk) — so unregistered AND registered-but-vanished are BOTH orphans (the explicit reclaim;
@@ -1433,9 +1455,10 @@ function acquireUpdateLock(home, { timeout = null } = {}, env = process.env) {
1433
1455
  // brand-new, not-yet-stale ticket for the SAME generation, so A and C would BOTH
1434
1456
  // believe they are the sole reclaimer — the exact double-hold bug this ticket
1435
1457
  // mechanism was built to prevent, reintroduced one level down).
1436
- let currentTino = null;
1437
- try { currentTino = fs.statSync(ticketPath).ino; } catch (_e) { /* already gone */ }
1438
- if (currentTino === tst.ino) {
1458
+ // Same reuse guard as the main lock: only unlink a ticket STILL at its observed inode
1459
+ // AND still aged past the ticket-stale threshold (a freed inode number can be reused
1460
+ // by a fresh, live ticket for a new generation).
1461
+ if (stillStaleGeneration(ticketPath, tst.ino, LOCK_TICKET_STALE_SECONDS)) {
1439
1462
  try { fs.unlinkSync(ticketPath); } catch (_e) {} // already gone — harmless
1440
1463
  }
1441
1464
  try { ticketFd = fs.openSync(ticketPath, "wx"); } catch (_e) {} // someone else won it
@@ -1462,9 +1485,12 @@ function acquireUpdateLock(home, { timeout = null } = {}, env = process.env) {
1462
1485
  // generation we ticketed for; otherwise our ticket is moot — leave the (unrelated,
1463
1486
  // currently live) file completely alone and let the loop's own open/EEXIST/age logic
1464
1487
  // re-evaluate reality fresh on the next iteration.
1465
- let currentIno = null;
1466
- try { currentIno = fs.statSync(lockPath).ino; } catch (_e) { /* already gone */ }
1467
- if (currentIno === st.ino) {
1488
+ // Re-verify STALENESS, not just inode identity: a freed inode number is REUSED by
1489
+ // Linux (ext4/tmpfs), so a live holder's fresh reused-inode lock must never be
1490
+ // unlinked on a bare inode match. Only remove it if it is STILL the observed inode
1491
+ // AND still stale — a live/heartbeated file (age < staleAfterMs) is spared, closing
1492
+ // the peak=2 double-hold.
1493
+ if (stillStaleGeneration(lockPath, st.ino, staleAfterMs / 1000)) {
1468
1494
  try { fs.unlinkSync(lockPath); } catch (_e) {} // already gone — harmless
1469
1495
  }
1470
1496
  } finally {
@@ -1596,9 +1622,10 @@ function acquireProjectLock(addDir, env = process.env) {
1596
1622
  // mechanism was built to prevent, reintroduced one level down). Exactly one extra
1597
1623
  // self-heal attempt — never a second, matching M7's own "no poll, ever" discipline
1598
1624
  // already governing the main lock's own reclaim above.
1599
- let currentTino = null;
1600
- try { currentTino = fs.statSync(ticketPath).ino; } catch (_e) { /* already gone */ }
1601
- if (currentTino === tst.ino) {
1625
+ // Same reuse guard as the main lock: only unlink a ticket STILL at its observed inode
1626
+ // AND still aged past the ticket-stale threshold (a freed inode number can be reused
1627
+ // by a fresh, live ticket for a new generation).
1628
+ if (stillStaleGeneration(ticketPath, tst.ino, PROJECT_LOCK_TICKET_STALE_SECONDS)) {
1602
1629
  try { fs.unlinkSync(ticketPath); } catch (_e) {} // already gone — harmless
1603
1630
  }
1604
1631
  try { ticketFd = fs.openSync(ticketPath, "wx"); } catch (_e) {} // someone else won it
@@ -1624,9 +1651,12 @@ function acquireProjectLock(addDir, env = process.env) {
1624
1651
  // moot — leave the (unrelated, currently live) file completely alone and let the
1625
1652
  // single retry below observe reality fresh (EEXIST -> fail-fast, per M7 — this lock
1626
1653
  // never polls a live holder).
1627
- let currentIno = null;
1628
- try { currentIno = fs.statSync(lockPath).ino; } catch (_e) { /* already gone */ }
1629
- if (currentIno === st.ino) {
1654
+ // Re-verify STALENESS, not just inode identity: a freed inode number is REUSED by
1655
+ // Linux (ext4/tmpfs), so a live holder's fresh reused-inode lock must never be
1656
+ // unlinked on a bare inode match. Only remove it if it is STILL the observed inode
1657
+ // AND still stale — a live/heartbeated file (age < staleAfterMs) is spared, closing
1658
+ // the peak=2 double-hold.
1659
+ if (stillStaleGeneration(lockPath, st.ino, staleAfterMs / 1000)) {
1630
1660
  try { fs.unlinkSync(lockPath); } catch (_e) {} // already gone — harmless
1631
1661
  }
1632
1662
  } finally {
@@ -1846,6 +1876,7 @@ if (require.main === module) {
1846
1876
  }
1847
1877
 
1848
1878
  module.exports = {
1879
+ stillStaleGeneration: stillStaleGeneration,
1849
1880
  detectAgent: detectAgent,
1850
1881
  detectAgentEnriched: detectAgentEnriched,
1851
1882
  readinessLine: readinessLine,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotspace/add",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "ADD (AI-Driven Development) 2.0. The agent is the hands; ADD is the memory, judgment, and conscience — a minimal, state-tracked skill: one freeze per feature, trust from passing tests, state on disk so context rot never survives a new session.",
5
5
  "bin": {
6
6
  "add": "bin/cli.js"
@@ -15,7 +15,7 @@ argument-hint: "status | init | continue | --todo <text> | [describe new short g
15
15
  license: MIT
16
16
  metadata:
17
17
  author: add
18
- version: "2.3.0"
18
+ version: "2.4.0"
19
19
  ---
20
20
 
21
21
  # ADD — memory · judgment · conscience (the agent is the hands)
@@ -100,6 +100,7 @@ confirmable delta the human confirms rewrites `SOUL.md` (the human is the only w
100
100
  - §3 FROZEN → auto-gated run `run.md` · subagent roster + pipelines (agent-call-preferred, the
101
101
  default execution mode) → `phases/verify.md` · self-score → `phases/direction.md`
102
102
  - UI/experience surface → UDD loop `design.md` · milestone goal unmet at `milestone-done` → `loop.md`
103
+ - multi-task / high-uncertainty milestone → persona-framed strategy loop `strategy.md` (fills the `## Strategy` slot; micro/`--tiny` → skip)
103
104
  - graduation · release · monorepo green-bars → persona-owned playbooks, `beyond.md` ·
104
105
  the persona loop (`.add/personas/`) → `docs/18-personas.md` · `sensitivity:`/`advisor-gate-relax` → `phases/verify.md`
105
106
 
@@ -20,6 +20,10 @@ prose. Load it (or the one guide you need) only when a trigger fires.
20
20
  Tool-agnostic; the engine never renders.
21
21
  - Tasks all done but the milestone **goal** unmet → `milestone-done` holds it open; the loop turns open
22
22
  deltas + extras into the next tasks until the goal is met — `loop.md`.
23
+ - **A multi-task / high-uncertainty milestone** needs its task DAG sequenced before the tasks are built →
24
+ the persona-framed **strategy loop** (DISCUSS → OPTIMIZE → CONVERGE): the persona loaded at intake fills
25
+ the milestone's `## Strategy` slot with the optimized plan, converging on the six-dimension self-score —
26
+ `strategy.md`. SOFT/advisory, never a gate; a micro / `--tiny` milestone skips it (drafted-blank).
23
27
  - **Graduating mvp → production** — co-specify interview → draft ≥1 production milestone →
24
28
  human confirm → then `stage production`. Guarded (`stage_no_roadmap`); the FINAL step, never a
25
29
  bare flip. The quality-auditor persona carries the readiness playbook.
@@ -4,6 +4,23 @@ Before a task exists, ADD turns a raw request into correctly-sized, versioned sc
4
4
  **intake level** (the per-task flow is phases 0–7; intake is the step *before* a task). You
5
5
  (the AI) **propose**; the human **confirms**. Never create scope without a confirmed proposal.
6
6
 
7
+ ## Load the fitting persona first
8
+
9
+ Intake is a decision — size it WITH the project's expertise, not generically. Before you
10
+ analyze or size, load the fitting persona (the PM / product-direction lens), exactly as
11
+ `design.md` loads the design-fit persona:
12
+
13
+ - **match** a persona in `.add/personas/` by role/flow (frontmatter, else description-match)
14
+ — e.g. a `product-lead` / `method-product-owner` lens;
15
+ - **none fits?** seed from `.add/personas-teacher/` via the add agent in persona mode, then
16
+ load — offered, **never required**;
17
+ - the persona shapes the framing, the latent-requirement read, and the sizing tradeoffs. It
18
+ is **advisory**: it never lowers the `ask_human` floor, the frozen-scope tie-break, or the
19
+ security-always-escalates rule.
20
+
21
+ No fitting persona and none seeded? Proceed generically — intake still runs. The persona already
22
+ OWNS the intake report (`gate-udd.md`); loading it here makes it own the SIZING too, so the two agree.
23
+
7
24
  ## Analyze the request before you size it
8
25
 
9
26
  A raw request is rarely a task yet — it is intent wrapped in prose. ADD's first job is not to
@@ -0,0 +1,54 @@
1
+ # Strategy — the persona-framed PM loop that fills the `## Strategy` slot
2
+
3
+ A multi-task or high-uncertainty milestone earns a plan over its tasks *before* they are
4
+ built. This guide drives that plan: a short **DISCUSS → OPTIMIZE → CONVERGE** loop that fills
5
+ the milestone's `## Strategy` slot (`MILESTONE.md`) with a sequenced, optimized task DAG.
6
+
7
+ **Trigger:** a milestone with several tasks or real uncertainty, at intake or when it activates.
8
+ **Skip:** a micro / `--tiny` milestone — a **drafted-blank** slot is valid, run nothing (risk-proportional).
9
+
10
+ The persona already loaded at intake (`intake.md`) **drives** this loop — you don't re-select
11
+ one. It frames the sequencing WITH the project's PM lens; it stays advisory throughout.
12
+
13
+ ## Run the loop (with the intake persona)
14
+
15
+ 1. **DISCUSS** — surface the task DAG: the dependencies, the shared/risky contracts, the
16
+ tradeoffs. Reflect the milestone goal, name what's in and out. Ask **one load-bearing
17
+ question per live lens** — an interview toward ~95% confidence, not a survey.
18
+ 2. **OPTIMIZE** — sequence the DAG. Fill the slot's four facets (defined there, not restated
19
+ here): **approach** (risk-first | dependency-first | first-slice-unblocks — and WHY) ·
20
+ the **freeze-first** contract(s) · the **parallel waves** behind those frozen contracts ·
21
+ the **first unblocking slice**. Name the alternative decompositions you rejected.
22
+ 3. **CONVERGE** — before you record, pressure-test the plan. If the milestone is
23
+ **high-uncertainty** (the sequencing is contested, or the self-score won't clear its bar),
24
+ spawn `add-advisor` in **refute** mode to try to **break** the strategy — the approach, the
25
+ freeze-first choice, the wave partition. Fold what survives; concede what holds. A
26
+ low-uncertainty / micro / `--tiny` milestone **skips** the spawn — no forced ceremony.
27
+ Then self-score with the existing six-dimension confidence self-score (`phases/direction.md`);
28
+ refine until it clears its bar (no dimension < 0.9 ≈ ~95% confident). Do **not** invent a
29
+ second threshold — that bar IS convergence. Record the converged plan in the `## Strategy` slot.
30
+
31
+ The refute is **advisory**: the advisor hands back the concrete break — it **cannot block**.
32
+ The human still confirms the strategy at the human decision point, through the persona-owned
33
+ gate (`gate-udd.md`) — the persona decides the report's shape and cadence. Security stays **HARD-STOP**.
34
+
35
+ ## It stays SOFT
36
+
37
+ The `## Strategy` slot is the **preferred** plan, exactly like a task's §5 Build-strategy: the
38
+ build loop may deviate and records what it actually did. It is **advisory** — **never a new
39
+ gate**, and it never lowers a floor. Security stays **HARD-STOP** everywhere. A milestone is
40
+ never blocked on reaching a confidence bar; the bar guides the draft, it does not gate the work.
41
+
42
+ ## How deep? (risk-proportional)
43
+
44
+ Loop depth scales with the milestone's risk/size — more risk/size, more depth:
45
+
46
+ - **micro / `--tiny`** → skip the loop. A drafted-blank `## Strategy` is valid; the loop runs
47
+ nothing — **zero added per-turn cost**.
48
+ - **multi-task, low-uncertainty** → run DISCUSS → OPTIMIZE → CONVERGE; no advisor.
49
+ - **high-uncertainty** (contested sequencing, or the self-score won't clear its bar) → the full
50
+ loop **plus** the `add-advisor` refute at CONVERGE.
51
+
52
+ This is the skill's judgment, reusing the Trigger / Skip and CONVERGE signals above — **never an
53
+ engine gate** on `## Strategy`. The ladder is **SOFT**: a run may go deeper or shallower and
54
+ records what it did. Security stays **HARD-STOP**.
@@ -82,7 +82,7 @@ def book_url(chapter: str) -> str:
82
82
  # `add.py guide` copy: per-phase (concrete next action, book chapter to read).
83
83
  # Keep the action wording aligned with each phase's EXIT line in the TASK template.
84
84
  PHASE_GUIDE = {
85
- "direction": ("draft the Direction bundle top-to-bottom — §1 rules (Must / Reject + named codes / After, assumptions ranked lowest-confidence first) · §2 one scenario per rule · §3 the change PLAN: ground the real code, draft the contract, and DESCRIBE what this task will do (scope · ordered batches · approach — the plan-of-action the freeze report shows the human) · §4 red suite failing for the right reason; then the ONE approval: freeze --by <name> --cross",
85
+ "direction": ("draft the Direction bundle top-to-bottom — §1 rules (Must / Reject + named codes / After, assumptions ranked lowest-confidence first) · §3 the change PLAN: ground the real code, draft the contract, and DESCRIBE what this task will do (scope · ordered batches · approach — the plan-of-action the freeze report shows the human) · §4 red suite failing for the right reason, one case per rule (scenarios live here with the tests); then the ONE approval: freeze --by <name> --cross",
86
86
  "03-step-1-specify.md"),
87
87
  "build": ("write the minimum code to pass the tests; change no test and no contract",
88
88
  "07-step-5-build.md"),
@@ -29,7 +29,7 @@ binding verdict: a §6 Advisor 3-lens verdict with Binding:yes — engine-enforc
29
29
  bounded self-heal: the verify-gate loop (heal-then-escalate) that gives a CONFIRMED cheat — mechanical tamper (the tripwire) or a reported earned-green failure (`add.py heal --reason`) — a chance to redo honestly before it stops: the engine returns the task to build, COUNTS the attempt, CAPS it at 3 (`HEAL_CAP`), and on the next confirmed cheat forces a HARD-STOP that escalates to the human. The counter is MONOTONIC — it never auto-resets (cmd_phase is unguarded, so a reset would be a zero-human cap bypass). The engine counts/caps/escalates; the AGENT does the honest re-build (never the engine). An honest build passes even at the cap (the cap bites a CONTINUED cheat, never a recovery); a gamed green is never auto-passed, never RISK-ACCEPTED-waived.
30
30
  onboarding: the install -> first-milestone path (formerly "on-ramp").
31
31
  primary flow: the solid forward path of the flow diagram — a phase starts only when its input exists (formerly "forward spine").
32
- ground: the phase-0 preamble before specify — the AI gathers the real current codebase a task touches (files, symbols, signatures, conventions) into a §0 grounding map; AI-owned, adds no gate (the one approval stays at the contract freeze).
32
+ ground: the grounding pass that opens the direction phase — the AI gathers the real current codebase a task touches (files, symbols, signatures, conventions) into a §0 grounding map; AI-owned, adds no gate (the one approval stays at the contract freeze).
33
33
  grounding map / anchors: the §0 GROUND artifact — the real files/symbols/conventions a task touches plus the anchors the frozen contract cites; task-specific delta only (defers to PROJECT.md / CONVENTIONS.md), surfaced by status/check (measure, never block).
34
34
  cross-cutting concern: a concern running through every step rather than being one step — security, testing, observability, cost (formerly "spine / continuous concern").
35
35
  working state: everything an agent loads each session — skill router, active phase, PROJECT/MILESTONE/TASK, state.json (formerly "state surface").
@@ -40,7 +40,7 @@ Issues/Risks (shared): <traps in the shared code that feed each task's §1 expec
40
40
  ## Exit criteria (observable; map each to the task that delivers it)
41
41
  - [ ] User can <observable behavior — the SEEN outcome only, NOT the task's plan line> (← <slug>)
42
42
 
43
- ## Strategy (AI-drafted WITH the human — the optimized task plan; SOFT/advisory like a task's Build-strategy; drafted-blank for a micro/--fast milestone)
43
+ ## Strategy (AI-drafted WITH the human — the optimized task plan; SOFT/advisory like a task's Build-strategy; drafted-blank for a micro/--tiny milestone)
44
44
  > The persona-led strategy over THIS milestone's tasks — sequencing, freeze-first contracts,
45
45
  > parallel waves, the first unblocking slice, tradeoffs named. SOFT: the preferred plan; the
46
46
  > loop may deviate and records what it did. Drafted-blank is valid (risk-proportional).
@@ -3,7 +3,7 @@
3
3
  slug: {{slug}} · created: {{date}} · stage: {{stage}}
4
4
  milestone: {{milestone}}
5
5
  autonomy: {{autonomy}} <!-- manual<conservative<auto — lower for high-risk (`add.py autonomy set`); a `component: <name>` line joins that root to §3 Scope; task edges: `--depends-on`/`--extends`/`--relates-to`; high-risk/method-defining? declare `risk: high` on the slug line; headless agent-crossed freeze? declare `gate_mode: ai-plan-verify` here (human floor: security|data|architecture never AI-frozen) -->
6
- phase: direction <!-- direction→build→verify→done; direction drafts §1–§4 (rules · scenarios · change plan · red suite) to the ONE freeze -->
6
+ phase: direction <!-- direction→build→verify→done; direction drafts §1–§4 (rules · change plan · red suite) to the ONE freeze -->
7
7
 
8
8
  > One file = one task — an ATOMIC node: persist the interface (contract · red suite · scope · verdict); reason everything else in-context, don't write essays. The phase marker above is the single source of truth (`add.py phase`).
9
9