deepflow 0.1.107 → 0.1.109
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/bin/install.js +25 -7
- package/bin/install.test.js +113 -0
- package/bin/plan-consolidator.js +19 -1
- package/bin/plan-consolidator.test.js +150 -0
- package/bin/ratchet.js +11 -6
- package/bin/ratchet.test.js +172 -0
- package/bin/worktree-deps.js +127 -0
- package/hooks/ac-coverage.js +213 -0
- package/hooks/df-explore-protocol.js +227 -28
- package/hooks/df-explore-protocol.test.js +460 -81
- package/hooks/df-spec-lint.js +13 -2
- package/hooks/df-spec-lint.test.js +133 -0
- package/package.json +4 -1
- package/src/commands/df/execute.md +112 -2
- package/src/commands/df/plan.md +244 -16
- package/src/commands/df/verify.md +46 -8
- package/templates/config-template.yaml +1 -0
- package/templates/explore-protocol.md.bak +69 -0
- package/templates/plan-template.md +11 -0
- package/templates/spec-template.md +15 -0
package/hooks/df-spec-lint.js
CHANGED
|
@@ -123,12 +123,23 @@ function computeLayer(content) {
|
|
|
123
123
|
* @param {string} content - The raw markdown content of the spec file.
|
|
124
124
|
* @param {object} opts
|
|
125
125
|
* @param {'interactive'|'auto'} opts.mode
|
|
126
|
+
* @param {string|null} opts.filename - Optional filename (basename) used for stem validation.
|
|
126
127
|
* @returns {{ hard: string[], advisory: string[] }}
|
|
127
128
|
*/
|
|
128
|
-
function validateSpec(content, { mode = 'interactive', specsDir = null } = {}) {
|
|
129
|
+
function validateSpec(content, { mode = 'interactive', specsDir = null, filename = null } = {}) {
|
|
129
130
|
const hard = [];
|
|
130
131
|
const advisory = [];
|
|
131
132
|
|
|
133
|
+
// ── Spec filename stem validation ────────────────────────────────────
|
|
134
|
+
if (filename !== null) {
|
|
135
|
+
let stem = path.basename(filename, '.md');
|
|
136
|
+
stem = stem.replace(/^(doing-|done-)/, '');
|
|
137
|
+
const SAFE_STEM = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
138
|
+
if (!SAFE_STEM.test(stem)) {
|
|
139
|
+
hard.push(`Spec filename stem contains unsafe characters: "${stem}"`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
// ── Frontmatter: parse and validate derives-from ─────────────────────
|
|
133
144
|
const { frontmatter } = parseFrontmatter(content);
|
|
134
145
|
if (frontmatter['derives-from'] !== undefined) {
|
|
@@ -339,7 +350,7 @@ if (require.main === module) {
|
|
|
339
350
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
340
351
|
const mode = process.argv.includes('--auto') ? 'auto' : 'interactive';
|
|
341
352
|
const specsDir = path.resolve(path.dirname(filePath));
|
|
342
|
-
const result = validateSpec(content, { mode, specsDir });
|
|
353
|
+
const result = validateSpec(content, { mode, specsDir, filename: path.basename(filePath) });
|
|
343
354
|
|
|
344
355
|
if (result.hard.length > 0) {
|
|
345
356
|
console.error('HARD invariant failures:');
|
|
@@ -410,3 +410,136 @@ describe('derives-from validation', () => {
|
|
|
410
410
|
assert.deepEqual(resultWith.hard, resultWithout.hard);
|
|
411
411
|
});
|
|
412
412
|
});
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// validateSpec — spec filename stem validation
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
describe('validateSpec stem validation', () => {
|
|
419
|
+
test('valid plain name passes', () => {
|
|
420
|
+
const result = validateSpec(fullSpec(), { filename: 'my-spec.md' });
|
|
421
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
422
|
+
assert.equal(stemErrors.length, 0);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('valid name with numbers passes', () => {
|
|
426
|
+
const result = validateSpec(fullSpec(), { filename: 'spec-v2-fix.md' });
|
|
427
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
428
|
+
assert.equal(stemErrors.length, 0);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('single character name passes', () => {
|
|
432
|
+
const result = validateSpec(fullSpec(), { filename: 'a.md' });
|
|
433
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
434
|
+
assert.equal(stemErrors.length, 0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('doing- prefix is stripped before validation', () => {
|
|
438
|
+
const result = validateSpec(fullSpec(), { filename: 'doing-my-spec.md' });
|
|
439
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
440
|
+
assert.equal(stemErrors.length, 0);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('done- prefix is stripped before validation', () => {
|
|
444
|
+
const result = validateSpec(fullSpec(), { filename: 'done-my-spec.md' });
|
|
445
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
446
|
+
assert.equal(stemErrors.length, 0);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('filename with dollar sign is rejected as hard failure', () => {
|
|
450
|
+
const result = validateSpec(fullSpec(), { filename: 'spec-$bad.md' });
|
|
451
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
452
|
+
assert.equal(stemErrors.length, 1);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('filename with backtick is rejected as hard failure', () => {
|
|
456
|
+
const result = validateSpec(fullSpec(), { filename: 'spec-`bad.md' });
|
|
457
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
458
|
+
assert.equal(stemErrors.length, 1);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('filename with pipe character is rejected as hard failure', () => {
|
|
462
|
+
const result = validateSpec(fullSpec(), { filename: 'spec|bad.md' });
|
|
463
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
464
|
+
assert.equal(stemErrors.length, 1);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('filename with semicolon is rejected as hard failure', () => {
|
|
468
|
+
const result = validateSpec(fullSpec(), { filename: 'spec;bad.md' });
|
|
469
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
470
|
+
assert.equal(stemErrors.length, 1);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('filename with ampersand is rejected as hard failure', () => {
|
|
474
|
+
const result = validateSpec(fullSpec(), { filename: 'spec&bad.md' });
|
|
475
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
476
|
+
assert.equal(stemErrors.length, 1);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test('filename with space is rejected as hard failure', () => {
|
|
480
|
+
const result = validateSpec(fullSpec(), { filename: 'spec bad.md' });
|
|
481
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
482
|
+
assert.equal(stemErrors.length, 1);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test('filename with path traversal (..) is rejected as hard failure', () => {
|
|
486
|
+
const result = validateSpec(fullSpec(), { filename: '..evil.md' });
|
|
487
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
488
|
+
assert.equal(stemErrors.length, 1);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test('filename with leading hyphen is rejected as hard failure', () => {
|
|
492
|
+
const result = validateSpec(fullSpec(), { filename: '-leading.md' });
|
|
493
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
494
|
+
assert.equal(stemErrors.length, 1);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('filename with trailing hyphen is rejected as hard failure', () => {
|
|
498
|
+
const result = validateSpec(fullSpec(), { filename: 'trailing-.md' });
|
|
499
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
500
|
+
assert.equal(stemErrors.length, 1);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('empty stem (only prefix) is rejected as hard failure', () => {
|
|
504
|
+
// A filename of just "doing-.md" strips to empty string
|
|
505
|
+
const result = validateSpec(fullSpec(), { filename: 'doing-.md' });
|
|
506
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
507
|
+
assert.equal(stemErrors.length, 1);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test('empty filename stem (.md only) is rejected as hard failure', () => {
|
|
511
|
+
const result = validateSpec(fullSpec(), { filename: '.md' });
|
|
512
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
513
|
+
assert.equal(stemErrors.length, 1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('stem validation failure is in hard array, not advisory', () => {
|
|
517
|
+
const result = validateSpec(fullSpec(), { filename: 'spec$bad.md' });
|
|
518
|
+
const hardErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
519
|
+
const advisoryErrors = result.advisory.filter((m) => m.includes('unsafe characters'));
|
|
520
|
+
assert.equal(hardErrors.length, 1);
|
|
521
|
+
assert.equal(advisoryErrors.length, 0);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('no filename passed (null) skips stem validation', () => {
|
|
525
|
+
// No filename option — stem check should not run
|
|
526
|
+
const result = validateSpec(fullSpec());
|
|
527
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
528
|
+
assert.equal(stemErrors.length, 0);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test('all existing repo spec names pass validation', () => {
|
|
532
|
+
const existingNames = [
|
|
533
|
+
'done-dashboard-model-cost-fixes.md',
|
|
534
|
+
'done-orchestrator-v2.md',
|
|
535
|
+
'done-plan-cleanup.md',
|
|
536
|
+
'done-plan-fanout.md',
|
|
537
|
+
'done-quality-gates.md',
|
|
538
|
+
];
|
|
539
|
+
for (const filename of existingNames) {
|
|
540
|
+
const result = validateSpec(fullSpec(), { filename });
|
|
541
|
+
const stemErrors = result.hard.filter((m) => m.includes('unsafe characters'));
|
|
542
|
+
assert.equal(stemErrors.length, 0, `Expected ${filename} to pass but got stem errors`);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.109",
|
|
4
4
|
"description": "Doing reveals what thinking can't predict — spec-driven iterative development for Claude Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"playwright": "^1.58.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"typescript": "^6.0.2"
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -44,6 +44,14 @@ Shell: `` !`cat .deepflow/checkpoint.json 2>/dev/null || echo 'NOT_FOUND'` `` /
|
|
|
44
44
|
|
|
45
45
|
Require clean HEAD. Derive SPEC_NAME from `specs/doing-*.md`. Create `.deepflow/worktrees/{spec}` on branch `df/{spec}`. Reuse if exists; `--fresh` deletes first. If `worktree.sparse_paths` non-empty: `git worktree add --no-checkout`, `sparse-checkout set {paths}`, checkout.
|
|
46
46
|
|
|
47
|
+
### 1.5.1. SYMLINK DEPENDENCIES
|
|
48
|
+
|
|
49
|
+
After worktree creation, symlink `node_modules` from the main repo so TypeScript/LSP/build can resolve dependencies without a full install:
|
|
50
|
+
```bash
|
|
51
|
+
node "${HOME}/.claude/bin/worktree-deps.js" --source "$(git rev-parse --show-toplevel)" --worktree "${WORKTREE_PATH}"
|
|
52
|
+
```
|
|
53
|
+
The script finds `node_modules` at root and inside monorepo directories (`packages/`, `apps/`, etc.) and creates symlinks in the worktree. Outputs JSON: `{"linked": N, "total": M}`. Errors are non-fatal — log and continue.
|
|
54
|
+
|
|
47
55
|
### 1.6. RATCHET SNAPSHOT
|
|
48
56
|
|
|
49
57
|
Snapshot pre-existing test files — only these count for ratchet (agent-created excluded):
|
|
@@ -159,7 +167,7 @@ The script handles all health checks internally and outputs structured JSON:
|
|
|
159
167
|
**Broken-tests policy:** Updating pre-existing tests requires a separate dedicated task in PLAN.md with explicit justification — never inline during execution.
|
|
160
168
|
|
|
161
169
|
**Orchestrator response by exit code:**
|
|
162
|
-
- **Exit 0 (PASS):** Commit stands. TaskUpdate(status: "completed"), update PLAN.md [x] + commit hash.
|
|
170
|
+
- **Exit 0 (PASS):** Commit stands. **AC coverage check** (see §5.5.1). TaskUpdate(status: "completed"), update PLAN.md [x] + commit hash. **Extract decisions** (see §5.5.2).
|
|
163
171
|
- **Exit 1 (FAIL):** Script already reverted. Set `TaskUpdate(status: "pending")`. Recompute remaining waves:
|
|
164
172
|
```
|
|
165
173
|
WAVE_JSON=!`node "${HOME}/.claude/bin/wave-runner.js" --json --plan PLAN.md --recalc --failed T{N} 2>/dev/null || echo 'WAVE_ERROR'`
|
|
@@ -168,6 +176,32 @@ The script handles all health checks internally and outputs structured JSON:
|
|
|
168
176
|
Report: `"✗ T{n}: reverted"`.
|
|
169
177
|
- **Exit 2 (SALVAGEABLE):** Spawn `Agent(model="sonnet")` to fix lint/typecheck issues. Re-run `node "${HOME}/.claude/bin/ratchet.js"`. If still non-zero → revert both commits, set status pending.
|
|
170
178
|
|
|
179
|
+
#### 5.5.1. AC COVERAGE CHECK (after ratchet pass)
|
|
180
|
+
|
|
181
|
+
After ratchet PASS (exit 0), run AC coverage check to verify agent reported all acceptance criteria:
|
|
182
|
+
```bash
|
|
183
|
+
node "${HOME}/.claude/bin/hooks/ac-coverage.js" --spec {spec_path} --output-file {agent_output_file} --status pass
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
where `{spec_path}` is the path to `specs/doing-{spec_name}.md` and `{agent_output_file}` is the task agent's full output transcript (from TaskOutput or notification context).
|
|
187
|
+
|
|
188
|
+
**Exit codes from ac-coverage.js:**
|
|
189
|
+
- **Exit 0:** All ACs covered or no ACs in spec. Status remains PASS. Proceed to decision extraction (§5.5.2).
|
|
190
|
+
- **Exit 2 (SALVAGEABLE):** Missed ACs detected despite agent reporting TASK_STATUS:pass. Script outputs summary: `[ac-coverage] N/M ACs covered — missed: AC-X, AC-Y; ...`. Override final status to SALVAGEABLE. Commit stands. TaskUpdate(status: "completed") with note that ACs are incomplete.
|
|
191
|
+
- **Exit 1 (script error):** Log error, do not change status. Proceed as if ratchet PASS (exit 0 from ac-coverage).
|
|
192
|
+
|
|
193
|
+
#### 5.5.2. DECISION EXTRACTION (on ratchet pass)
|
|
194
|
+
|
|
195
|
+
Parse the agent's response for `DECISIONS:` line. If present:
|
|
196
|
+
1. Split by ` | ` to get individual decisions
|
|
197
|
+
2. Each decision has format `[TAG] description — rationale` where TAG ∈ {APPROACH, PROVISIONAL, ASSUMPTION, FUTURE, UPDATE}
|
|
198
|
+
3. Append to `.deepflow/decisions.md` under `### {date} — {spec_name}` header (create header if first decision for this spec today, reuse if exists)
|
|
199
|
+
4. Format: `- [TAG] description — rationale`
|
|
200
|
+
|
|
201
|
+
If no `DECISIONS:` line in agent output → skip silently (mechanical tasks don't produce decisions).
|
|
202
|
+
|
|
203
|
+
**This runs on every ratchet pass, not just at verify time.** Decisions are captured incrementally as tasks complete, so they're never lost even if verify fails or merge is manual.
|
|
204
|
+
|
|
171
205
|
**Edit scope validation:** `git diff HEAD~1 --name-only` vs allowed globs. Violation → revert, report.
|
|
172
206
|
**Impact completeness:** diff vs Impact callers/duplicates. Gap → advisory warning (no revert).
|
|
173
207
|
|
|
@@ -304,6 +338,25 @@ TASK_DETAIL=!`cat .deepflow/plans/doing-{task_id}.md 2>/dev/null || echo 'NOT_FO
|
|
|
304
338
|
```
|
|
305
339
|
If `TASK_DETAIL` is not `NOT_FOUND`, use it as the full Middle section (Steps, ACs, Impact) in the agent prompt, overriding the inline PLAN.md block. If `NOT_FOUND`, fall back to the inline PLAN.md task block.
|
|
306
340
|
|
|
341
|
+
**Pre-prompt type context extraction (before building agent prompt):**
|
|
342
|
+
|
|
343
|
+
Run LSP `documentSymbol` on the task's `files` list to collect existing type definitions. This runs BEFORE prompt construction so the result can be injected as `EXISTING_TYPES`.
|
|
344
|
+
|
|
345
|
+
<!-- AC-7: No new tool calls or latency added when context sources are empty -->
|
|
346
|
+
**Early exit (AC-7):** If the task's `Files:` list is empty, skip all `documentSymbol` calls entirely. Set `EXISTING_TYPES` to empty string immediately and proceed to prompt construction.
|
|
347
|
+
|
|
348
|
+
Steps (only when `Files:` list is non-empty):
|
|
349
|
+
1. Cap the file list at 10 files (take the first 10 from the task's `Files:` list).
|
|
350
|
+
2. For each file (up to the cap), call `documentSymbol` via LSP.
|
|
351
|
+
3. Filter results: keep only symbols with kind ∈ {Class, Interface, Enum, TypeAlias} (LSP SymbolKind values 5, 11, 10, 26 respectively).
|
|
352
|
+
4. For each matching symbol, extract the source range (`range.start.line` to `range.end.line`) — read those lines from the file.
|
|
353
|
+
5. Accumulate extracted lines with a **120-line total budget** — stop adding symbols once the budget is reached.
|
|
354
|
+
6. Join all extracted ranges into a single string: `EXISTING_TYPES`.
|
|
355
|
+
|
|
356
|
+
**AC-8 — graceful no-op:** If no matching symbols are found across all processed files (either `documentSymbol` returns nothing or no Class/Interface/Enum/TypeAlias symbols exist), set `EXISTING_TYPES` to empty string. No context block is added to the prompt.
|
|
357
|
+
|
|
358
|
+
<!-- AC-6: Backward-compatible no-op — when neither Domain Model section exists in the spec nor Existing Types extraction yields content (EXISTING_TYPES is empty string), the Standard Task prompt contains no extra context blocks and is identical to the pre-injection baseline. Zero prompt overhead, zero tool calls for tasks that lack these context sources. -->
|
|
359
|
+
|
|
307
360
|
**Standard Task** (`Agent(model="{Model}", ...)`):
|
|
308
361
|
```
|
|
309
362
|
--- START ---
|
|
@@ -317,6 +370,16 @@ spike_results:
|
|
|
317
370
|
insight: {insight from probe_learnings}
|
|
318
371
|
}
|
|
319
372
|
Success criteria: {ACs from spec relevant to this task}
|
|
373
|
+
{If spec contains ## Domain Model section:
|
|
374
|
+
--- CONTEXT: Domain Model ---
|
|
375
|
+
{Domain Model section content from doing-*.md, extracted via shell injection:
|
|
376
|
+
DOMAIN_MODEL=!`sed -n '/^## Domain Model$/,/^## [^D]/p' specs/doing-{spec_name}.md | head -n -1 2>/dev/null || echo 'NOT_FOUND'`
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
{If EXISTING_TYPES is non-empty:
|
|
380
|
+
--- CONTEXT: Existing Types ---
|
|
381
|
+
{EXISTING_TYPES}
|
|
382
|
+
}
|
|
320
383
|
--- MIDDLE (omit for low effort; omit deps for medium) ---
|
|
321
384
|
{TASK_DETAIL if available, else inline block:}
|
|
322
385
|
Impact: Callers: {file} ({why}) | Duplicates: [active→consolidate] [dead→DELETE] | Data flow: {consumers}
|
|
@@ -324,12 +387,58 @@ Prior tasks: {dep_id}: {summary}
|
|
|
324
387
|
Steps: 1. chub search/get for APIs 2. LSP findReferences, add unlisted callers 3. LSP documentSymbol on Impact files → Read with offset/limit on relevant ranges only (never read full files) 4. Implement 5. Commit
|
|
325
388
|
--- END ---
|
|
326
389
|
Duplicates: [active]→consolidate [dead]→DELETE. ONLY job: code+commit. No merge/rename/checkout.
|
|
390
|
+
**Acceptance Criteria Coverage:** If the spec has acceptance criteria (AC-N), emit this block:
|
|
391
|
+
```
|
|
392
|
+
AC_COVERAGE:
|
|
393
|
+
AC-1:done
|
|
394
|
+
AC-2:skip:reason here (if applicable)
|
|
395
|
+
AC_COVERAGE_END
|
|
396
|
+
```
|
|
397
|
+
Format: one line per AC with either `AC-N:done` or `AC-N:skip:reason`. Omit this block if the spec has no acceptance criteria.
|
|
398
|
+
DECISIONS: If you made non-obvious choices, append to the LAST LINE BEFORE TASK_STATUS:
|
|
399
|
+
DECISIONS: [TAG] {decision} — {rationale} | [TAG] {decision2} — {rationale2}
|
|
400
|
+
Tags:
|
|
401
|
+
[APPROACH] — chose X over Y (architectural/design choice)
|
|
402
|
+
[PROVISIONAL] — works for now but won't scale / needs revisit
|
|
403
|
+
[ASSUMPTION] — assumed X is true; if wrong, Y breaks
|
|
404
|
+
[FUTURE] — deferred X because Y; revisit when Z
|
|
405
|
+
[UPDATE] — changed prior decision from X to Y because Z
|
|
406
|
+
Skip for trivial/mechanical changes.
|
|
327
407
|
Last line of your response MUST be: TASK_STATUS:pass (if successful) or TASK_STATUS:fail (if failed) or TASK_STATUS:revert (if reverted)
|
|
328
408
|
```
|
|
329
409
|
|
|
410
|
+
**Integration Task** (`Agent(model="opus")`):
|
|
411
|
+
```
|
|
412
|
+
--- START ---
|
|
413
|
+
{task_id} [INTEGRATION]: Verify contracts between {spec_a} ↔ {spec_b}
|
|
414
|
+
Integration ACs: {list from PLAN.md}
|
|
415
|
+
--- MIDDLE ---
|
|
416
|
+
Specs involved: {spec file paths}
|
|
417
|
+
Interface Map: {from integration task detail}
|
|
418
|
+
Contract Risks: {from integration task detail}
|
|
419
|
+
--- END ---
|
|
420
|
+
RULES:
|
|
421
|
+
- Fix the CONSUMER to match the PRODUCER's declared interface. Never weaken the producer.
|
|
422
|
+
- Each fix must reference the specific contract being repaired.
|
|
423
|
+
- If a migration conflict exists, make ALL migrations idempotent (IF NOT EXISTS, IF NOT COLUMN, etc.)
|
|
424
|
+
- Do NOT create new variables or intermediate adapters to paper over mismatches. Fix the actual call site.
|
|
425
|
+
- Do NOT modify acceptance criteria or spec definitions.
|
|
426
|
+
- Commit as fix({spec}): {contract description}. One commit per contract fix.
|
|
427
|
+
**Acceptance Criteria Coverage:** If the spec has acceptance criteria (AC-N), emit this block:
|
|
428
|
+
```
|
|
429
|
+
AC_COVERAGE:
|
|
430
|
+
AC-1:done
|
|
431
|
+
AC-2:skip:reason here (if applicable)
|
|
432
|
+
AC_COVERAGE_END
|
|
433
|
+
```
|
|
434
|
+
Format: one line per AC with either `AC-N:done` or `AC-N:skip:reason`. Omit this block if the spec has no acceptance criteria.
|
|
435
|
+
DECISIONS: Report each contract fix as: [TAG] {what was mismatched} — {which side changed and why}. Use [APPROACH] for definitive fixes, [PROVISIONAL] if the fix is a workaround, [UPDATE] if changing a prior decision.
|
|
436
|
+
Last line: TASK_STATUS:pass or TASK_STATUS:fail
|
|
437
|
+
```
|
|
438
|
+
|
|
330
439
|
**Bootstrap:** `BOOTSTRAP: Write tests for edit_scope files. Do NOT change implementation. Commit as test({spec}): bootstrap. Last line: TASK_STATUS:pass or TASK_STATUS:fail`
|
|
331
440
|
|
|
332
|
-
**Spike:** `{task_id} [SPIKE]: {hypothesis}. Files+Spec. {reverted warnings}. Minimal spike. Commit as spike({spec}): {desc}. Last line: TASK_STATUS:pass or TASK_STATUS:fail`
|
|
441
|
+
**Spike:** `{task_id} [SPIKE]: {hypothesis}. Files+Spec. {reverted warnings}. Minimal spike. Commit as spike({spec}): {desc}. If you discovered constraints, rejected approaches, or made assumptions, report: DECISIONS: [TAG] {finding} — {why it matters} (use PROVISIONAL for "works but needs revisit", ASSUMPTION for "assumed X; if wrong Y breaks", APPROACH for definitive choices). Last line: TASK_STATUS:pass or TASK_STATUS:fail`
|
|
333
442
|
|
|
334
443
|
**Optimize Task** (`Agent(model="opus")`):
|
|
335
444
|
```
|
|
@@ -399,6 +508,7 @@ Reverted task: `TaskUpdate(status: "pending")`, dependents stay blocked. Repeate
|
|
|
399
508
|
|
|
400
509
|
| Rule | Detail |
|
|
401
510
|
|------|--------|
|
|
511
|
+
| Integration tasks run last | [INTEGRATION] tasks execute after all blocked-by tasks complete. Fix tasks from integration failures are prescriptive (name the contract, producer, consumer, and which side to change). Never weaken the producer's declared interface — prefer fixing the consumer. |
|
|
402
512
|
| Zero tests → bootstrap first | Sole task when snapshot empty |
|
|
403
513
|
| 1 task = 1 agent = 1 commit | `atomic-commits` skill |
|
|
404
514
|
| 1 file = 1 writer | Sequential on conflict |
|