deepflow 0.1.100 → 0.1.102
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 +16 -1
- package/bin/plan-consolidator.js +16 -16
- package/bin/wave-runner.js +48 -16
- package/package.json +1 -1
- package/src/commands/df/execute.md +9 -9
- package/src/commands/df/plan.md +29 -19
package/bin/install.js
CHANGED
|
@@ -134,6 +134,17 @@ async function main() {
|
|
|
134
134
|
);
|
|
135
135
|
log('Agents installed');
|
|
136
136
|
|
|
137
|
+
// Copy bin utilities (plan-consolidator, wave-runner, ratchet)
|
|
138
|
+
const binDest = path.join(CLAUDE_DIR, 'bin');
|
|
139
|
+
fs.mkdirSync(binDest, { recursive: true });
|
|
140
|
+
for (const script of ['plan-consolidator.js', 'wave-runner.js', 'ratchet.js']) {
|
|
141
|
+
const src = path.join(PACKAGE_DIR, 'bin', script);
|
|
142
|
+
if (fs.existsSync(src)) {
|
|
143
|
+
fs.copyFileSync(src, path.join(binDest, script));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
log('Bin utilities installed');
|
|
147
|
+
|
|
137
148
|
// Copy hooks (global only - statusline requires global settings)
|
|
138
149
|
if (level === 'global') {
|
|
139
150
|
const hooksDir = path.join(PACKAGE_DIR, 'hooks');
|
|
@@ -186,6 +197,7 @@ async function main() {
|
|
|
186
197
|
console.log(' commands/df/ — /df:discover, /df:debate, /df:spec, /df:plan, /df:execute, /df:verify, /df:auto, /df:update');
|
|
187
198
|
console.log(' skills/ — gap-discovery, atomic-commits, code-completeness, browse-fetch, browse-verify, auto-cycle');
|
|
188
199
|
console.log(' agents/ — reasoner (/df:auto — autonomous execution via /loop)');
|
|
200
|
+
console.log(' bin/ — plan-consolidator, wave-runner, ratchet');
|
|
189
201
|
if (level === 'global') {
|
|
190
202
|
console.log(' hooks/ — statusline, update checker, invariant checker, worktree guard');
|
|
191
203
|
}
|
|
@@ -592,7 +604,10 @@ async function uninstall() {
|
|
|
592
604
|
'skills/gap-discovery',
|
|
593
605
|
'skills/browse-fetch',
|
|
594
606
|
'skills/browse-verify',
|
|
595
|
-
'agents/reasoner.md'
|
|
607
|
+
'agents/reasoner.md',
|
|
608
|
+
'bin/plan-consolidator.js',
|
|
609
|
+
'bin/wave-runner.js',
|
|
610
|
+
'bin/ratchet.js'
|
|
596
611
|
];
|
|
597
612
|
|
|
598
613
|
if (level === 'global') {
|
package/bin/plan-consolidator.js
CHANGED
|
@@ -208,7 +208,8 @@ function consolidate(specEntries, fileConflicts) {
|
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
210
|
* Render consolidated tasks as PLAN.md-compatible markdown.
|
|
211
|
-
* Groups tasks under ### {specName} headings.
|
|
211
|
+
* Groups tasks under ### doing-{specName} headings with a details reference line.
|
|
212
|
+
* One line per task — no sub-bullets. Files omitted (live in mini-plans only).
|
|
212
213
|
* Compatible with wave-runner's parsePlan regex (see wave-runner.js parsePlan).
|
|
213
214
|
*/
|
|
214
215
|
function formatConsolidated(consolidated) {
|
|
@@ -221,11 +222,15 @@ function formatConsolidated(consolidated) {
|
|
|
221
222
|
|
|
222
223
|
for (const task of consolidated) {
|
|
223
224
|
if (task.specName !== lastSpec) {
|
|
224
|
-
|
|
225
|
+
// Close previous spec with trailing blank line (already added after last task)
|
|
226
|
+
const doingName = `doing-${task.specName}`;
|
|
227
|
+
const planPath = `.deepflow/plans/${doingName}.md`;
|
|
228
|
+
lines.push(`### ${doingName}\n`);
|
|
229
|
+
lines.push(`> Details: [\`${planPath}\`](${planPath})\n`);
|
|
225
230
|
lastSpec = task.specName;
|
|
226
231
|
}
|
|
227
232
|
|
|
228
|
-
// Task header line
|
|
233
|
+
// Task header line — one line, no sub-bullets
|
|
229
234
|
const tagPart = task.tags ? ` ${task.tags}` : '';
|
|
230
235
|
// Append conflict annotations to description if any
|
|
231
236
|
const conflictPart = task.conflictAnnotations.length > 0
|
|
@@ -233,23 +238,18 @@ function formatConsolidated(consolidated) {
|
|
|
233
238
|
: '';
|
|
234
239
|
const descPart = (task.description + conflictPart).trim();
|
|
235
240
|
const headerDesc = descPart ? `: ${descPart}` : '';
|
|
236
|
-
lines.push(`- [ ] **${task.globalId}**${tagPart}${headerDesc}`);
|
|
237
241
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// Blocked by annotation
|
|
244
|
-
if (task.blockedBy.length > 0) {
|
|
245
|
-
lines.push(` - Blocked by: ${task.blockedBy.join(', ')}`);
|
|
246
|
-
} else {
|
|
247
|
-
lines.push(' - Blocked by: none');
|
|
248
|
-
}
|
|
242
|
+
// Blocked by suffix — omit entirely when empty
|
|
243
|
+
const blockedSuffix = task.blockedBy.length > 0
|
|
244
|
+
? ` | Blocked by: ${task.blockedBy.join(', ')}`
|
|
245
|
+
: '';
|
|
249
246
|
|
|
250
|
-
lines.push(
|
|
247
|
+
lines.push(`- [ ] **${task.globalId}**${tagPart}${headerDesc}${blockedSuffix}`);
|
|
251
248
|
}
|
|
252
249
|
|
|
250
|
+
// Trailing newline after last task
|
|
251
|
+
lines.push('');
|
|
252
|
+
|
|
253
253
|
return lines.join('\n');
|
|
254
254
|
}
|
|
255
255
|
|
package/bin/wave-runner.js
CHANGED
|
@@ -85,14 +85,38 @@ function parsePlan(text) {
|
|
|
85
85
|
// Match pending task header: - [ ] **T{N}**...
|
|
86
86
|
const taskMatch = line.match(/^\s*-\s+\[\s+\]\s+\*\*T(\d+)\*\*(?:\s+\[[^\]]*\])?[:\s]*(.*)/);
|
|
87
87
|
if (taskMatch) {
|
|
88
|
+
const rest = taskMatch[2].trim();
|
|
89
|
+
|
|
90
|
+
// Extract inline blocked-by (from " | Blocked by: T1, T2")
|
|
91
|
+
let inlineBlockedBy = [];
|
|
92
|
+
let descPart = rest;
|
|
93
|
+
const blockedInlineMatch = rest.match(/\s*\|\s*Blocked\s+by:\s+(.+)$/i);
|
|
94
|
+
if (blockedInlineMatch) {
|
|
95
|
+
descPart = rest.substring(0, rest.length - blockedInlineMatch[0].length).trim();
|
|
96
|
+
inlineBlockedBy = blockedInlineMatch[1]
|
|
97
|
+
.split(/[,\s]+/)
|
|
98
|
+
.map(s => s.trim())
|
|
99
|
+
.filter(s => /^T\d+$/.test(s));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Extract inline model/effort (from " — model/effort")
|
|
103
|
+
let inlineModel = null;
|
|
104
|
+
let inlineEffort = null;
|
|
105
|
+
const modelInlineMatch = descPart.match(/\s*\u2014\s*(haiku|sonnet|opus)\/(low|medium|high)\s*$/i);
|
|
106
|
+
if (modelInlineMatch) {
|
|
107
|
+
descPart = descPart.substring(0, descPart.length - modelInlineMatch[0].length).trim();
|
|
108
|
+
inlineModel = modelInlineMatch[1].toLowerCase();
|
|
109
|
+
inlineEffort = modelInlineMatch[2].toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
current = {
|
|
89
113
|
id: `T${taskMatch[1]}`,
|
|
90
114
|
num: parseInt(taskMatch[1], 10),
|
|
91
|
-
description:
|
|
92
|
-
blockedBy:
|
|
93
|
-
model:
|
|
115
|
+
description: descPart,
|
|
116
|
+
blockedBy: inlineBlockedBy,
|
|
117
|
+
model: inlineModel,
|
|
94
118
|
files: null,
|
|
95
|
-
effort:
|
|
119
|
+
effort: inlineEffort,
|
|
96
120
|
spec: currentSpec,
|
|
97
121
|
};
|
|
98
122
|
tasks.push(current);
|
|
@@ -107,35 +131,43 @@ function parsePlan(text) {
|
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
if (current) {
|
|
110
|
-
// Match "Blocked by:" annotation
|
|
134
|
+
// Match "Blocked by:" annotation — only apply if inline parsing found no deps
|
|
111
135
|
const blockedMatch = line.match(/^\s+-\s+Blocked\s+by:\s+(.+)/i);
|
|
112
136
|
if (blockedMatch) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
if (current.blockedBy.length === 0) {
|
|
138
|
+
const deps = blockedMatch[1]
|
|
139
|
+
.split(/[,\s]+/)
|
|
140
|
+
.map(s => s.trim())
|
|
141
|
+
.filter(s => /^T\d+$/.test(s));
|
|
142
|
+
current.blockedBy.push(...deps);
|
|
143
|
+
}
|
|
118
144
|
continue;
|
|
119
145
|
}
|
|
120
146
|
|
|
121
|
-
// Match "Model:" annotation
|
|
147
|
+
// Match "Model:" annotation — only apply if inline parsing found no model
|
|
122
148
|
const modelMatch = line.match(/^\s+-\s+Model:\s+(.+)/i);
|
|
123
149
|
if (modelMatch) {
|
|
124
|
-
current.model
|
|
150
|
+
if (current.model === null) {
|
|
151
|
+
current.model = modelMatch[1].trim();
|
|
152
|
+
}
|
|
125
153
|
continue;
|
|
126
154
|
}
|
|
127
155
|
|
|
128
|
-
// Match "Files:" annotation
|
|
156
|
+
// Match "Files:" annotation — always apply (no inline equivalent)
|
|
129
157
|
const filesMatch = line.match(/^\s+-\s+Files:\s+(.+)/i);
|
|
130
158
|
if (filesMatch) {
|
|
131
|
-
current.files
|
|
159
|
+
if (current.files === null) {
|
|
160
|
+
current.files = filesMatch[1].trim();
|
|
161
|
+
}
|
|
132
162
|
continue;
|
|
133
163
|
}
|
|
134
164
|
|
|
135
|
-
// Match "Effort:" annotation
|
|
165
|
+
// Match "Effort:" annotation — only apply if inline parsing found no effort
|
|
136
166
|
const effortMatch = line.match(/^\s+-\s+Effort:\s+(.+)/i);
|
|
137
167
|
if (effortMatch) {
|
|
138
|
-
current.effort
|
|
168
|
+
if (current.effort === null) {
|
|
169
|
+
current.effort = effortMatch[1].trim();
|
|
170
|
+
}
|
|
139
171
|
continue;
|
|
140
172
|
}
|
|
141
173
|
}
|
package/package.json
CHANGED
|
@@ -83,7 +83,7 @@ Load PLAN.md (required), specs/doing-*.md, .deepflow/config.yaml. Missing → "N
|
|
|
83
83
|
```
|
|
84
84
|
PLAN_TASK_FILES=!`ls .deepflow/plans/doing-*.md 2>/dev/null | tr '\n' ' ' || echo 'NOT_FOUND'`
|
|
85
85
|
```
|
|
86
|
-
When `PLAN_TASK_FILES` is not `NOT_FOUND`, each file `.deepflow/plans/doing-{
|
|
86
|
+
When `PLAN_TASK_FILES` is not `NOT_FOUND`, each file `.deepflow/plans/doing-{specName}.md` contains the full task detail (Files, Steps, ACs, Impact) for all tasks in that spec. Load a task's detail on demand when building its agent prompt (§6). PLAN.md is a slim index — Files and Impact live only in mini-plans.
|
|
87
87
|
|
|
88
88
|
### 2.5. REGISTER NATIVE TASKS
|
|
89
89
|
|
|
@@ -95,7 +95,7 @@ Warn if unplanned `specs/*.md` (excluding doing-/done-) exist (non-blocking).
|
|
|
95
95
|
|
|
96
96
|
**Wave computation (shell injection — do NOT compute manually):**
|
|
97
97
|
```
|
|
98
|
-
WAVE_JSON=!`node bin/wave-runner.js --json --plan PLAN.md 2>/dev/null || echo 'WAVE_ERROR'`
|
|
98
|
+
WAVE_JSON=!`node "${HOME}/.claude/bin/wave-runner.js" --json --plan PLAN.md 2>/dev/null || echo 'WAVE_ERROR'`
|
|
99
99
|
```
|
|
100
100
|
`WAVE_JSON` is structured JSON (produced by T1's `--json` flag). Parse it to determine the current wave and scheduling decisions:
|
|
101
101
|
```json
|
|
@@ -112,7 +112,7 @@ Use `waves[0].tasks` as the ready set for the current wave. Use `isolation` fiel
|
|
|
112
112
|
|
|
113
113
|
**Fallback (text mode):** If `WAVE_JSON` is `WAVE_ERROR` or cannot be parsed as JSON, fall back to text mode:
|
|
114
114
|
```
|
|
115
|
-
WAVE_PLAN=!`node bin/wave-runner.js --plan PLAN.md 2>/dev/null || echo 'WAVE_ERROR'`
|
|
115
|
+
WAVE_PLAN=!`node "${HOME}/.claude/bin/wave-runner.js" --plan PLAN.md 2>/dev/null || echo 'WAVE_ERROR'`
|
|
116
116
|
```
|
|
117
117
|
Text output format:
|
|
118
118
|
```
|
|
@@ -134,7 +134,7 @@ Context ≥50% → checkpoint and exit. Before spawning: `TaskUpdate(status: "in
|
|
|
134
134
|
|
|
135
135
|
**Intra-wave isolation:** For standard (non-spike, non-optimize) parallel tasks, use `isolation: "worktree"` so each agent works in its own isolated branch. Spikes use sub-worktrees managed by §5.7. Optimize tasks run one at a time in the shared worktree.
|
|
136
136
|
|
|
137
|
-
**File conflicts (1 file = 1 writer):** Check `Files:`
|
|
137
|
+
**File conflicts (1 file = 1 writer):** Check `Files:` from wave-runner JSON output or from mini-plan detail files (`.deepflow/plans/doing-{specName}.md`). Overlap → spawn lowest-numbered only; rest stay pending. Log: `"⏳ T{N} deferred — file conflict with T{M} on {filename}"`
|
|
138
138
|
|
|
139
139
|
**≥2 [SPIKE] tasks same problem →** Parallel Spike Probes (§5.7). **[OPTIMIZE] tasks →** Optimize Cycle (§5.9), one at a time.
|
|
140
140
|
|
|
@@ -165,9 +165,9 @@ Spawn Agent(model="haiku", isolation: "none", run_in_background=false):
|
|
|
165
165
|
|
|
166
166
|
### 5.5. RATCHET CHECK
|
|
167
167
|
|
|
168
|
-
Run `node bin/ratchet.js` in the worktree directory after each agent completes:
|
|
168
|
+
Run `node "${HOME}/.claude/bin/ratchet.js"` in the worktree directory after each agent completes:
|
|
169
169
|
```bash
|
|
170
|
-
node bin/ratchet.js --worktree ${WORKTREE_PATH} --snapshot .deepflow/auto-snapshot.txt --task T{N}
|
|
170
|
+
node "${HOME}/.claude/bin/ratchet.js" --worktree ${WORKTREE_PATH} --snapshot .deepflow/auto-snapshot.txt --task T{N}
|
|
171
171
|
```
|
|
172
172
|
|
|
173
173
|
The script handles all health checks internally and outputs structured JSON:
|
|
@@ -190,11 +190,11 @@ The script handles all health checks internally and outputs structured JSON:
|
|
|
190
190
|
- **Exit 0 (PASS):** Commit stands. Proceed to §5.6 wave test agent.
|
|
191
191
|
- **Exit 1 (FAIL):** Script already reverted. Set `TaskUpdate(status: "pending")`. Recompute remaining waves:
|
|
192
192
|
```
|
|
193
|
-
WAVE_JSON=!`node bin/wave-runner.js --json --plan PLAN.md --recalc --failed T{N} 2>/dev/null || echo 'WAVE_ERROR'`
|
|
193
|
+
WAVE_JSON=!`node "${HOME}/.claude/bin/wave-runner.js" --json --plan PLAN.md --recalc --failed T{N} 2>/dev/null || echo 'WAVE_ERROR'`
|
|
194
194
|
```
|
|
195
|
-
(Fall back to text mode if `--json` is unavailable: `node bin/wave-runner.js --plan PLAN.md --recalc --failed T{N}`)
|
|
195
|
+
(Fall back to text mode if `--json` is unavailable: `node "${HOME}/.claude/bin/wave-runner.js" --plan PLAN.md --recalc --failed T{N}`)
|
|
196
196
|
Report: `"✗ T{n}: reverted"`.
|
|
197
|
-
- **Exit 2 (SALVAGEABLE):** Spawn `Agent(model="sonnet")` to fix lint/typecheck issues. Re-run `node bin/ratchet.js`. If still non-zero → revert both commits, set status pending.
|
|
197
|
+
- **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.
|
|
198
198
|
|
|
199
199
|
**Edit scope validation:** `git diff HEAD~1 --name-only` vs allowed globs. Violation → revert, report.
|
|
200
200
|
**Impact completeness:** diff vs Impact callers/duplicates. Gap → advisory warning (no revert).
|
package/src/commands/df/plan.md
CHANGED
|
@@ -161,28 +161,23 @@ You are a spec planner. Your job is to independently analyze a spec and produce
|
|
|
161
161
|
|
|
162
162
|
## OUTPUT FORMAT — MANDATORY (no deviations)
|
|
163
163
|
Return ONLY a markdown task list. Use local T-numbering starting at T1.
|
|
164
|
-
Each task
|
|
164
|
+
Each task is ONE LINE. No sub-bullets in the output.
|
|
165
165
|
|
|
166
166
|
### {spec-name}
|
|
167
167
|
|
|
168
168
|
- [ ] **T{N}**: {Task description}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
Optional fields (add when applicable):
|
|
173
|
-
- Model: haiku | sonnet | opus
|
|
174
|
-
- Effort: low | medium | high
|
|
175
|
-
- Impact: {blast radius details, L3 only}
|
|
176
|
-
- Optimize: {metric block, for metric ACs only}
|
|
169
|
+
- [ ] **T{N}** [SPIKE]: {Task description} | Blocked by: T{M}
|
|
170
|
+
- [ ] **T{N}**: {Task description} | Blocked by: T{M}, T{K}
|
|
177
171
|
|
|
178
172
|
Rules:
|
|
179
|
-
-
|
|
173
|
+
- No blockers → omit `| Blocked by:` entirely (do NOT write "none")
|
|
174
|
+
- Has blockers → append ` | Blocked by: T{N}[, T{M}...]` to end of line
|
|
180
175
|
- T-numbers are local to this spec (T1, T2, T3...)
|
|
181
176
|
- One task = one atomic commit
|
|
182
177
|
- Spike tasks use: **T{N}** [SPIKE]: {description}
|
|
183
178
|
- L0-L1 specs: ONLY spike tasks allowed
|
|
184
179
|
- L2+ specs: spikes + implementation tasks allowed
|
|
185
|
-
-
|
|
180
|
+
- Files, Impact, Model, Effort live ONLY in the mini-plan file (NOT in this output)
|
|
186
181
|
```
|
|
187
182
|
|
|
188
183
|
#### 4.7.3. Collect & Persist Mini-Plans
|
|
@@ -244,13 +239,15 @@ Then apply §5.5 routing matrix. Continue to §6.
|
|
|
244
239
|
|
|
245
240
|
Shell-inject the consolidator output:
|
|
246
241
|
|
|
247
|
-
`` !`node bin/plan-consolidator.js --plans-dir .deepflow/plans/ 2>/dev/null || true` ``
|
|
242
|
+
`` !`node "${HOME}/.claude/bin/plan-consolidator.js" --plans-dir .deepflow/plans/ 2>/dev/null || node .claude/bin/plan-consolidator.js --plans-dir .deepflow/plans/ 2>/dev/null || true` ``
|
|
248
243
|
|
|
249
|
-
This produces
|
|
244
|
+
This produces a slim `## Tasks` section with:
|
|
250
245
|
- Globally sequential T-ids (no gaps, no duplicates) — AC-4
|
|
251
|
-
- Remapped `Blocked by` references (local → global)
|
|
246
|
+
- Remapped `Blocked by` references (local → global), inline on task line
|
|
252
247
|
- `[file-conflict: {filename}]` annotations on cross-spec file overlaps
|
|
248
|
+
- Mini-plan reference links (`> Details: ...`) per spec section
|
|
253
249
|
- Mini-plan files left byte-identical (read-only) — AC-5
|
|
250
|
+
- **No Files, Impact, Model, or Effort** — those live only in mini-plans
|
|
254
251
|
|
|
255
252
|
If the consolidator output is empty or contains `(no mini-plan files found` → abort, report error.
|
|
256
253
|
|
|
@@ -319,14 +316,18 @@ Defaults: sonnet / medium.
|
|
|
319
316
|
|
|
320
317
|
## Tasks
|
|
321
318
|
|
|
322
|
-
{Insert the consolidated tasks from plan-consolidator verbatim, adding
|
|
319
|
+
{Insert the consolidated tasks from plan-consolidator verbatim, adding ` — model/effort` to each task line per the routing matrix. Do NOT alter T-ids, descriptions, Blocked by, or conflict annotations.}
|
|
320
|
+
|
|
321
|
+
Example transformation:
|
|
322
|
+
Input: `- [ ] **T3**: Create pkg/engine/go.mod | Blocked by: T8`
|
|
323
|
+
Output: `- [ ] **T3**: Create pkg/engine/go.mod — haiku/low | Blocked by: T8`
|
|
323
324
|
|
|
324
325
|
Rules:
|
|
325
326
|
- Do NOT renumber T-ids — they are already globally sequential from plan-consolidator
|
|
326
|
-
- Do NOT modify Blocked by
|
|
327
|
-
-
|
|
328
|
-
- Preserve all existing fields (Impact:, Optimize:, tags, etc.)
|
|
327
|
+
- Do NOT modify Blocked by or conflict annotations — they are mechanical outputs
|
|
328
|
+
- Insert ` — {model}/{effort}` BEFORE ` | Blocked by:` (or at end if no blocker)
|
|
329
329
|
- Spike tasks keep their [SPIKE] or [OPTIMIZE] markers
|
|
330
|
+
- One line per task — no sub-bullets
|
|
330
331
|
```
|
|
331
332
|
|
|
332
333
|
**Post-consolidation:**
|
|
@@ -423,7 +424,16 @@ Prune stale `done-*` sections and orphaned headers. Recalculate Summary. Empty
|
|
|
423
424
|
|
|
424
425
|
**Fan-out path:** Run ONLY after §5B consolidation is complete (AC-13). Operate on successfully planned specs only — specs whose sub-agents failed (§4.7.3) are NOT renamed and NOT appended to PLAN.md.
|
|
425
426
|
|
|
426
|
-
|
|
427
|
+
Write PLAN.md as a **slim index** with progressive disclosure:
|
|
428
|
+
- Summary table (counts)
|
|
429
|
+
- Spec Gaps (from Opus output)
|
|
430
|
+
- Cross-Spec Resolution narrative (from Opus output, if >1 spec)
|
|
431
|
+
- Tasks grouped by `### doing-{spec-name}` with `> Details:` reference to mini-plan
|
|
432
|
+
- One line per task: `- [ ] **T{N}**: desc — model/effort [| Blocked by: ...]`
|
|
433
|
+
|
|
434
|
+
Files, Impact, Steps live ONLY in mini-plans (`.deepflow/plans/doing-{name}.md`).
|
|
435
|
+
|
|
436
|
+
Rename `specs/feature.md` → `specs/doing-feature.md` for each successfully planned spec only.
|
|
427
437
|
|
|
428
438
|
Report:
|
|
429
439
|
```
|