@pilotspace/add 2.2.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 +104 -0
- package/GETTING-STARTED.md +19 -19
- package/agents/add-advisor.md +3 -2
- package/bin/cli.js +45 -149
- package/package.json +1 -1
- package/skill/add/SKILL.md +6 -5
- package/skill/add/beyond.md +4 -0
- package/skill/add/deltas.md +1 -1
- package/skill/add/design.md +1 -1
- package/skill/add/intake.md +17 -0
- package/skill/add/phases/build.md +22 -0
- package/skill/add/phases/direction.md +13 -10
- package/skill/add/phases/verify.md +23 -2
- package/skill/add/strategy.md +54 -0
- package/tooling/add.py +407 -33
- package/tooling/add_engine/constants.py +11 -16
- package/tooling/add_engine/guidelines.py +86 -136
- package/tooling/templates/GLOSSARY.md.tmpl +1 -1
- package/tooling/templates/MILESTONE.md.tmpl +9 -0
- package/tooling/templates/PLAN.md.tmpl +13 -15
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,110 @@ 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
|
+
|
|
51
|
+
## [2.3.0] — 2026-07-24
|
|
52
|
+
|
|
53
|
+
Minor: three waves — a **signal graph** view over the task DAG, the **§2
|
|
54
|
+
Scenarios fold** into §4, and a **call-lean pass** that kills the two measured
|
|
55
|
+
WM1 freeze/scope call sinks. Engine changes are additive; templates migrate
|
|
56
|
+
in place (no manual task-file edits).
|
|
57
|
+
|
|
58
|
+
### Signal graph — one addressable node for every note/todo/delta
|
|
59
|
+
- **unified signal model** — note, todo, and spec-delta collapse into ONE
|
|
60
|
+
addressable `signal` node (status + edges), projected as a VIEW, not a new
|
|
61
|
+
store; `_signals` reader.
|
|
62
|
+
- **`graph --signals`** — opt-in overlay renders signals on the task DAG;
|
|
63
|
+
**exit-criteria** render as `ec_` *delivered-by* nodes (a milestone's own
|
|
64
|
+
criteria appear as ✓/○ nodes wired to the tasks that meet them).
|
|
65
|
+
- **`graph --html`** — emits a self-rendering HTML page to tmp (Mermaid inline,
|
|
66
|
+
no external host).
|
|
67
|
+
- **atomicity signal** — the freeze SEEDS a persistent atomicity signal into the
|
|
68
|
+
task's todos instead of an ephemeral print, so the nudge survives the session.
|
|
69
|
+
|
|
70
|
+
### §2 Scenarios folded into §4 — one place for cases
|
|
71
|
+
- the standalone **§2 SCENARIOS** section is **retired in place**: pass/fail cases
|
|
72
|
+
now live with the tests in **§4 · TESTS & SCENARIOS** (primary-case rigor). The
|
|
73
|
+
§3–§7 numbers are unchanged so the freeze parser and every §-reference keep
|
|
74
|
+
working — the §1→§3 jump is intentional.
|
|
75
|
+
- **PROJECT.md** gains a managed `ADD:SPECS` pointer block (init/migrate inject it
|
|
76
|
+
from the 5-DD spec set); foundation references reconciled to the `.add/specs/`
|
|
77
|
+
model; the generated **CLAUDE.md** block finalized in the generator.
|
|
78
|
+
- the Step-2 Scenarios book chapter folds into Step-4; `add-flow.png` retired.
|
|
79
|
+
|
|
80
|
+
### Call-lean pass — the WM1 freeze/scope sinks die at the source
|
|
81
|
+
- **freeze-flag slot** — `PLAN.md.tmpl` §3 carries a drafted-blank *Least-sure
|
|
82
|
+
flag* slot; the first freeze no longer fails `unflagged_freeze` (0/6 reps in the
|
|
83
|
+
re-measure, was 3/3). The unfilled part-menu placeholder still never satisfies
|
|
84
|
+
the gate — the floor holds.
|
|
85
|
+
- **scope-first freeze** — a §3 Scope that resolves to zero cover is now refused
|
|
86
|
+
`scope_unresolved` AT the freeze with a paste-ready fix, instead of surfacing
|
|
87
|
+
later as `scope_violation` → re-cross → re-gate. UNDECLARED stays grandfathered;
|
|
88
|
+
greenfield `[MISSING]` tokens still freeze; a `.add/tasks/` token gets a
|
|
89
|
+
task-dir teach note.
|
|
90
|
+
- **scope-walk prunes** — `.venv` · `venv` · `.tox` · `.mypy_cache` · `.ruff_cache`
|
|
91
|
+
· `.eggs` and any `*.egg-info` dir are pruned from the scope walk, so an
|
|
92
|
+
in-workspace virtualenv or `pip install -e .` metadata is never read as an
|
|
93
|
+
out-of-scope write. `dist`/`build` stay watched (can be a real write-set). The
|
|
94
|
+
untouched-Scope-default warning now self-explains (a note, not a blocker; clears
|
|
95
|
+
only by editing the Scope line — re-cross does not clear it).
|
|
96
|
+
- **honest fidelity meter** — the artifact-blind LLM `spec_fidelity` metric is
|
|
97
|
+
replaced by a deterministic `requirement_coverage` (frozen per-WM checklists +
|
|
98
|
+
probes, no LLM in the metric path); `oracle_pass_rate` promoted to the headline;
|
|
99
|
+
the judge demoted to an advisory `code_quality_annotation`.
|
|
100
|
+
- **§3 Build-strategy relabel** — *Scope (may touch)* is the HARD scope-lock; the
|
|
101
|
+
rest (Strategy · Regression floor · Persona) is SOFT/optional. `MILESTONE.md`
|
|
102
|
+
gains a drafted-blank `## Strategy` slot.
|
|
103
|
+
|
|
104
|
+
Retired (no CLI/install surface change): the standalone **§2 SCENARIOS** section
|
|
105
|
+
(folded into §4) · the **ccsk / rule-file** mode · `add-flow.png`.
|
|
106
|
+
|
|
107
|
+
Pinned by `test_release_2_3_0.py`. `ENGINE_MD5` re-aimed to `60eef504…`
|
|
108
|
+
(scope refusal + walk prunes); the four tooling twins stay byte-identical;
|
|
109
|
+
full tooling suite green (2277 tests).
|
|
110
|
+
|
|
7
111
|
## [2.2.0] — 2026-07-22
|
|
8
112
|
|
|
9
113
|
Minor: the Direction beat gains a **fable reasoning discipline** — a prompt-only
|
package/GETTING-STARTED.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
package/agents/add-advisor.md
CHANGED
|
@@ -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
|
|
29
|
-
|
|
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
|
@@ -39,7 +39,7 @@ function parseArgs(argv) {
|
|
|
39
39
|
// hint only echoes flags the user actually chose (shortest true command).
|
|
40
40
|
const args = { _: [], force: false, check: false, noSkill: false, stage: null, name: null,
|
|
41
41
|
yes: false, nonInteractive: false, global: false, globalData: false,
|
|
42
|
-
fromGlobalData: false,
|
|
42
|
+
fromGlobalData: false, lockTimeout: null };
|
|
43
43
|
for (let i = 0; i < argv.length; i++) {
|
|
44
44
|
const a = argv[i];
|
|
45
45
|
if (a === "--force") args.force = true;
|
|
@@ -61,9 +61,6 @@ function parseArgs(argv) {
|
|
|
61
61
|
// already provides the `add` skill, so a plugin bootstrap uses this to materialize
|
|
62
62
|
// .add/tooling/ into the project without a duplicate .claude/skills/add.
|
|
63
63
|
else if (a === "--no-skill") args.noSkill = true;
|
|
64
|
-
// --rule-file: write the ADD block to .claude/rules/add-workflows.md + reference it from
|
|
65
|
-
// CLAUDE.md instead of inlining (auto-on for ccsk projects with a .ccsk/ dir).
|
|
66
|
-
else if (a === "--rule-file") args.ruleFile = true;
|
|
67
64
|
else if (a === "--stage" || a === "--name") {
|
|
68
65
|
const v = argv[++i];
|
|
69
66
|
// fail loudly on a trailing/abutting flag — never silently drop a value
|
|
@@ -263,126 +260,6 @@ function writeAgentPointer(target, profile) {
|
|
|
263
260
|
}
|
|
264
261
|
}
|
|
265
262
|
|
|
266
|
-
// --- rule-file mode: ccsk-style relocation of the CLAUDE.md block ---------------------
|
|
267
|
-
// Mirror of add.py / _installer.py rule-file logic (twins by duplication). For ccsk projects
|
|
268
|
-
// (.ccsk/ dir) or an explicit --rule-file, the ADD pointer goes to .claude/rules/add-workflows.md
|
|
269
|
-
// and CLAUDE.md keeps only a reference bullet. CLAUDE-only — AGENTS.md/.clinerules stay inline.
|
|
270
|
-
const RULES_FILE_REL = path.join(".claude", "rules", "add-workflows.md");
|
|
271
|
-
const WORKFLOW_HEADINGS = ["Rules & Workflows", "Workflows", "Rules"];
|
|
272
|
-
const RULE_REF_LINE = "- ADD (AI-Driven Development) Workflows rules: ./.claude/rules/add-workflows.md";
|
|
273
|
-
|
|
274
|
-
// True when the ADD block belongs in .claude/rules/add-workflows.md instead of inline.
|
|
275
|
-
// Re-derived from disk: explicit flag, a ccsk project (.ccsk/), or a rule file already present.
|
|
276
|
-
function ruleFileMode(root, flag) {
|
|
277
|
-
if (flag) return true;
|
|
278
|
-
try {
|
|
279
|
-
if (fs.existsSync(path.join(root, ".ccsk")) &&
|
|
280
|
-
fs.statSync(path.join(root, ".ccsk")).isDirectory()) return true;
|
|
281
|
-
if (fs.existsSync(path.join(root, RULES_FILE_REL))) return true;
|
|
282
|
-
} catch (_e) { /* fail-soft */ }
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Remove an inline ADD:BEGIN..END region (migration), collapsing the gap. An unterminated
|
|
287
|
-
// BEGIN is left as-is rather than eating the rest of the file.
|
|
288
|
-
function stripInlineBlock(text) {
|
|
289
|
-
const begin = text.indexOf(GUIDE_BEGIN);
|
|
290
|
-
if (begin === -1) return text;
|
|
291
|
-
let end = text.indexOf(GUIDE_END, begin);
|
|
292
|
-
if (end === -1) return text;
|
|
293
|
-
end += GUIDE_END.length;
|
|
294
|
-
const head = text.slice(0, begin).replace(/\n+$/, "");
|
|
295
|
-
const tail = text.slice(end).replace(/^\n+/, "");
|
|
296
|
-
if (head && tail) return head + "\n\n" + tail;
|
|
297
|
-
return head || tail;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Insert the ADD rule-file bullet under an existing Workflows/Rules heading, or append a fresh
|
|
301
|
-
// '## Workflows' section. Caller guarantees the bullet is absent.
|
|
302
|
-
function insertRuleReference(text) {
|
|
303
|
-
const lines = text.split("\n");
|
|
304
|
-
let headingIdx = -1;
|
|
305
|
-
for (let i = 0; i < lines.length; i++) {
|
|
306
|
-
const m = lines[i].match(/^(#{1,6})\s+(.*?)\s*$/);
|
|
307
|
-
if (m && WORKFLOW_HEADINGS.some((h) => m[2].trim().toLowerCase() === h.toLowerCase())) {
|
|
308
|
-
headingIdx = i;
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (headingIdx === -1) {
|
|
313
|
-
const body = text.replace(/\n+$/, "");
|
|
314
|
-
const sep = body ? "\n\n" : "";
|
|
315
|
-
return body + sep + "## Workflows\n\n" + RULE_REF_LINE + "\n";
|
|
316
|
-
}
|
|
317
|
-
const level = lines[headingIdx].match(/^(#{1,6})/)[1].length;
|
|
318
|
-
let end = lines.length;
|
|
319
|
-
for (let j = headingIdx + 1; j < lines.length; j++) {
|
|
320
|
-
const m = lines[j].match(/^(#{1,6})\s+/);
|
|
321
|
-
if (m && m[1].length <= level) { end = j; break; }
|
|
322
|
-
}
|
|
323
|
-
let insertAt = headingIdx + 1;
|
|
324
|
-
for (let j = headingIdx + 1; j < end; j++) {
|
|
325
|
-
if (lines[j].trim()) insertAt = j + 1;
|
|
326
|
-
}
|
|
327
|
-
lines.splice(insertAt, 0, RULE_REF_LINE);
|
|
328
|
-
return lines.join("\n");
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Make CLAUDE.md reference the ADD rule file under a Workflows/Rules heading, migrating any prior
|
|
332
|
-
// inline ADD block out. created|updated|unchanged|skipped; .bak on change; fail-soft.
|
|
333
|
-
function ensureClaudeReference(claudeMd) {
|
|
334
|
-
try {
|
|
335
|
-
const existed = fs.existsSync(claudeMd);
|
|
336
|
-
const current = existed ? fs.readFileSync(claudeMd, "utf8") : "";
|
|
337
|
-
let next = stripInlineBlock(current);
|
|
338
|
-
if (next.indexOf("add-workflows.md") === -1) next = insertRuleReference(next);
|
|
339
|
-
if (!next.endsWith("\n")) next += "\n";
|
|
340
|
-
if (next === current) return "unchanged";
|
|
341
|
-
if (existed) fs.writeFileSync(claudeMd + ".bak", current);
|
|
342
|
-
fs.writeFileSync(claudeMd, next);
|
|
343
|
-
return existed ? "updated" : "created";
|
|
344
|
-
} catch (e) {
|
|
345
|
-
warn("could not write CLAUDE.md — " + (e && e.message ? e.message : e) + "; skipped");
|
|
346
|
-
return "skipped";
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Rule-file variant of writeAgentPointer for the Claude profile: write the ADD pointer block to
|
|
351
|
-
// .claude/rules/add-workflows.md and leave a reference in CLAUDE.md. Fail-soft.
|
|
352
|
-
function writeRuleFilePointer(target, profile) {
|
|
353
|
-
const rulesPath = path.join(target, RULES_FILE_REL);
|
|
354
|
-
const block = agentPointerBlock(profile);
|
|
355
|
-
try {
|
|
356
|
-
if (fs.existsSync(rulesPath)) {
|
|
357
|
-
const current = fs.readFileSync(rulesPath, "utf8");
|
|
358
|
-
const begin = current.indexOf(GUIDE_BEGIN);
|
|
359
|
-
let next;
|
|
360
|
-
if (begin !== -1) {
|
|
361
|
-
const endIdx = current.indexOf(GUIDE_END, begin);
|
|
362
|
-
if (endIdx !== -1) {
|
|
363
|
-
next = current.slice(0, begin) + block + current.slice(endIdx + GUIDE_END.length);
|
|
364
|
-
} else {
|
|
365
|
-
next = current.replace(/\n+$/, "") + "\n\n" + block + "\n";
|
|
366
|
-
}
|
|
367
|
-
} else {
|
|
368
|
-
next = current.replace(/\n+$/, "") + "\n\n" + block + "\n";
|
|
369
|
-
}
|
|
370
|
-
if (next !== current) {
|
|
371
|
-
fs.writeFileSync(rulesPath + ".bak", current);
|
|
372
|
-
fs.writeFileSync(rulesPath, next);
|
|
373
|
-
}
|
|
374
|
-
} else {
|
|
375
|
-
fs.mkdirSync(path.dirname(rulesPath), { recursive: true });
|
|
376
|
-
fs.writeFileSync(rulesPath, block + "\n");
|
|
377
|
-
}
|
|
378
|
-
} catch (e) {
|
|
379
|
-
warn("could not write " + RULES_FILE_REL + " — " + (e && e.message ? e.message : e) + "; skipped");
|
|
380
|
-
return "skipped";
|
|
381
|
-
}
|
|
382
|
-
ensureClaudeReference(path.join(target, "CLAUDE.md"));
|
|
383
|
-
return "ok";
|
|
384
|
-
}
|
|
385
|
-
|
|
386
263
|
// --- interactive layer (clack on a real TTY; plain text everywhere else) -----
|
|
387
264
|
// Designed-for-failure: any doubt (non-TTY, CI, --yes, a failed import, an
|
|
388
265
|
// un-promptable stream) degrades to the EXACT plain-text path below. The clack
|
|
@@ -650,16 +527,7 @@ function dropFiles(args, target, profile, intent) {
|
|
|
650
527
|
// Agent detection: write THE detected agent's integration file (a marker-delimited
|
|
651
528
|
// pointer init's sync-guidelines later supersedes) + tailor the closing next-step.
|
|
652
529
|
// Best-effort + fail-soft — never aborts the successful drop above.
|
|
653
|
-
|
|
654
|
-
// .claude/rules/add-workflows.md + a reference — CLAUDE-only; other agents stay inline.
|
|
655
|
-
if (profile.integration_file === "CLAUDE.md" && ruleFileMode(target, args.ruleFile)) {
|
|
656
|
-
if (fs.existsSync(path.join(target, ".ccsk"))) {
|
|
657
|
-
log(" ccsk detected — ADD rules go to .claude/rules/add-workflows.md");
|
|
658
|
-
}
|
|
659
|
-
writeRuleFilePointer(target, profile);
|
|
660
|
-
} else {
|
|
661
|
-
writeAgentPointer(target, profile);
|
|
662
|
-
}
|
|
530
|
+
writeAgentPointer(target, profile);
|
|
663
531
|
|
|
664
532
|
// Gemini CLI auto-loads GEMINI.md, not AGENTS.md — so for the gemini profile we ALSO merge
|
|
665
533
|
// .gemini/settings.json (context.fileName) to load the AGENTS.md pointer. Fail-soft + idempotent.
|
|
@@ -1341,6 +1209,28 @@ function agedReclaimTickets(dir, lockFileName, staleAfterSeconds, nowMs) {
|
|
|
1341
1209
|
}).sort();
|
|
1342
1210
|
}
|
|
1343
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
|
+
|
|
1344
1234
|
// prune-data: reclaim ORPHANED snapshots under <home>/data. An orphan is a <home>/data/<key>
|
|
1345
1235
|
// whose key is owned by NO LIVE registry entry (LIVE = a registered path that still EXISTS on
|
|
1346
1236
|
// disk) — so unregistered AND registered-but-vanished are BOTH orphans (the explicit reclaim;
|
|
@@ -1565,9 +1455,10 @@ function acquireUpdateLock(home, { timeout = null } = {}, env = process.env) {
|
|
|
1565
1455
|
// brand-new, not-yet-stale ticket for the SAME generation, so A and C would BOTH
|
|
1566
1456
|
// believe they are the sole reclaimer — the exact double-hold bug this ticket
|
|
1567
1457
|
// mechanism was built to prevent, reintroduced one level down).
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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)) {
|
|
1571
1462
|
try { fs.unlinkSync(ticketPath); } catch (_e) {} // already gone — harmless
|
|
1572
1463
|
}
|
|
1573
1464
|
try { ticketFd = fs.openSync(ticketPath, "wx"); } catch (_e) {} // someone else won it
|
|
@@ -1594,9 +1485,12 @@ function acquireUpdateLock(home, { timeout = null } = {}, env = process.env) {
|
|
|
1594
1485
|
// generation we ticketed for; otherwise our ticket is moot — leave the (unrelated,
|
|
1595
1486
|
// currently live) file completely alone and let the loop's own open/EEXIST/age logic
|
|
1596
1487
|
// re-evaluate reality fresh on the next iteration.
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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)) {
|
|
1600
1494
|
try { fs.unlinkSync(lockPath); } catch (_e) {} // already gone — harmless
|
|
1601
1495
|
}
|
|
1602
1496
|
} finally {
|
|
@@ -1728,9 +1622,10 @@ function acquireProjectLock(addDir, env = process.env) {
|
|
|
1728
1622
|
// mechanism was built to prevent, reintroduced one level down). Exactly one extra
|
|
1729
1623
|
// self-heal attempt — never a second, matching M7's own "no poll, ever" discipline
|
|
1730
1624
|
// already governing the main lock's own reclaim above.
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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)) {
|
|
1734
1629
|
try { fs.unlinkSync(ticketPath); } catch (_e) {} // already gone — harmless
|
|
1735
1630
|
}
|
|
1736
1631
|
try { ticketFd = fs.openSync(ticketPath, "wx"); } catch (_e) {} // someone else won it
|
|
@@ -1756,9 +1651,12 @@ function acquireProjectLock(addDir, env = process.env) {
|
|
|
1756
1651
|
// moot — leave the (unrelated, currently live) file completely alone and let the
|
|
1757
1652
|
// single retry below observe reality fresh (EEXIST -> fail-fast, per M7 — this lock
|
|
1758
1653
|
// never polls a live holder).
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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)) {
|
|
1762
1660
|
try { fs.unlinkSync(lockPath); } catch (_e) {} // already gone — harmless
|
|
1763
1661
|
}
|
|
1764
1662
|
} finally {
|
|
@@ -1978,6 +1876,7 @@ if (require.main === module) {
|
|
|
1978
1876
|
}
|
|
1979
1877
|
|
|
1980
1878
|
module.exports = {
|
|
1879
|
+
stillStaleGeneration: stillStaleGeneration,
|
|
1981
1880
|
detectAgent: detectAgent,
|
|
1982
1881
|
detectAgentEnriched: detectAgentEnriched,
|
|
1983
1882
|
readinessLine: readinessLine,
|
|
@@ -1985,7 +1884,4 @@ module.exports = {
|
|
|
1985
1884
|
scopeOptions: scopeOptions,
|
|
1986
1885
|
writeIntentNote: writeIntentNote,
|
|
1987
1886
|
writeGeminiSettings: writeGeminiSettings,
|
|
1988
|
-
ruleFileMode: ruleFileMode,
|
|
1989
|
-
ensureClaudeReference: ensureClaudeReference,
|
|
1990
|
-
writeRuleFilePointer: writeRuleFilePointer,
|
|
1991
1887
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilotspace/add",
|
|
3
|
-
"version": "2.
|
|
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"
|
package/skill/add/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: add
|
|
|
3
3
|
description: >-
|
|
4
4
|
ADD (AI-Driven Development) — a minimal, state-tracked workflow: the AI writes
|
|
5
5
|
the code, the human owns direction and verification. Drives every feature through
|
|
6
|
-
one lean PLAN.md: Specify →
|
|
6
|
+
one lean PLAN.md: Specify → Plan → Tests & Scenarios → Build → Verify → Observe,
|
|
7
7
|
red/green TDD built in. Use whenever a repo has `.add/`, or the user says "add",
|
|
8
8
|
"start a task", "next phase", "specify this feature", "ADD method", "AI-driven
|
|
9
9
|
development", or wants spec/tests-first discipline over vague-prompt coding. Also
|
|
@@ -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.
|
|
18
|
+
version: "2.4.0"
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
# ADD — memory · judgment · conscience (the agent is the hands)
|
|
@@ -70,9 +70,9 @@ confirms. Unsharp intent? **Interview before you size** (`intake.md`). A milesto
|
|
|
70
70
|
Every task is three beats (seven steps, folded), three engine calls, ONE human decision:
|
|
71
71
|
|
|
72
72
|
1. **DIRECTION** — load the domain-fit persona (seed via add-worker persona-mode if none), then compose
|
|
73
|
-
the whole bundle in ONE silent draft — §1
|
|
74
|
-
§
|
|
75
|
-
§4 red suite
|
|
73
|
+
the whole bundle in ONE silent draft — §1·§3 + §5-scope, no per-section narration; §4 then runs red — in PLAN.md: §1 rules + ranked ⚠ flag (co-specify) ·
|
|
74
|
+
§3 PLAN (grounding → frozen contract shape → build-strategy + Scope + Target) ·
|
|
75
|
+
§4 TESTS & SCENARIOS — the red suite (cases live here; optional inline gherkin when a human needs it): one test per Must & per Reject (a Must/Reject encoded in no §4 test = §1 not understood — stop); minor behaviors are prose build-guidance, not gated; run red for the RIGHT reason; fill each `covers:` key. Then the ONE approval,
|
|
76
76
|
presented lowest-confidence-first: `add.py freeze` (a setup session's baseline `lock` IS this approval).
|
|
77
77
|
2. **BUILD** — code in `src/` until every red is green; change no test, no frozen contract; stay
|
|
78
78
|
inside the §3 Scope. A test OUTSIDE your suite failing? `add.py locate` names the owning node, the
|
|
@@ -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
|
|
package/skill/add/beyond.md
CHANGED
|
@@ -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.
|
package/skill/add/deltas.md
CHANGED
|
@@ -43,7 +43,7 @@ If a learning touches two, ask "which competency, once updated, would have PREVE
|
|
|
43
43
|
|
|
44
44
|
```
|
|
45
45
|
emit (OBSERVE) human review
|
|
46
|
-
open ───────────▶ folded (merged into
|
|
46
|
+
open ───────────▶ folded (merged into its `.add/specs/<dd>` spec; version bumps)
|
|
47
47
|
└──────────▶ rejected (deliberately NOT consolidated — trail kept)
|
|
48
48
|
```
|
|
49
49
|
|
package/skill/add/design.md
CHANGED
|
@@ -42,7 +42,7 @@ Record **before** review-domain: project **defaults** in DESIGN.md's `## Design
|
|
|
42
42
|
|
|
43
43
|
### 1 · review-domain
|
|
44
44
|
Start from the **domain**, not a blank canvas. Read the domain model — entities, flows, the
|
|
45
|
-
ubiquitous language in
|
|
45
|
+
ubiquitous language in `.add/specs/domain.md` / `GLOSSARY.md` — and derive **which screens** the feature needs
|
|
46
46
|
+ each screen's **regions**. Map each entity to a *presentational* component (owns no domain
|
|
47
47
|
decision). Output: the screen list + regions.
|
|
48
48
|
|
package/skill/add/intake.md
CHANGED
|
@@ -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
|
|
@@ -5,6 +5,28 @@ acceptance check — passes, without changing any test or the contract. This is
|
|
|
5
5
|
AI leads; §1–§4 removed all ambiguity. Write code into `.add/tasks/<slug>/src/` (a non-coding
|
|
6
6
|
task writes its artifact to the path its §5 Scope declares).
|
|
7
7
|
|
|
8
|
+
## The reasoning discipline — build's slice of the arc (definitions: `phases/direction.md`)
|
|
9
|
+
|
|
10
|
+
**Fluent ≠ true** holds in code: a diff feels done in proportion to how much you wrote, not how
|
|
11
|
+
much you verified — a green you haven't refuted is a claim, not evidence. Run the arc per batch:
|
|
12
|
+
|
|
13
|
+
- **FRAME** — restate the batch's tests and the frozen §3 clause they cover before writing a line.
|
|
14
|
+
- **GROUND** — open every anchor before editing it; a recalled file/symbol/flag is `[PRIOR]`
|
|
15
|
+
until re-confirmed against the live tree THIS session; a live read outranks memory.
|
|
16
|
+
- **REASON** — name the mechanism ("because…"), simulate one concrete input through the change
|
|
17
|
+
before committing it; when a red survives a fix, hold a second hypothesis instead of patching harder.
|
|
18
|
+
- **ATTACK** — before presenting green: re-run the suite fresh, then reviewer-read your own diff
|
|
19
|
+
hunting the cheap falsifying input (file · line · values). An earned green is one you tried to
|
|
20
|
+
kill. Security findings stay HARD-STOP.
|
|
21
|
+
- **DELIVER** — the green report leads with the outcome and tags its claims: a pass you ran THIS
|
|
22
|
+
session is `[OBSERVED]`; a remembered pass is `[PRIOR]` — never present it bare.
|
|
23
|
+
- **Constraint loop on the §5 Scope allowlist** — don't eyeball it: list the files actually
|
|
24
|
+
touched (`git status` / diff) and check each against the declared Scope tokens mechanically
|
|
25
|
+
before the gate; a miss is `scope_violation` — caught by you, not by the engine.
|
|
26
|
+
- **Follow-through (the Floor's second check)** — green tests ≠ the goal reached; simulate the
|
|
27
|
+
human's end state once — run the artifact under the BARE declared runtime — before calling
|
|
28
|
+
the batch done.
|
|
29
|
+
|
|
8
30
|
## Work in small batches
|
|
9
31
|
|
|
10
32
|
Pick ONE task-sized slice, restate its tests, implement, iterate to green —
|