@pilotspace/add 1.16.1 → 1.17.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 +83 -0
- package/agents/add-advisor.md +1 -1
- package/agents/add-build.md +1 -1
- package/agents/add-design.md +1 -1
- package/agents/add-persona.md +6 -4
- package/agents/add-verify.md +3 -1
- package/bin/cli.js +143 -8
- package/docs/10-setup-and-stages.md +1 -1
- package/docs/18-personas.md +2 -2
- package/package.json +1 -1
- package/skill/add/advisor.md +2 -1
- package/skill/add/deltas.md +1 -1
- package/skill/add/design.md +2 -2
- package/skill/add/fold.md +7 -7
- package/skill/add/intake.md +17 -12
- package/skill/add/loop.md +6 -7
- package/skill/add/phases/0-setup.md +3 -3
- package/skill/add/phases/4-tests.md +19 -17
- package/skill/add/phases/7-observe.md +8 -10
- package/skill/add/phases/fast-lane.md +3 -0
- package/skill/add/report-template.md +7 -6
- package/skill/add/run.md +7 -17
- package/skill/add/scope.md +1 -1
- package/skill/add/self-improve.md +20 -0
- package/skill/add/streams.md +14 -13
- package/tooling/add.py +502 -37
- package/tooling/add_engine/constants.py +12 -0
- package/tooling/add_engine/io_state.py +13 -0
- package/tooling/templates/TASK.fast.md.tmpl +2 -1
- package/tooling/templates/TASK.md.tmpl +16 -51
- package/tooling/templates/personas/_template.md.tmpl +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,89 @@ 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
|
+
## [1.17.0] — 2026-07-06
|
|
8
|
+
|
|
9
|
+
Minor: four milestones — **method-ergonomics** (every recurring gate rule becomes
|
|
10
|
+
a form the engine presents at that moment, cutting per-task ceremony without
|
|
11
|
+
moving the safety floor), **persona-domain-fit** (a new milestone or task
|
|
12
|
+
whose domain no existing persona covers gets a concrete draft-one nudge),
|
|
13
|
+
**dynamic-personas** (a drafted persona carries `flow:` routing to its agent
|
|
14
|
+
surface, at teacher-grade depth and load cost), and **self-improving-loop**
|
|
15
|
+
(the observe→deltas→fold→compact loop surfaces its own accumulation instead of
|
|
16
|
+
rotting silently) — plus twelve loose tasks. New engine verbs are additive; no
|
|
17
|
+
gate weakened, nothing removed or renamed.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`add.py gate --explain [slug]`** — a read-only dry-run of the verify gate:
|
|
21
|
+
prints phase · autonomy · risk · sensitivity · advisor lines and one
|
|
22
|
+
`path: AUTO | HUMAN | RELAX | REFUSED (reason)` verdict, always ending with
|
|
23
|
+
the security floor (a security finding is always HARD-STOP). Writes nothing.
|
|
24
|
+
- **`add.py advance --to <phase>`** — fast-forwards a drafted bundle's
|
|
25
|
+
bookkeeping crossings in one call, stopping hard at `tests` (the freeze gate,
|
|
26
|
+
tamper tripwire and scope snapshot are never skipped).
|
|
27
|
+
- **`add.py re-cross`** — records a human-approved post-freeze re-cross
|
|
28
|
+
(`--by` required): re-runs the full tests→build gate stack to legally
|
|
29
|
+
re-snapshot after an approved test addition, never bypassing the freeze.
|
|
30
|
+
- **`add.py worktree-prep <slug>`** — mechanizes the spawn-isolation recipe:
|
|
31
|
+
cuts a git worktree at HEAD, materializes the gitignored `.add/tooling` +
|
|
32
|
+
`.add/docs` a tracked-only checkout lacks, and echoes the fork base for the
|
|
33
|
+
WAVE.md ledger. Workspace-only; state.json is never written.
|
|
34
|
+
- **Verify-record rollup** — `add.py audit` folds its four §6 shape lints into
|
|
35
|
+
one `verify_record_incomplete` line per task.
|
|
36
|
+
- **Delta verbs reach archived tasks** — `drop-delta` / `carry-delta` /
|
|
37
|
+
`reopen-delta` now operate on a light-archived task's on-disk TASK.md
|
|
38
|
+
(explicit slug only, `(archived — on-disk record)` marker, state untouched).
|
|
39
|
+
- **Batched intake + batched gates** — intake.md and report-template.md
|
|
40
|
+
document one report + one confirm for N same-gate items (per-item
|
|
41
|
+
lowest-confidence flags; any item holdable by name). Presentation only.
|
|
42
|
+
- **Domain test forms** — 4-tests.md states that a test is any
|
|
43
|
+
machine-checkable assertion (metric threshold · reconciliation query ·
|
|
44
|
+
plan-diff · rendered-screen diff), red-first holding for each.
|
|
45
|
+
- **Persona domain-fit nudge** — a milestone/task whose domain no existing
|
|
46
|
+
persona covers is nudged toward drafting a fitting one; TASK templates now
|
|
47
|
+
require a named persona and carry a domain-strategy hint.
|
|
48
|
+
- **Fast-lane ground anchor** — the `--fast` template's §0 gains the
|
|
49
|
+
`Ground SHA:` drift anchor, so a fast task can clear the stale-line-ref WARN.
|
|
50
|
+
- **Persona `flow:` routing** — a persona's `flow:` frontmatter (design | build
|
|
51
|
+
| advisor) is now read, not just written: the 4 flow-routed roster agents,
|
|
52
|
+
`design.md`'s evidence checklist, and `advisor.md`'s spawn block all select
|
|
53
|
+
a persona flow-first; add-persona drafts the current schema (`flow:` /
|
|
54
|
+
`source:` / `## Abilities`) and returns `flow` in its verdict.
|
|
55
|
+
- **Persona load performance** — every seeded persona gains `## Abilities`
|
|
56
|
+
(orient-command-led) and `## Anti-patterns`; selection is frontmatter-first
|
|
57
|
+
(name · vibe · flow, then one body — not the whole roster); add-persona
|
|
58
|
+
routes the teacher library by division directory, never its catalog README.
|
|
59
|
+
- **Fold grows the current persona schema** — `add.py fold` routes a
|
|
60
|
+
`persona:<slug> · anti-pattern|ability` lesson into those sections too (was
|
|
61
|
+
limited to `critical-rule`/`success-metric`), so the persona learning loop
|
|
62
|
+
can grow the sections that shape agent behavior.
|
|
63
|
+
- **Loop-surfacing status cues** — `add.py status` names the carried
|
|
64
|
+
spec-delta backlog and the un-compacted foundation tail (last-rolled vs.
|
|
65
|
+
current `foundation-version`) once either passes a noticeable size;
|
|
66
|
+
`release-report` lists the carried total. Additive; a clean project's
|
|
67
|
+
output is unchanged.
|
|
68
|
+
- **`skill/add/self-improve.md`** — one map of how ADD improves itself: the
|
|
69
|
+
four self-improving artifacts (foundation · personas · `SOUL.md` · next
|
|
70
|
+
scope), routed across the 5 domains, fed by all 8 steps.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
- **Sequential+auto is the default run mode** — parallel streams stay a
|
|
74
|
+
deliberate, persisted opt-in.
|
|
75
|
+
- **Leaner TASK.md template** — instructional comment bloat trimmed 16%
|
|
76
|
+
(comments −27%); every machine-parsed marker untouched.
|
|
77
|
+
- **Leaner guides** — run.md's specification-bundle section is a pointer at
|
|
78
|
+
its one home (`phases/3-contract.md`); the ⚠ flag grammar now lives in
|
|
79
|
+
exactly one guide.
|
|
80
|
+
- **Waiver field census is case-insensitive** — `audit` recognizes
|
|
81
|
+
`Owner:/Ticket:/Expires:` as written by the signed records; the
|
|
82
|
+
missing-field refusal is unchanged.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
- The CI-observed reclaim-ticket race in `_update_lock` (+ its JS/npm
|
|
86
|
+
heartbeat twin), orphaned reclaim-ticket sweep, the prune-data/update-global
|
|
87
|
+
lock race, the ADR harvester's multi-line field capture, and the
|
|
88
|
+
strip-scaffold backtick-comment over-strip.
|
|
89
|
+
|
|
7
90
|
## [1.16.1] — 2026-07-04
|
|
8
91
|
|
|
9
92
|
Patch: two loose, additive persona-loop improvements found while dogfooding
|
package/agents/add-advisor.md
CHANGED
|
@@ -8,7 +8,7 @@ color: cyan
|
|
|
8
8
|
You are the **advisor** in ADD's phase-agent roster — not a phase-worker but a cross-cutting, consultative service (modeled on Claude Code's advisor tool) that any of the other agents, or the orchestrator, consults when a decision is genuinely medium-hard: an ambiguous interpretation, a risky shape, a change-of-approach, a "which of these is right" the caller cannot cheaply resolve alone. Given the situation and its context you return ADVICE — a recommendation, the tradeoffs weighed, the risks and edge-cases, and a confidence self-score. You **advise; you never decide**: you run nothing, record nothing, edit nothing, and you never lower a gate. You own no ADD phase (like add-persona), and you never perform add-verify's earned-green refute-read — that adversarial check stays with the verifier; you advise on the decision, you do not sign the gate.
|
|
9
9
|
|
|
10
10
|
## Become the persona (do this FIRST — before acting on any task-specific instructions in your prompt)
|
|
11
|
-
Load the fit `.add/personas/<slug>.md` for whatever domain the decision sits in and BECOME it — a senior-engineer / architect / domain-analyst stance matched to the question; its `## Critical Rules` are your constraints, its `## Success Metrics` sharpen the recommendation. Even when the caller hands you a different return shape than `## Return` below, keep the in-character judgment — a self-contained prompt says WHAT to weigh, never whether to weigh it in-character. No persona seeded or matched? Use a generic senior engineer, correctness over speed — the generic body never blocks the advice.
|
|
11
|
+
Load the fit `.add/personas/<slug>.md` for whatever domain the decision sits in and BECOME it — select a `flow: advisor` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become), then a senior-engineer / architect / domain-analyst stance matched to the question; its `## Critical Rules` are your constraints, its `## Success Metrics` sharpen the recommendation. Even when the caller hands you a different return shape than `## Return` below, keep the in-character judgment — a self-contained prompt says WHAT to weigh, never whether to weigh it in-character. No persona seeded or matched? Use a generic senior engineer, correctness over speed — the generic body never blocks the advice.
|
|
12
12
|
|
|
13
13
|
## What you own (consultative advice — a cross-cutting service, not an ADD phase)
|
|
14
14
|
- **Read the situation** — the diff, the real code, and the task/plan files the caller points you at; confirm you understand the decision before weighing in. An advisor who misread the question gives confident, wrong advice.
|
package/agents/add-build.md
CHANGED
|
@@ -8,7 +8,7 @@ color: green
|
|
|
8
8
|
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).
|
|
9
9
|
|
|
10
10
|
## Become the persona
|
|
11
|
-
Load the fit `.add/personas/<slug>.md` and BECOME it —
|
|
11
|
+
Load the fit `.add/personas/<slug>.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.
|
|
12
12
|
|
|
13
13
|
## What you own (tests → build)
|
|
14
14
|
- **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.
|
package/agents/add-design.md
CHANGED
|
@@ -8,7 +8,7 @@ color: blue
|
|
|
8
8
|
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.
|
|
9
9
|
|
|
10
10
|
## Become the persona
|
|
11
|
-
Load the fit `.add/personas/<slug>.md` and BECOME it —
|
|
11
|
+
Load the fit `.add/personas/<slug>.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.
|
|
12
12
|
|
|
13
13
|
## What you own (the design span: setup → ground → specify → scenarios → contract)
|
|
14
14
|
- **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.
|
package/agents/add-persona.md
CHANGED
|
@@ -8,12 +8,12 @@ color: purple
|
|
|
8
8
|
You are the **persona** specialist in ADD's phase-agent roster — not a phase-worker like the other three, but a cross-cutting SERVICE the design, build, and verify agents (or the orchestrator) consult when they need to know which domain stance to adopt for a piece of work. You read PROJECT.md and the vendored `.add/personas-teacher/` library directly to judge fit.
|
|
9
9
|
|
|
10
10
|
## Become the persona
|
|
11
|
-
There is no persona to become for persona-selection itself — read the existing roster under `.add/personas/`, `PROJECT.md` (domain), and the vendored `.add/personas-teacher/` library (read off-build; never fetched) to judge which existing persona fits, or what a new one needs.
|
|
11
|
+
There is no persona to become for persona-selection itself — read the existing roster under `.add/personas/`, `PROJECT.md` (domain), and the vendored `.add/personas-teacher/` library (read off-build; never fetched) to judge which existing persona fits, or what a new one needs. Route into the teacher by its division directory names (the domain index — `engineering/`, `testing/`, `security/`, `design/`, `product/`, …); read only the 1–2 matched teacher files, never the catalog README.
|
|
12
12
|
|
|
13
13
|
## What you own (persona selection/drafting — a cross-cutting service, not an ADD phase)
|
|
14
14
|
- Given a piece of work's domain, select the best-fit EXISTING `.add/personas/<slug>.md` if one matches.
|
|
15
|
-
- If none fits, draft a NEW persona file conforming to the schema: frontmatter `name`/`vibe
|
|
16
|
-
- Return the chosen or drafted slug
|
|
15
|
+
- If none fits, draft a NEW persona file conforming to the CURRENT schema (`templates/personas/_template.md.tmpl`): frontmatter `name`/`vibe` (required) plus `flow:` (which apply-surface loads it — design | build | advisor) and `source:` (the teacher file(s) it distills); sections `## Identity` / `## Critical Rules` / `## Default Requirement` / `## Success Metrics` plus `## Abilities` — sourced from PROJECT.md plus the vendored teacher library, never invented from nothing. Dynamic never means shallow: a drafted persona carries teacher-grade depth or it isn't worth drafting.
|
|
16
|
+
- Return the chosen or drafted slug, its flow, and a one-line rationale for the calling agent to load and become.
|
|
17
17
|
- Never overwrite an existing `.add/personas/<slug>.md` — a new draft always gets a new file, even when an existing one is partial or outdated.
|
|
18
18
|
|
|
19
19
|
## Boundary (the irreducible floor)
|
|
@@ -26,6 +26,8 @@ Self-score with the confidence.md six dimensions (Completeness · Clarity · Pra
|
|
|
26
26
|
|
|
27
27
|
## Return (disclose progress)
|
|
28
28
|
End with a structured verdict the calling agent or orchestrator parses:
|
|
29
|
-
`{ phase: persona, slug, drafted: true|false, rationale, confidence: {per-dimension 0–1}, open_questions }`.
|
|
29
|
+
`{ phase: persona, slug, flow, drafted: true|false, rationale, confidence: {per-dimension 0–1}, open_questions }`.
|
|
30
|
+
|
|
31
|
+
Personas GROW after drafting: recommend the calling agent tag behavioral lessons `· persona:<slug> · <critical-rule|success-metric|anti-pattern|ability>` at observe — `add.py fold` routes each into that section, never clobbering.
|
|
30
32
|
|
|
31
33
|
Method depth: the AIDD book in `.add/docs/` — `0-setup.md`'s persona-seeding convention (no single phase chapter owns cross-cutting persona work).
|
package/agents/add-verify.md
CHANGED
|
@@ -8,7 +8,7 @@ color: red
|
|
|
8
8
|
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).
|
|
9
9
|
|
|
10
10
|
## Become the persona (do this FIRST — before acting on any task-specific instructions in your prompt)
|
|
11
|
-
Load the fit `.add/personas/<slug>.md` and BECOME it —
|
|
11
|
+
Load the fit `.add/personas/<slug>.md` and BECOME it — select a `flow: advisor` persona first (the frontmatter routing field — choose from frontmatter alone: name · vibe · flow, then read the body of the one you become; the verify refute-read rides the advisor surface), 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.
|
|
12
12
|
|
|
13
13
|
## What you own (verify → observe)
|
|
14
14
|
- **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.
|
|
@@ -31,4 +31,6 @@ Treat any Strategy the builder used as their PREFERRED path, not a hard rule you
|
|
|
31
31
|
End with a structured verdict the orchestrator parses:
|
|
32
32
|
`{ phase: verify|observe, persona, result, evidence, residue, outcome, deltas, confidence: {per-dimension 0–1}, open_questions }`.
|
|
33
33
|
|
|
34
|
+
When observe surfaces a lesson about HOW an agent should behave (a suspect instinct, a checkable skill), recommend tagging it `· persona:<slug> · anti-pattern|ability` so `add.py fold` grows that persona instead of the shared conventions pile.
|
|
35
|
+
|
|
34
36
|
Method depth: the AIDD book in `.add/docs/` — `08-step-6-verify.md` · `09-the-loop.md`.
|
package/bin/cli.js
CHANGED
|
@@ -796,6 +796,37 @@ function sleepSync(ms) {
|
|
|
796
796
|
Atomics.wait(ia, 0, 0, ms);
|
|
797
797
|
}
|
|
798
798
|
|
|
799
|
+
// An async, event-loop-YIELDING sleep (unlike sleepSync's Atomics.wait, which blocks the very
|
|
800
|
+
// thread a setInterval-based heartbeat needs to fire on) — js-reclaim-lock-heartbeat.
|
|
801
|
+
function sleepAsync(ms) {
|
|
802
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// js-reclaim-lock-heartbeat: the JS analog of the Python `_lock_heartbeat` fix
|
|
806
|
+
// (reclaim-ticket-race) — refreshes a held lock file's own mtime on a `.unref()`'d
|
|
807
|
+
// `setInterval` for as long as it is held, so a live-but-slow holder is never misjudged
|
|
808
|
+
// stale by a sibling racer purely because wall-clock age crosses `staleAfterMs`. `.unref()`
|
|
809
|
+
// means the timer never keeps the process alive on its own; `stop()` clears it explicitly on
|
|
810
|
+
// release (belt AND suspenders, mirrors the interval's own `max(50ms, min(staleAfterMs/4,
|
|
811
|
+
// 5000ms))` formula the frozen contract cites). A crash simply stops the interval firing — the
|
|
812
|
+
// file still ages out and self-heals via the existing reclaim-ticket mechanism, unchanged.
|
|
813
|
+
//
|
|
814
|
+
// This is a probabilistic mitigation, not a mathematical guarantee: a callback fires only
|
|
815
|
+
// between synchronous JS turns, so whole-event-loop starvation (a long synchronous operation
|
|
816
|
+
// blocking the process) can defeat it exactly as whole-process scheduling starvation once
|
|
817
|
+
// defeated the Python fix's own real OS thread on real CI (see TASK.md's least-sure flag) —
|
|
818
|
+
// accepted given production defaults (600s/120s) make that starvation window vanishingly
|
|
819
|
+
// unlikely in practice.
|
|
820
|
+
function startLockHeartbeat(lockPath, staleAfterMs) {
|
|
821
|
+
const intervalMs = Math.max(50, Math.min(staleAfterMs / 4, 5000));
|
|
822
|
+
const timer = setInterval(() => {
|
|
823
|
+
const now = new Date();
|
|
824
|
+
try { fs.utimesSync(lockPath, now, now); } catch (_e) {} // released/reclaimed — best-effort
|
|
825
|
+
}, intervalMs);
|
|
826
|
+
timer.unref();
|
|
827
|
+
return { stop: () => clearInterval(timer) };
|
|
828
|
+
}
|
|
829
|
+
|
|
799
830
|
function pkgVersion() {
|
|
800
831
|
try { return require(path.join(PKG_ROOT, "package.json")).version; }
|
|
801
832
|
catch (_e) { return "0.0.0"; }
|
|
@@ -1249,17 +1280,44 @@ function installGlobalDataRestore(chosenTarget, force) {
|
|
|
1249
1280
|
else log(" (no snapshot for this project at " + snap + " — nothing restored)");
|
|
1250
1281
|
}
|
|
1251
1282
|
|
|
1283
|
+
// Every "<lockFileName>.reclaim-*" directly under dir aged past staleAfterSeconds — a LEAKED
|
|
1284
|
+
// per-generation reclaim ticket (its own holder crashed between winning it and its own
|
|
1285
|
+
// best-effort cleanup; a live, currently-in-flight ticket is never this old). Returns full
|
|
1286
|
+
// paths, sorted; [] if dir does not exist. Mirror of _installer.py:_aged_reclaim_tickets.
|
|
1287
|
+
function agedReclaimTickets(dir, lockFileName, staleAfterSeconds, nowMs) {
|
|
1288
|
+
if (!fs.existsSync(dir)) return [];
|
|
1289
|
+
const prefix = lockFileName + ".reclaim-";
|
|
1290
|
+
return fs.readdirSync(dir).filter((name) => name.startsWith(prefix)).map((name) => path.join(dir, name))
|
|
1291
|
+
.filter((p) => {
|
|
1292
|
+
try { return (nowMs - fs.statSync(p).mtimeMs) / 1000 > staleAfterSeconds; }
|
|
1293
|
+
catch (_e) { return false; } // vanished mid-sweep — nothing to report
|
|
1294
|
+
}).sort();
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1252
1297
|
// prune-data: reclaim ORPHANED snapshots under <home>/data. An orphan is a <home>/data/<key>
|
|
1253
1298
|
// whose key is owned by NO LIVE registry entry (LIVE = a registered path that still EXISTS on
|
|
1254
1299
|
// disk) — so unregistered AND registered-but-vanished are BOTH orphans (the explicit reclaim;
|
|
1255
1300
|
// DIVERGES from update --global's keep-vanished). Reads the registry FIRST (corrupt throws,
|
|
1256
|
-
// before any removal).
|
|
1301
|
+
// before any removal).
|
|
1302
|
+
//
|
|
1303
|
+
// sweep-orphan-reclaim-tickets: ALSO sweeps LEAKED per-generation reclaim tickets — a
|
|
1304
|
+
// "<LOCK_FILE>.reclaim-*" under home (home-scope) and a "<PROJECT_LOCK_FILE>.reclaim-*" under
|
|
1305
|
+
// every LIVE registered project's own .add/ (project-scope) — each aged past its OWN kind's
|
|
1306
|
+
// existing staleness constant (LOCK_TICKET_STALE_SECONDS / PROJECT_LOCK_TICKET_STALE_SECONDS,
|
|
1307
|
+
// reused verbatim; no new threshold). Reuses the registry already read for the data-orphan
|
|
1308
|
+
// sweep — no new read.
|
|
1309
|
+
//
|
|
1310
|
+
// Returns {orphans, removed, ticketOrphans, ticketsRemoved} — extends the prior {orphans,
|
|
1311
|
+
// removed} object non-breakingly (existing dot-access callers are unaffected). ticketOrphans/
|
|
1312
|
+
// ticketsRemoved are full paths; ticketsRemoved == [] on a dry-run, == ticketOrphans under
|
|
1313
|
+
// force (each unlinked best-effort, matching the existing reclaim code's own swallow-errors
|
|
1314
|
+
// convention). Mirror of _installer.py:_prune_data.
|
|
1257
1315
|
function pruneData(home, force) {
|
|
1258
1316
|
const reg = readRegistry(home); // corrupt -> throw (LOUD, zero removal)
|
|
1259
|
-
const
|
|
1317
|
+
const livePaths = reg.filter((p) => fs.existsSync(p));
|
|
1318
|
+
const live = new Set(livePaths.map(dataKey));
|
|
1260
1319
|
const dataDir = path.join(home, "data");
|
|
1261
|
-
|
|
1262
|
-
const orphans = fs.readdirSync(dataDir).filter((name) => {
|
|
1320
|
+
const orphans = !fs.existsSync(dataDir) ? [] : fs.readdirSync(dataDir).filter((name) => {
|
|
1263
1321
|
try { return fs.statSync(path.join(dataDir, name)).isDirectory() && !live.has(name); }
|
|
1264
1322
|
catch (_e) { return false; }
|
|
1265
1323
|
}).sort();
|
|
@@ -1270,23 +1328,67 @@ function pruneData(home, force) {
|
|
|
1270
1328
|
removed.push(key);
|
|
1271
1329
|
}
|
|
1272
1330
|
}
|
|
1273
|
-
|
|
1331
|
+
|
|
1332
|
+
const now = Date.now();
|
|
1333
|
+
let ticketCandidates = agedReclaimTickets(home, LOCK_FILE, LOCK_TICKET_STALE_SECONDS, now)
|
|
1334
|
+
.map((p) => ({ path: p, staleAfter: LOCK_TICKET_STALE_SECONDS }));
|
|
1335
|
+
for (const project of livePaths) {
|
|
1336
|
+
ticketCandidates = ticketCandidates.concat(
|
|
1337
|
+
agedReclaimTickets(path.join(project, ".add"), PROJECT_LOCK_FILE, PROJECT_LOCK_TICKET_STALE_SECONDS, now)
|
|
1338
|
+
.map((p) => ({ path: p, staleAfter: PROJECT_LOCK_TICKET_STALE_SECONDS }))
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const ticketOrphans = ticketCandidates.map((c) => c.path);
|
|
1342
|
+
const ticketsRemoved = [];
|
|
1343
|
+
if (force) {
|
|
1344
|
+
for (const { path: ticket, staleAfter } of ticketCandidates) {
|
|
1345
|
+
try {
|
|
1346
|
+
if ((Date.now() - fs.statSync(ticket).mtimeMs) / 1000 <= staleAfter) continue; // no longer stale at unlink time — §5 safety rule
|
|
1347
|
+
fs.unlinkSync(ticket);
|
|
1348
|
+
ticketsRemoved.push(ticket);
|
|
1349
|
+
} catch (_e) { /* already gone — harmless, matches reclaim's own convention */ }
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return { orphans: orphans, removed: removed, ticketOrphans: ticketOrphans, ticketsRemoved: ticketsRemoved };
|
|
1274
1354
|
}
|
|
1275
1355
|
|
|
1276
1356
|
// prune-data command: dry-run lists orphans (removes nothing); --force deletes. no_global_home /
|
|
1277
1357
|
// registry_corrupt = fail-closed (LOUD, nothing removed). Mirror of pip _installer.prune_data.
|
|
1358
|
+
//
|
|
1359
|
+
// prune-data-update-lock: the registry-read + orphan-computation + removal critical section now
|
|
1360
|
+
// holds the SAME home lock (acquireUpdateLock) `update --global` already holds during its own
|
|
1361
|
+
// reconcile (which refreshes an existing project's <home>/data/<key> snapshot) — so the two can
|
|
1362
|
+
// never interleave. Reuses the existing, proven primitive verbatim (fail-fast, no poll — the
|
|
1363
|
+
// primitive's own fail() call handles a contended lock, no extra catch needed here).
|
|
1364
|
+
//
|
|
1365
|
+
// sweep-orphan-reclaim-tickets: ALSO reports (and, with --force, removes) leaked reclaim
|
|
1366
|
+
// tickets found by pruneData — see its own comment; a separate, additive count from the
|
|
1367
|
+
// data-orphan sweep above.
|
|
1278
1368
|
function cmdPruneData(args) {
|
|
1279
1369
|
const home = resolveGlobalHome(process.env);
|
|
1280
1370
|
if (!fs.existsSync(path.join(home, STAMP_FILE))) {
|
|
1281
1371
|
fail("no global ADD install at " + home + " (.add-version not found) — nothing to prune");
|
|
1282
1372
|
}
|
|
1373
|
+
acquireUpdateLock(home, { timeout: null }, process.env);
|
|
1283
1374
|
let result;
|
|
1284
1375
|
try { result = pruneData(home, args.force); }
|
|
1285
1376
|
catch (_e) { fail("global registry " + registryPath(home) + " is corrupt — fix or delete it; not pruning"); }
|
|
1286
|
-
if (result.orphans.length === 0
|
|
1287
|
-
|
|
1377
|
+
if (result.orphans.length === 0 && result.ticketOrphans.length === 0) {
|
|
1378
|
+
log(" no orphaned snapshots — nothing to prune");
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (args.force) {
|
|
1382
|
+
if (result.orphans.length > 0) log(" ✓ " + result.removed.length + " removed");
|
|
1383
|
+
if (result.ticketOrphans.length > 0) log(" ✓ " + result.ticketsRemoved.length + " reclaim ticket(s) removed");
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1288
1386
|
for (const key of result.orphans) log(" orphan: " + key);
|
|
1289
|
-
log(" " + result.orphans.length + " orphan(s); re-run with --force to remove");
|
|
1387
|
+
if (result.orphans.length > 0) log(" " + result.orphans.length + " orphan(s); re-run with --force to remove");
|
|
1388
|
+
for (const ticket of result.ticketOrphans) log(" ticket orphan: " + ticket);
|
|
1389
|
+
if (result.ticketOrphans.length > 0) {
|
|
1390
|
+
log(" " + result.ticketOrphans.length + " reclaim ticket orphan(s); re-run with --force to remove");
|
|
1391
|
+
}
|
|
1290
1392
|
}
|
|
1291
1393
|
|
|
1292
1394
|
// init --global: install the managed layer ONCE to the shared home + register this project,
|
|
@@ -1477,7 +1579,9 @@ function acquireUpdateLock(home, { timeout = null } = {}, env = process.env) {
|
|
|
1477
1579
|
}
|
|
1478
1580
|
try { fs.writeSync(fd, process.pid + " " + new Date().toISOString() + "\n"); }
|
|
1479
1581
|
catch (_e) {} // diagnostics are best-effort — never fail an acquired lock over this
|
|
1582
|
+
const heartbeat = startLockHeartbeat(lockPath, staleAfterMs); // js-reclaim-lock-heartbeat
|
|
1480
1583
|
const release = () => {
|
|
1584
|
+
heartbeat.stop(); // cleared FIRST — never outlives the fd/lockPath it refreshes
|
|
1481
1585
|
try { fs.closeSync(fd); } catch (_e) {}
|
|
1482
1586
|
try { fs.unlinkSync(lockPath); } catch (_e) {}
|
|
1483
1587
|
};
|
|
@@ -1631,7 +1735,9 @@ function acquireProjectLock(addDir, env = process.env) {
|
|
|
1631
1735
|
}
|
|
1632
1736
|
try { fs.writeSync(fd, process.pid + " " + new Date().toISOString() + "\n"); }
|
|
1633
1737
|
catch (_e) {} // diagnostics are best-effort — never fail an acquired lock over this
|
|
1738
|
+
const heartbeat = startLockHeartbeat(lockPath, staleAfterMs); // js-reclaim-lock-heartbeat
|
|
1634
1739
|
const release = () => {
|
|
1740
|
+
heartbeat.stop(); // cleared FIRST — never outlives the fd/lockPath it refreshes
|
|
1635
1741
|
try { fs.closeSync(fd); } catch (_e) {}
|
|
1636
1742
|
try { fs.unlinkSync(lockPath); } catch (_e) {}
|
|
1637
1743
|
if (createdDir) {
|
|
@@ -1736,8 +1842,37 @@ function cmdUpdate(args) {
|
|
|
1736
1842
|
" refreshed) · your project state untouched.");
|
|
1737
1843
|
}
|
|
1738
1844
|
|
|
1845
|
+
// js-reclaim-lock-heartbeat: a test-only entrypoint so the Python subprocess suite can drive
|
|
1846
|
+
// real multi-process contention against acquireUpdateLock/acquireProjectLock without duplicating
|
|
1847
|
+
// their acquire/release logic in the test itself. Intercepted BEFORE the `cmd`/switch dispatch
|
|
1848
|
+
// below, guarded behind an undocumented flag — never listed in --help, never reachable via any
|
|
1849
|
+
// documented public command, so it carries zero surface on the real CLI path.
|
|
1850
|
+
async function cmdInternalAcquireLock(argv) {
|
|
1851
|
+
const kind = argv[0];
|
|
1852
|
+
const targetPath = argv[1];
|
|
1853
|
+
const holdMs = Number(argv[2]);
|
|
1854
|
+
let release;
|
|
1855
|
+
if (kind === "update") {
|
|
1856
|
+
release = acquireUpdateLock(targetPath, { timeout: null }, process.env);
|
|
1857
|
+
} else if (kind === "project") {
|
|
1858
|
+
release = acquireProjectLock(targetPath, process.env);
|
|
1859
|
+
} else {
|
|
1860
|
+
fail("internal_acquire_lock_bad_kind: expected 'update' or 'project', got '" + kind + "'");
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
log("HELD " + Date.now());
|
|
1864
|
+
await sleepAsync(holdMs); // event-loop-yielding — lets the heartbeat's setInterval fire
|
|
1865
|
+
log("RELEASED " + Date.now());
|
|
1866
|
+
release();
|
|
1867
|
+
process.exit(0);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1739
1870
|
async function main() {
|
|
1740
1871
|
const argv = process.argv.slice(2);
|
|
1872
|
+
if (argv[0] === "--internal-acquire-lock") {
|
|
1873
|
+
await cmdInternalAcquireLock(argv.slice(1));
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1741
1876
|
const cmd = argv[0] && !argv[0].startsWith("--") ? argv.shift() : "init";
|
|
1742
1877
|
const args = parseArgs(argv);
|
|
1743
1878
|
switch (cmd) {
|
|
@@ -113,6 +113,6 @@ The default is one task at a time. But when a milestone holds several tasks whos
|
|
|
113
113
|
|
|
114
114
|
**Design for failure (required).** Lease each task to its worker with a timeout — if a worker dies, release the claim back to READY rather than trusting partial work. A worker that hits a stop-and-escalate blocks only its own task; siblings keep running. And if several workers fail in one wave, trip a circuit-breaker and fall back to sequential — repeated failure means the scope was wrong, not the parallelism.
|
|
115
115
|
|
|
116
|
-
**The hard boundary.** The orchestrator owns every shared write — `state.json`, `MILESTONE.md`, and each `add.py advance`/`gate` call (always with the explicit task slug). A worker owns only its own task directory and is isolated in a git worktree, so concurrent builds cannot collide. Merge is **serial**: bring worktrees back one at a time and run an **integration Verify** for the concurrency and architecture conflicts that two-green-in-isolation tasks can still produce — automation never auto-passes that step.
|
|
116
|
+
**The hard boundary.** The orchestrator owns every shared write — `state.json`, `MILESTONE.md`, and each `add.py advance`/`gate` call (always with the explicit task slug). A worker owns only its own task directory and is isolated in a git worktree — ADD's default for any agent-spawned step, not only a wave — so concurrent builds cannot collide. Merge is **serial**: bring worktrees back one at a time and run an **integration Verify** for the concurrency and architecture conflicts that two-green-in-isolation tasks can still produce — automation never auto-passes that step.
|
|
117
117
|
|
|
118
118
|
The full, agent-agnostic worker contract (the prompt a worker runs) and the per-runner spawn adapter live in the skill's `streams.md`; this section is the *why* and the safety frame, not the operational recipe.
|
package/docs/18-personas.md
CHANGED
|
@@ -35,11 +35,11 @@ task actually applies one.
|
|
|
35
35
|
|
|
36
36
|
Personas are **living documents**: they improve through the same loop the foundation uses. In a
|
|
37
37
|
task's **observe** phase the AI emits a **persona delta** — a one-line, tagged proposal to add or
|
|
38
|
-
sharpen a critical-rule
|
|
38
|
+
sharpen a critical-rule, success-metric, anti-pattern, or ability, written `open` with evidence (just like a lesson learned).
|
|
39
39
|
At a retrospective the human **consolidates** confirmed deltas into the persona file (the same
|
|
40
40
|
`fold` step the foundation uses), bumping it forward. The consolidation is judgment-free
|
|
41
41
|
transcription: the engine routes a confirmed delta into the named
|
|
42
|
-
persona's `## Critical Rules
|
|
42
|
+
persona's `## Critical Rules`, `## Success Metrics`, `## Anti-patterns`, or `## Abilities` section and **never clobbers** existing content.
|
|
43
43
|
So a persona gets *more* accurate every milestone instead of drifting.
|
|
44
44
|
|
|
45
45
|
## Apply — three surfaces
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilotspace/add",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "ADD (AI-Driven Development). One skill. Eight steps. Five disciplines. Every feature ships through the loop — a minimal, state-tracked Claude Code skill that ships the AIDD book as its trust layer.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"add": "bin/cli.js"
|
package/skill/add/advisor.md
CHANGED
|
@@ -34,7 +34,8 @@ surrounding decisions. Return a verdict; do not record state.
|
|
|
34
34
|
</objective>
|
|
35
35
|
|
|
36
36
|
<persona>
|
|
37
|
-
SELECT the
|
|
37
|
+
SELECT the persona by frontmatter — flow: match first, then domain; read ONE body —
|
|
38
|
+
and load `.add/personas/{{PERSONA_SLUG}}.md` —
|
|
38
39
|
Identity→your stance · Critical Rules→constraints · Success Metrics→done-bar (streams.md's worker
|
|
39
40
|
contract). No match → a {{DOMAIN}} engineer, correctness over speed; never blocks.
|
|
40
41
|
Work step by step, following the plan:
|
package/skill/add/deltas.md
CHANGED
|
@@ -16,7 +16,7 @@ Each delta begins on its own **tag line**; the learning may wrap:
|
|
|
16
16
|
- `<status>` — `open` | `folded` | `rejected`. A **newly emitted delta is `open`**.
|
|
17
17
|
- `<learning>` — the insight; the tag line comes **first**, `(evidence: …)` **closes** the delta.
|
|
18
18
|
- `(evidence: …)` — **required**, non-empty: a failing scenario, a production signal, a review note. No evidence → it is an opinion, not a delta.
|
|
19
|
-
- **persona target (optional)** — a competency lesson MAY add `· persona:<slug> · <critical-rule|success-metric>` in brackets, e.g. `- [UDD · open · persona:ui-designer · success-metric] 4.5:1 contrast (evidence: audit)`. At `add.py fold` it lands in `.add/personas/<slug>.md` under that section (newest-first, never clobbering) instead of `PROJECT.md` (`fold.md`).
|
|
19
|
+
- **persona target (optional)** — a competency lesson MAY add `· persona:<slug> · <critical-rule|success-metric|anti-pattern|ability>` in brackets, e.g. `- [UDD · open · persona:ui-designer · success-metric] 4.5:1 contrast (evidence: audit)`. At `add.py fold` it lands in `.add/personas/<slug>.md` under that section (newest-first, never clobbering) instead of `PROJECT.md` (`fold.md`).
|
|
20
20
|
|
|
21
21
|
A long learning may wrap — `add.py check` joins continuation lines into **one** delta:
|
|
22
22
|
|
package/skill/add/design.md
CHANGED
|
@@ -57,8 +57,8 @@ partial per catalog component), with realistic **mock** data.
|
|
|
57
57
|
feature's `TASK.md`** — so the approved screen is traceable from the task. The HTML mock is the
|
|
58
58
|
*visible evidence*; the json-render tree is the *machine-checkable* record.
|
|
59
59
|
|
|
60
|
-
**Persona evidence checklist.** Before design-confirm,
|
|
61
|
-
(`.add/personas/*`
|
|
60
|
+
**Persona evidence checklist.** Before design-confirm, load the `flow: design` personas
|
|
61
|
+
(`.add/personas/*` frontmatter, else description-match) and render their `## Success Metrics`
|
|
62
62
|
as a confirmable **checklist** beside the captured image — **both dimensions**: **UI-Designer**
|
|
63
63
|
(visual + WCAG-AA **accessibility**) and **UX-Researcher** (methodology-first,
|
|
64
64
|
**validated by user evidence, not assumed**). Each item traces to a success-metric
|
package/skill/add/fold.md
CHANGED
|
@@ -6,7 +6,7 @@ This **closes the loop**. `deltas.md` lets a task EMIT `open` lessons in OBSERVE
|
|
|
6
6
|
|
|
7
7
|
## When to consolidate
|
|
8
8
|
|
|
9
|
-
At **milestone close**, or
|
|
9
|
+
At **milestone close**, or on demand when opens pile up. One `add.py fold` = ONE session: one `foundation-version` bump, every resolved lesson stamped with it.
|
|
10
10
|
|
|
11
11
|
## The ritual
|
|
12
12
|
|
|
@@ -25,11 +25,11 @@ At **milestone close**, or **on demand** when open lessons pile up. One `add.py
|
|
|
25
25
|
| competency | consolidates into | how |
|
|
26
26
|
|------------|-----------|-----|
|
|
27
27
|
| `DDD` | `PROJECT.md` §Domain | transcribed bullet at the top (newest-first) |
|
|
28
|
-
| `SDD` | `PROJECT.md` §Spec |
|
|
29
|
-
| `UDD` | `PROJECT.md` §Users |
|
|
30
|
-
| `TDD` | `CONVENTIONS.md` §Method learnings |
|
|
31
|
-
| `ADD` | `CONVENTIONS.md` §Method learnings |
|
|
32
|
-
| `persona:<slug>` | `.add/personas/<slug>.md` §Critical Rules / §Success Metrics | dated bullet at top; schema stays conformant |
|
|
28
|
+
| `SDD` | `PROJECT.md` §Spec | same |
|
|
29
|
+
| `UDD` | `PROJECT.md` §Users | same |
|
|
30
|
+
| `TDD` | `CONVENTIONS.md` §Method learnings | same |
|
|
31
|
+
| `ADD` | `CONVENTIONS.md` §Method learnings | same |
|
|
32
|
+
| `persona:<slug>` | `.add/personas/<slug>.md` §Critical Rules / §Success Metrics / §Anti-patterns / §Abilities | dated bullet at top; schema stays conformant |
|
|
33
33
|
|
|
34
34
|
A `persona:<slug> · <hint>` lesson routes into that persona doc, not a foundation file; it is still flipped `folded` and still bumps the version once (`deltas.md`).
|
|
35
35
|
|
|
@@ -45,7 +45,7 @@ A `persona:<slug> · <hint>` lesson routes into that persona doc, not a foundati
|
|
|
45
45
|
- `missing_route_section` — a lesson routes to a foundation section that does not exist. Add the section header, then re-run. Nothing is written.
|
|
46
46
|
- `no_foundation_version` — `PROJECT.md` carries no parseable `foundation-version:` marker to bump.
|
|
47
47
|
- `missing_persona_target` — a `persona:<slug>` lesson with no `.add/personas/<slug>.md`. Fail-closed: nothing written, no bump. Seed the persona first.
|
|
48
|
-
- `persona_section_unroutable` — the section hint is not `critical-rule
|
|
48
|
+
- `persona_section_unroutable` — the section hint is not one of `critical-rule | success-metric | anti-pattern | ability`. Nothing is written.
|
|
49
49
|
- `persona_clobber_forbidden` — INVARIANT: a persona consolidation prepends only; it never drops existing content or breaks the schema.
|
|
50
50
|
</reject_codes>
|
|
51
51
|
|
package/skill/add/intake.md
CHANGED
|
@@ -31,8 +31,13 @@ fits the active milestone → `task`.
|
|
|
31
31
|
|
|
32
32
|
**One-task gap rule.** If the request is ONE task but does NOT fit the active milestone's stated
|
|
33
33
|
scope, do not force it into `sub-milestone` (which requires "too big for one task"): create a new
|
|
34
|
-
micro-milestone to house it (`new-milestone` + `new-task`) —
|
|
35
|
-
|
|
34
|
+
micro-milestone to house it (`new-milestone` + `new-task`) — ledger attribution + clear
|
|
35
|
+
exit criteria without inflating scope.
|
|
36
|
+
|
|
37
|
+
**Batched intake.** N same-bucket items arriving together (one directive; a drafted
|
|
38
|
+
milestone's task list) classify as ONE proposal: one report listing every item, one
|
|
39
|
+
human confirm covering the batch — never N sequential asks. Mixed buckets stay
|
|
40
|
+
`split_required`.
|
|
36
41
|
|
|
37
42
|
## What you emit (the proposal)
|
|
38
43
|
|
|
@@ -59,25 +64,25 @@ goal/body, the new TASK.md, or a note in the affected TASK.md — never in state
|
|
|
59
64
|
|
|
60
65
|
## Roadmap — a request that is several milestones
|
|
61
66
|
|
|
62
|
-
Some requests decompose into **N>1 milestones of the same line** — a roadmap
|
|
67
|
+
Some requests decompose into **N>1 milestones of the same line** — a roadmap.
|
|
63
68
|
Don't create only the first and lose the rest. Instead:
|
|
64
69
|
|
|
65
70
|
1. **Propose** the roadmap — the ordered milestone list, each with a one-line goal.
|
|
66
|
-
2. **Confirm** — the human confirms
|
|
67
|
-
milestones unprompted
|
|
71
|
+
2. **Confirm** — the human confirms before anything is created; never auto-create N
|
|
72
|
+
milestones unprompted (the `ask_human` floor holds).
|
|
68
73
|
3. **Create** all N on confirm — the first with `add.py new-milestone <slug>` (active), the rest
|
|
69
74
|
with `add.py new-milestone --queued <slug>` (status `queued`, not focused).
|
|
70
|
-
4. **Promote**
|
|
71
|
-
|
|
75
|
+
4. **Promote** — `add.py activate <slug>` flips each queued→active as started; one active
|
|
76
|
+
milestone at a time, the queue is the agreed backlog surfaced at resume.
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
NOT `split_required` (that is for a request spanning **different buckets**); a roadmap is
|
|
79
|
+
several milestones of the **same line**, created queued.
|
|
75
80
|
|
|
76
81
|
## Worked examples (from this project's own history)
|
|
77
82
|
|
|
78
83
|
| request | bucket | rationale |
|
|
79
84
|
|---------|--------|-----------|
|
|
80
|
-
| give ADD a hosted web dashboard | new-major | a new product theme
|
|
81
|
-
| add the build corridor + tests-red-before-build | sub-milestone | a slice of the live v4
|
|
85
|
+
| give ADD a hosted web dashboard | new-major | a new product theme → a fresh major line (v5) |
|
|
86
|
+
| add the build corridor + tests-red-before-build | sub-milestone | a slice of the live v4 theme, too big for one task → v4-2 |
|
|
82
87
|
| expose owner/stop as --json | task | fits the active v4-1 (intake interface) scope → one task |
|
|
83
|
-
| guide --json phase/gate should be nullable | change-request | changes
|
|
88
|
+
| guide --json phase/gate should be nullable | change-request | changes a FROZEN contract → reopen its CONTRACT, never a new milestone |
|
package/skill/add/loop.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# The dynamic loop — open deltas and extras become the next tasks
|
|
2
2
|
|
|
3
|
-
A milestone is
|
|
4
|
-
This guide drives
|
|
5
|
-
|
|
6
|
-
are all met.
|
|
3
|
+
A milestone is done when its **GOAL** is met, not when its tasks are.
|
|
4
|
+
This guide drives toward that goal: turn what each task leaves behind (open lessons, and
|
|
5
|
+
work discovered but out of scope) into the next tasks until the exit criteria are all met.
|
|
7
6
|
|
|
8
7
|
You **gather and propose**; the **human confirms**; the existing `add.py new-task`
|
|
9
8
|
creates each one. The engine never decides what the next task is — that is judgment.
|
|
@@ -24,15 +23,15 @@ both refuse a milestone that is not done. The one gate is enough — there is no
|
|
|
24
23
|
|
|
25
24
|
## The loop
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
Every task done but not the goal? `add.py status` shows
|
|
28
27
|
`goal not met (m/n exit criteria)`. That is the cue:
|
|
29
28
|
|
|
30
29
|
1. **Gather** the carried inventory:
|
|
31
|
-
- open lessons — `add.py deltas` (
|
|
30
|
+
- open lessons — `add.py deltas` (§7 deltas still `open`) + the deferred pile — `add.py deltas --carried`;
|
|
32
31
|
- the planned-but-unscaffolded tasks — the plan-vs-state line in `add.py status`;
|
|
33
32
|
- any reopened task — one a deepened verify returned to the flow (see below).
|
|
34
33
|
2. **Propose** the next tasks: for each carried item worth doing now, draft a one-line task
|
|
35
|
-
(slug + title + why) and show the human. Group
|
|
34
|
+
(slug + title + why) and show the human. Group trivial ones; no noise.
|
|
36
35
|
3. **Confirm** — the human accepts, edits, or declines each. No task is created without this.
|
|
37
36
|
4. **Create** each accepted task — `add.py new-task <slug> --title "..."` — and run it through
|
|
38
37
|
the normal flow (specify → … → verify).
|
|
@@ -61,16 +61,16 @@ Capture each surfaced decision as an **ADR** in `PROJECT.md` **Key Decisions** a
|
|
|
61
61
|
Draft the full bundle **§1–§4** incl. the **§4 red suite** (`phases/4-tests.md`); the lock approves it whole. **Leave §3 `Status: DRAFT`** — the lock is its approval. You MAY `advance` pre-lock, but the engine **refuses build** until you `lock` (`setup_unlocked`). Sequence: **bundle (§1–§4, tests RED) → lock → build** — the red suite must FAIL before build.
|
|
62
62
|
4. **Write `.add/SETUP-REVIEW.md`** per `setup-review.md`: every drafted decision, **lowest-confidence-first**, tagged `guessed` | `evidence-grounded`.
|
|
63
63
|
|
|
64
|
-
## Run mode — how the build will be driven (propose
|
|
64
|
+
## Run mode — how the build will be driven (propose sequential + auto; confirm to keep)
|
|
65
65
|
|
|
66
66
|
Before the lock, surface the **run mode** — autonomy + streams (`run.md` · `streams.md`):
|
|
67
67
|
|
|
68
68
|
| Run mode | Human gates | Concurrency |
|
|
69
69
|
|----------|-------------|-------------|
|
|
70
|
+
| **sequential · auto** *(default)* | contract freeze **only** — Verify auto-PASSes on evidence | one task at a time |
|
|
70
71
|
| **sequential · manual/conservative** | contract freeze **and** every Verify | one task; safest |
|
|
71
|
-
| **parallel · auto** *(default)* | contract freeze **only** — Verify auto-PASSes on evidence | `add.py waves` overlaps independent builds behind frozen contracts |
|
|
72
72
|
|
|
73
|
-
**Propose `
|
|
73
|
+
**Propose `sequential + auto`; confirm-to-keep** (raise the gate: `add.py autonomy set conservative --project`; opt into parallel streaming for a milestone with independent tasks — `add.py streams set parallel --project`, then `add.py waves` schedules the fan-out). Record in **`PROJECT.md` Key Decisions**.
|
|
74
74
|
|
|
75
75
|
Floor: **one human approval per contract**.
|
|
76
76
|
|