deepflow 0.1.101 → 0.1.103
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 +55 -9
- package/bin/install.test.js +214 -0
- package/bin/plan-consolidator.js +16 -16
- package/bin/wave-runner.js +48 -16
- package/hooks/df-command-usage.js +287 -0
- package/hooks/df-command-usage.test.js +1019 -0
- package/hooks/df-subagent-registry.js +33 -14
- package/hooks/df-tool-usage.js +8 -0
- package/hooks/df-tool-usage.test.js +200 -0
- package/package.json +1 -1
- package/src/commands/df/execute.md +2 -2
- package/src/commands/df/plan.md +28 -18
|
@@ -9,34 +9,53 @@ process.stdin.on('data', d => raw += d);
|
|
|
9
9
|
process.stdin.on('end', () => {
|
|
10
10
|
try {
|
|
11
11
|
const event = JSON.parse(raw);
|
|
12
|
+
const { session_id, agent_type, agent_id, agent_transcript_path } = event;
|
|
12
13
|
|
|
13
|
-
//
|
|
14
|
-
|
|
14
|
+
// Parse subagent transcript to extract real model and token usage
|
|
15
|
+
let model = 'unknown';
|
|
16
|
+
let tokens_in = 0, tokens_out = 0, cache_read = 0, cache_creation = 0;
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if (agent_transcript_path && fs.existsSync(agent_transcript_path)) {
|
|
19
|
+
const lines = fs.readFileSync(agent_transcript_path, 'utf-8').split('\n');
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (!trimmed) continue;
|
|
23
|
+
try {
|
|
24
|
+
const evt = JSON.parse(trimmed);
|
|
25
|
+
const msg = evt.message || {};
|
|
26
|
+
const usage = msg.usage || evt.usage;
|
|
27
|
+
// Extract model from assistant messages
|
|
28
|
+
const m = msg.model || evt.model;
|
|
29
|
+
if (m && m !== 'unknown') model = m;
|
|
30
|
+
// Accumulate tokens
|
|
31
|
+
if (usage) {
|
|
32
|
+
tokens_in += usage.input_tokens || 0;
|
|
33
|
+
tokens_out += usage.output_tokens || 0;
|
|
34
|
+
cache_read += usage.cache_read_input_tokens || usage.cache_read_tokens || 0;
|
|
35
|
+
cache_creation += usage.cache_creation_input_tokens || usage.cache_creation_tokens || 0;
|
|
36
|
+
}
|
|
37
|
+
} catch { /* skip malformed lines */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
22
40
|
|
|
23
|
-
//
|
|
24
|
-
|
|
41
|
+
// Strip version suffix from model (e.g. claude-haiku-4-5-20251001 → claude-haiku-4-5)
|
|
42
|
+
model = model.replace(/-\d{8}$/, '').replace(/\[\d+[km]\]$/i, '');
|
|
25
43
|
|
|
26
|
-
// Build registry entry
|
|
27
44
|
const entry = {
|
|
28
45
|
session_id,
|
|
29
46
|
agent_type,
|
|
30
47
|
agent_id,
|
|
31
48
|
model,
|
|
32
|
-
|
|
49
|
+
tokens_in,
|
|
50
|
+
tokens_out,
|
|
51
|
+
cache_read,
|
|
52
|
+
cache_creation,
|
|
53
|
+
timestamp: new Date().toISOString()
|
|
33
54
|
};
|
|
34
55
|
|
|
35
|
-
// Append to registry file (fire-and-forget)
|
|
36
56
|
const registryPath = path.join(os.homedir(), '.claude', 'subagent-sessions.jsonl');
|
|
37
57
|
fs.appendFileSync(registryPath, JSON.stringify(entry) + '\n');
|
|
38
58
|
} catch {
|
|
39
|
-
// Exit 0 on any error (fail-open)
|
|
40
59
|
process.exit(0);
|
|
41
60
|
}
|
|
42
61
|
});
|
package/hooks/df-tool-usage.js
CHANGED
|
@@ -60,6 +60,13 @@ process.stdin.on('end', () => {
|
|
|
60
60
|
const toolResponse = data.tool_response;
|
|
61
61
|
const cwd = data.cwd || '';
|
|
62
62
|
|
|
63
|
+
let activeCommand = null;
|
|
64
|
+
try {
|
|
65
|
+
const markerPath = path.join(cwd || process.cwd(), '.deepflow', 'active-command.json');
|
|
66
|
+
const markerRaw = fs.readFileSync(markerPath, 'utf8');
|
|
67
|
+
activeCommand = JSON.parse(markerRaw).command || null;
|
|
68
|
+
} catch (_e) { /* no marker or unreadable — null */ }
|
|
69
|
+
|
|
63
70
|
const record = {
|
|
64
71
|
timestamp: new Date().toISOString(),
|
|
65
72
|
session_id: data.session_id || null,
|
|
@@ -71,6 +78,7 @@ process.stdin.on('end', () => {
|
|
|
71
78
|
project: cwd ? path.basename(cwd) : null,
|
|
72
79
|
phase: inferPhase(cwd),
|
|
73
80
|
task_id: extractTaskId(cwd),
|
|
81
|
+
active_command: activeCommand,
|
|
74
82
|
};
|
|
75
83
|
|
|
76
84
|
const logDir = path.dirname(TOOL_USAGE_LOG);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for hooks/df-tool-usage.js — T3: active_command field in tool-usage records
|
|
3
|
+
*
|
|
4
|
+
* Validates that the tool usage hook reads .deepflow/active-command.json marker
|
|
5
|
+
* and includes the active_command field in tool-usage.jsonl records.
|
|
6
|
+
*
|
|
7
|
+
* Uses Node.js built-in node:test to avoid adding dependencies.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const { execFileSync } = require('node:child_process');
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const HOOK_PATH = path.resolve(__dirname, 'df-tool-usage.js');
|
|
24
|
+
|
|
25
|
+
function makeTmpDir() {
|
|
26
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'df-tool-usage-test-'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rmrf(dir) {
|
|
30
|
+
if (fs.existsSync(dir)) {
|
|
31
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run the tool usage hook as a child process with JSON piped to stdin.
|
|
37
|
+
* Overrides HOME so tool-usage.jsonl goes to a temp location.
|
|
38
|
+
* Returns { stdout, stderr, code }.
|
|
39
|
+
*/
|
|
40
|
+
function runHook(input, { cwd, env: extraEnv } = {}) {
|
|
41
|
+
const json = typeof input === 'string' ? input : JSON.stringify(input);
|
|
42
|
+
const env = { ...process.env, ...extraEnv };
|
|
43
|
+
// Override HOME so the log file goes to our temp dir
|
|
44
|
+
env.HOME = cwd || os.tmpdir();
|
|
45
|
+
try {
|
|
46
|
+
const stdout = execFileSync(
|
|
47
|
+
process.execPath,
|
|
48
|
+
[HOOK_PATH],
|
|
49
|
+
{
|
|
50
|
+
input: json,
|
|
51
|
+
cwd: cwd || os.tmpdir(),
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
timeout: 5000,
|
|
54
|
+
env,
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
return { stdout, stderr: '', code: 0 };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
stdout: err.stdout || '',
|
|
61
|
+
stderr: err.stderr || '',
|
|
62
|
+
code: err.status ?? 1,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a minimal PostToolUse event payload.
|
|
69
|
+
*/
|
|
70
|
+
function makeToolInput(cwd) {
|
|
71
|
+
return {
|
|
72
|
+
session_id: 'tool-test-session',
|
|
73
|
+
tool_name: 'Read',
|
|
74
|
+
tool_input: { file_path: '/tmp/test.js' },
|
|
75
|
+
tool_response: { content: 'file contents here' },
|
|
76
|
+
cwd: cwd,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read the last record from tool-usage.jsonl.
|
|
82
|
+
*/
|
|
83
|
+
function readLastToolRecord(homeDir) {
|
|
84
|
+
const logPath = path.join(homeDir, '.claude', 'tool-usage.jsonl');
|
|
85
|
+
if (!fs.existsSync(logPath)) return null;
|
|
86
|
+
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n');
|
|
87
|
+
return JSON.parse(lines[lines.length - 1]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// T3: active_command field in tool-usage records
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
describe('T3 — tool-usage active_command field', () => {
|
|
95
|
+
let tmpDir;
|
|
96
|
+
let deepflowDir;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
tmpDir = makeTmpDir();
|
|
100
|
+
deepflowDir = path.join(tmpDir, '.deepflow');
|
|
101
|
+
fs.mkdirSync(deepflowDir, { recursive: true });
|
|
102
|
+
// Create .claude dir for tool-usage.jsonl output
|
|
103
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'), { recursive: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
rmrf(tmpDir);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('active_command is set when active-command.json marker exists', () => {
|
|
111
|
+
fs.writeFileSync(
|
|
112
|
+
path.join(deepflowDir, 'active-command.json'),
|
|
113
|
+
JSON.stringify({ command: 'df:plan', started_at: new Date().toISOString() })
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const input = makeToolInput(tmpDir);
|
|
117
|
+
const { code } = runHook(input, { cwd: tmpDir });
|
|
118
|
+
assert.equal(code, 0, 'Hook should exit successfully');
|
|
119
|
+
|
|
120
|
+
const record = readLastToolRecord(tmpDir);
|
|
121
|
+
assert.ok(record, 'Tool usage record should exist');
|
|
122
|
+
assert.equal(record.active_command, 'df:plan', 'active_command should match marker');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('active_command is null when no marker file exists', () => {
|
|
126
|
+
const input = makeToolInput(tmpDir);
|
|
127
|
+
const { code } = runHook(input, { cwd: tmpDir });
|
|
128
|
+
assert.equal(code, 0);
|
|
129
|
+
|
|
130
|
+
const record = readLastToolRecord(tmpDir);
|
|
131
|
+
assert.ok(record, 'Tool usage record should exist');
|
|
132
|
+
assert.equal(record.active_command, null, 'active_command should be null without marker');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('active_command is null when marker file contains corrupt JSON', () => {
|
|
136
|
+
fs.writeFileSync(
|
|
137
|
+
path.join(deepflowDir, 'active-command.json'),
|
|
138
|
+
'{{corrupt json'
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const input = makeToolInput(tmpDir);
|
|
142
|
+
const { code } = runHook(input, { cwd: tmpDir });
|
|
143
|
+
assert.equal(code, 0, 'Hook should not crash on corrupt marker');
|
|
144
|
+
|
|
145
|
+
const record = readLastToolRecord(tmpDir);
|
|
146
|
+
assert.ok(record);
|
|
147
|
+
assert.equal(record.active_command, null, 'active_command should be null for corrupt marker');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('active_command is null when marker has no command field', () => {
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
path.join(deepflowDir, 'active-command.json'),
|
|
153
|
+
JSON.stringify({ other_field: 'value' })
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const input = makeToolInput(tmpDir);
|
|
157
|
+
const { code } = runHook(input, { cwd: tmpDir });
|
|
158
|
+
assert.equal(code, 0);
|
|
159
|
+
|
|
160
|
+
const record = readLastToolRecord(tmpDir);
|
|
161
|
+
assert.ok(record);
|
|
162
|
+
assert.equal(record.active_command, null, 'active_command should be null when command field missing');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('active_command field always present in tool-usage record schema', () => {
|
|
166
|
+
const input = makeToolInput(tmpDir);
|
|
167
|
+
runHook(input, { cwd: tmpDir });
|
|
168
|
+
|
|
169
|
+
const record = readLastToolRecord(tmpDir);
|
|
170
|
+
assert.ok(record);
|
|
171
|
+
assert.ok('active_command' in record, 'active_command key must always be present');
|
|
172
|
+
// Verify other expected fields
|
|
173
|
+
assert.ok('timestamp' in record);
|
|
174
|
+
assert.ok('session_id' in record);
|
|
175
|
+
assert.ok('tool_name' in record);
|
|
176
|
+
assert.ok('phase' in record);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('active_command reads df:execute correctly', () => {
|
|
180
|
+
fs.writeFileSync(
|
|
181
|
+
path.join(deepflowDir, 'active-command.json'),
|
|
182
|
+
JSON.stringify({ command: 'df:execute' })
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const input = makeToolInput(tmpDir);
|
|
186
|
+
runHook(input, { cwd: tmpDir });
|
|
187
|
+
|
|
188
|
+
const record = readLastToolRecord(tmpDir);
|
|
189
|
+
assert.ok(record);
|
|
190
|
+
assert.equal(record.active_command, 'df:execute');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('hook exits 0 even when marker is unreadable (permissions)', () => {
|
|
194
|
+
// Write marker then make the deepflow dir inaccessible won't work on all OS.
|
|
195
|
+
// Instead, set cwd to a non-existent path to trigger the fallback.
|
|
196
|
+
const input = makeToolInput('/nonexistent/path/that/does/not/exist');
|
|
197
|
+
const { code } = runHook(input, { cwd: tmpDir });
|
|
198
|
+
assert.equal(code, 0, 'Hook should always exit 0');
|
|
199
|
+
});
|
|
200
|
+
});
|
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
|
|
|
@@ -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
|
|
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
|
|
@@ -246,11 +241,13 @@ Shell-inject the consolidator output:
|
|
|
246
241
|
|
|
247
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
|
```
|