deepflow 0.1.88 → 0.1.89
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 +45 -9
- package/hooks/df-dashboard-push.js +170 -0
- package/hooks/df-execution-history.js +120 -0
- package/hooks/df-invariant-check.js +126 -0
- package/hooks/df-worktree-guard.js +101 -0
- package/package.json +1 -1
- package/src/commands/df/dashboard.md +35 -0
- package/src/commands/df/execute.md +157 -8
- package/src/commands/df/report.md +2 -0
- package/templates/config-template.yaml +10 -0
package/bin/install.js
CHANGED
|
@@ -187,7 +187,7 @@ async function main() {
|
|
|
187
187
|
console.log(' skills/ — gap-discovery, atomic-commits, code-completeness, browse-fetch, browse-verify');
|
|
188
188
|
console.log(' agents/ — reasoner (/df:auto — autonomous execution via /loop)');
|
|
189
189
|
if (level === 'global') {
|
|
190
|
-
console.log(' hooks/ — statusline, update checker, invariant checker');
|
|
190
|
+
console.log(' hooks/ — statusline, update checker, invariant checker, worktree guard');
|
|
191
191
|
}
|
|
192
192
|
console.log(' hooks/df-spec-* — spec validation (auto-enforced by /df:spec and /df:plan)');
|
|
193
193
|
console.log(' env/ — ENABLE_LSP_TOOL (code navigation via goToDefinition, findReferences, workspaceSymbol)');
|
|
@@ -239,6 +239,10 @@ async function configureHooks(claudeDir) {
|
|
|
239
239
|
const consolidationCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-consolidation-check.js')}"`;
|
|
240
240
|
const quotaLoggerCmd = `node "${path.join(claudeDir, 'hooks', 'df-quota-logger.js')}"`;
|
|
241
241
|
const toolUsageCmd = `node "${path.join(claudeDir, 'hooks', 'df-tool-usage.js')}"`;
|
|
242
|
+
const dashboardPushCmd = `node "${path.join(claudeDir, 'hooks', 'df-dashboard-push.js')}"`;
|
|
243
|
+
const executionHistoryCmd = `node "${path.join(claudeDir, 'hooks', 'df-execution-history.js')}"`;
|
|
244
|
+
const worktreeGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-worktree-guard.js')}"`;
|
|
245
|
+
const invariantCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-invariant-check.js')}"`;
|
|
242
246
|
|
|
243
247
|
let settings = {};
|
|
244
248
|
|
|
@@ -324,10 +328,10 @@ async function configureHooks(claudeDir) {
|
|
|
324
328
|
settings.hooks.SessionEnd = [];
|
|
325
329
|
}
|
|
326
330
|
|
|
327
|
-
// Remove any existing quota logger from SessionEnd
|
|
331
|
+
// Remove any existing quota logger / dashboard push from SessionEnd
|
|
328
332
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
|
|
329
333
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
330
|
-
return !cmd.includes('df-quota-logger');
|
|
334
|
+
return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push');
|
|
331
335
|
});
|
|
332
336
|
|
|
333
337
|
// Add quota logger to SessionEnd
|
|
@@ -337,17 +341,25 @@ async function configureHooks(claudeDir) {
|
|
|
337
341
|
command: quotaLoggerCmd
|
|
338
342
|
}]
|
|
339
343
|
});
|
|
340
|
-
|
|
344
|
+
|
|
345
|
+
// Add dashboard push to SessionEnd (fire-and-forget, skips when dashboard_url unset)
|
|
346
|
+
settings.hooks.SessionEnd.push({
|
|
347
|
+
hooks: [{
|
|
348
|
+
type: 'command',
|
|
349
|
+
command: dashboardPushCmd
|
|
350
|
+
}]
|
|
351
|
+
});
|
|
352
|
+
log('Quota logger + dashboard push configured (SessionEnd)');
|
|
341
353
|
|
|
342
354
|
// Configure PostToolUse hook for tool usage instrumentation
|
|
343
355
|
if (!settings.hooks.PostToolUse) {
|
|
344
356
|
settings.hooks.PostToolUse = [];
|
|
345
357
|
}
|
|
346
358
|
|
|
347
|
-
// Remove any existing deepflow tool usage hooks from PostToolUse
|
|
359
|
+
// Remove any existing deepflow tool usage / execution history / worktree guard / invariant check hooks from PostToolUse
|
|
348
360
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
|
|
349
361
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
350
|
-
return !cmd.includes('df-tool-usage');
|
|
362
|
+
return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-invariant-check');
|
|
351
363
|
});
|
|
352
364
|
|
|
353
365
|
// Add tool usage hook
|
|
@@ -357,6 +369,30 @@ async function configureHooks(claudeDir) {
|
|
|
357
369
|
command: toolUsageCmd
|
|
358
370
|
}]
|
|
359
371
|
});
|
|
372
|
+
|
|
373
|
+
// Add execution history hook
|
|
374
|
+
settings.hooks.PostToolUse.push({
|
|
375
|
+
hooks: [{
|
|
376
|
+
type: 'command',
|
|
377
|
+
command: executionHistoryCmd
|
|
378
|
+
}]
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Add worktree guard hook (blocks Write/Edit to main-branch files when df/* worktree exists)
|
|
382
|
+
settings.hooks.PostToolUse.push({
|
|
383
|
+
hooks: [{
|
|
384
|
+
type: 'command',
|
|
385
|
+
command: worktreeGuardCmd
|
|
386
|
+
}]
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Add invariant check hook (exits 1 on hard failures after git commit)
|
|
390
|
+
settings.hooks.PostToolUse.push({
|
|
391
|
+
hooks: [{
|
|
392
|
+
type: 'command',
|
|
393
|
+
command: invariantCheckCmd
|
|
394
|
+
}]
|
|
395
|
+
});
|
|
360
396
|
log('PostToolUse hook configured');
|
|
361
397
|
|
|
362
398
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
@@ -539,7 +575,7 @@ async function uninstall() {
|
|
|
539
575
|
];
|
|
540
576
|
|
|
541
577
|
if (level === 'global') {
|
|
542
|
-
toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-consolidation-check.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js');
|
|
578
|
+
toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-consolidation-check.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js');
|
|
543
579
|
}
|
|
544
580
|
|
|
545
581
|
for (const item of toRemove) {
|
|
@@ -577,7 +613,7 @@ async function uninstall() {
|
|
|
577
613
|
if (settings.hooks?.SessionEnd) {
|
|
578
614
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(hook => {
|
|
579
615
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
580
|
-
return !cmd.includes('df-quota-logger');
|
|
616
|
+
return !cmd.includes('df-quota-logger') && !cmd.includes('df-dashboard-push');
|
|
581
617
|
});
|
|
582
618
|
if (settings.hooks.SessionEnd.length === 0) {
|
|
583
619
|
delete settings.hooks.SessionEnd;
|
|
@@ -586,7 +622,7 @@ async function uninstall() {
|
|
|
586
622
|
if (settings.hooks?.PostToolUse) {
|
|
587
623
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
|
|
588
624
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
589
|
-
return !cmd.includes('df-tool-usage');
|
|
625
|
+
return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-invariant-check');
|
|
590
626
|
});
|
|
591
627
|
if (settings.hooks.PostToolUse.length === 0) {
|
|
592
628
|
delete settings.hooks.PostToolUse;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deepflow dashboard push — SessionEnd hook
|
|
4
|
+
* Collects session summary (tokens, duration, tool calls, model), gets
|
|
5
|
+
* git user.name, and POSTs to dashboard_url from .deepflow/config.yaml.
|
|
6
|
+
* Silently skips if dashboard_url is not configured.
|
|
7
|
+
* Fire-and-forget: exits immediately after spawning background worker.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// Spawn background process so the hook returns immediately
|
|
13
|
+
if (process.argv[2] !== '--background') {
|
|
14
|
+
const { spawn } = require('child_process');
|
|
15
|
+
const child = spawn(process.execPath, [__filename, '--background'], {
|
|
16
|
+
detached: true,
|
|
17
|
+
stdio: 'ignore',
|
|
18
|
+
// Pass stdin data through env so background process can read it
|
|
19
|
+
env: { ...process.env, _DF_HOOK_INPUT: getStdinSync() }
|
|
20
|
+
});
|
|
21
|
+
child.unref();
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- Background process ---
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
const { execFileSync } = require('child_process');
|
|
31
|
+
const https = require('https');
|
|
32
|
+
const http = require('http');
|
|
33
|
+
|
|
34
|
+
function getStdinSync() {
|
|
35
|
+
// Non-blocking stdin read for the parent process (limited buffer)
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync('/dev/stdin', { encoding: 'utf8', flag: 'rs' }) || '';
|
|
38
|
+
} catch (_e) {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read .deepflow/config.yaml and extract dashboard_url (no yaml dep — regex parse). */
|
|
44
|
+
function getDashboardUrl(cwd) {
|
|
45
|
+
const configPath = path.join(cwd, '.deepflow', 'config.yaml');
|
|
46
|
+
if (!fs.existsSync(configPath)) return null;
|
|
47
|
+
try {
|
|
48
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
49
|
+
const match = content.match(/^\s*dashboard_url\s*:\s*(.+)$/m);
|
|
50
|
+
if (!match) return null;
|
|
51
|
+
const val = match[1].trim().replace(/^['"]|['"]$/g, '');
|
|
52
|
+
return val || null;
|
|
53
|
+
} catch (_e) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Get git user.name in the given directory. Returns 'unknown' on failure. */
|
|
59
|
+
function getGitUser(cwd) {
|
|
60
|
+
try {
|
|
61
|
+
return execFileSync('git', ['config', 'user.name'], {
|
|
62
|
+
cwd,
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
timeout: 3000,
|
|
65
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
66
|
+
}).trim() || 'unknown';
|
|
67
|
+
} catch (_e) {
|
|
68
|
+
return process.env.USER || 'unknown';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** POST JSON payload to url. Returns true on 200. */
|
|
73
|
+
function postJson(url, payload) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = new URL(url);
|
|
78
|
+
} catch (_e) {
|
|
79
|
+
resolve(false);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const body = JSON.stringify(payload);
|
|
84
|
+
const isHttps = parsed.protocol === 'https:';
|
|
85
|
+
const lib = isHttps ? https : http;
|
|
86
|
+
|
|
87
|
+
const options = {
|
|
88
|
+
hostname: parsed.hostname,
|
|
89
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
90
|
+
path: parsed.pathname,
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
'Content-Length': Buffer.byteLength(body)
|
|
95
|
+
},
|
|
96
|
+
timeout: 10000
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const req = lib.request(options, (res) => {
|
|
100
|
+
res.resume();
|
|
101
|
+
res.on('end', () => resolve(res.statusCode === 200));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
req.on('error', () => resolve(false));
|
|
105
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
106
|
+
req.write(body);
|
|
107
|
+
req.end();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function main() {
|
|
112
|
+
try {
|
|
113
|
+
const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
114
|
+
const dashboardUrl = getDashboardUrl(cwd);
|
|
115
|
+
|
|
116
|
+
// Silently skip if not configured
|
|
117
|
+
if (!dashboardUrl) process.exit(0);
|
|
118
|
+
|
|
119
|
+
// Parse session data from hook input (passed via env)
|
|
120
|
+
let hookData = {};
|
|
121
|
+
try {
|
|
122
|
+
const raw = process.env._DF_HOOK_INPUT || '';
|
|
123
|
+
if (raw) hookData = JSON.parse(raw);
|
|
124
|
+
} catch (_e) {
|
|
125
|
+
// fallback: empty data, we'll still send what we know
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const gitUser = getGitUser(cwd);
|
|
129
|
+
const projectName = path.basename(cwd);
|
|
130
|
+
const ts = new Date().toISOString();
|
|
131
|
+
|
|
132
|
+
// Extract token fields from hook data (Claude Code SessionEnd format)
|
|
133
|
+
const usage = hookData.usage || hookData.context_window?.current_usage || {};
|
|
134
|
+
const inputTokens = usage.input_tokens || 0;
|
|
135
|
+
const outputTokens = usage.output_tokens || 0;
|
|
136
|
+
const cacheReadTokens = usage.cache_read_input_tokens || usage.cache_read_tokens || 0;
|
|
137
|
+
const cacheCreationTokens = usage.cache_creation_input_tokens || usage.cache_creation_tokens || 0;
|
|
138
|
+
const model = hookData.model?.id || hookData.model?.display_name || hookData.model || 'unknown';
|
|
139
|
+
const sessionId = hookData.session_id || hookData.sessionId || `${gitUser}:${projectName}:${ts}`;
|
|
140
|
+
const durationMs = hookData.duration_ms || null;
|
|
141
|
+
const toolCalls = hookData.tool_calls || hookData.tool_use_count || 0;
|
|
142
|
+
|
|
143
|
+
const payload = {
|
|
144
|
+
user: gitUser,
|
|
145
|
+
project: projectName,
|
|
146
|
+
session_id: sessionId,
|
|
147
|
+
model,
|
|
148
|
+
tokens: {
|
|
149
|
+
[model]: {
|
|
150
|
+
input: inputTokens,
|
|
151
|
+
output: outputTokens,
|
|
152
|
+
cache_read: cacheReadTokens,
|
|
153
|
+
cache_creation: cacheCreationTokens
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
started_at: hookData.started_at || ts,
|
|
157
|
+
ended_at: ts,
|
|
158
|
+
duration_ms: durationMs,
|
|
159
|
+
tool_calls: toolCalls
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const ingestUrl = dashboardUrl.replace(/\/$/, '') + '/api/ingest';
|
|
163
|
+
await postJson(ingestUrl, payload);
|
|
164
|
+
} catch (_e) {
|
|
165
|
+
// Never break session end
|
|
166
|
+
}
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deepflow execution history recorder
|
|
4
|
+
* PostToolUse hook: fires when the Agent tool completes.
|
|
5
|
+
* Appends task_start + task_end records to {cwd}/.deepflow/execution-history.jsonl.
|
|
6
|
+
* Exits silently (code 0) on all errors — never blocks tool execution (REQ-8).
|
|
7
|
+
*
|
|
8
|
+
* Output record fields:
|
|
9
|
+
* type, task_id, spec, session_id, timestamp, status
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract task_id from Agent prompt.
|
|
19
|
+
* Pattern: T{n} anywhere in the prompt, e.g. "T21: fix bug" → "T21"
|
|
20
|
+
* Falls back to DEEPFLOW_TASK_ID env var (C-6).
|
|
21
|
+
*/
|
|
22
|
+
function extractTaskId(prompt) {
|
|
23
|
+
if (prompt) {
|
|
24
|
+
const match = prompt.match(/T(\d+)/);
|
|
25
|
+
if (match) return `T${match[1]}`;
|
|
26
|
+
}
|
|
27
|
+
return process.env.DEEPFLOW_TASK_ID || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract spec name from Agent prompt.
|
|
32
|
+
* Looks for pattern: "spec: {name}" or "spec:{name}"
|
|
33
|
+
*/
|
|
34
|
+
function extractSpec(prompt) {
|
|
35
|
+
if (!prompt) return null;
|
|
36
|
+
const match = prompt.match(/spec:\s*(\S+)/i);
|
|
37
|
+
return match ? match[1] : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse task status from tool_response content.
|
|
42
|
+
* Looks for TASK_STATUS:{pass|revert|fail} in the response text.
|
|
43
|
+
* Defaults to "unknown" if not found (REQ-2).
|
|
44
|
+
*/
|
|
45
|
+
function extractStatus(toolResponse) {
|
|
46
|
+
const responseStr = JSON.stringify(toolResponse || '');
|
|
47
|
+
const match = responseStr.match(/TASK_STATUS:(pass|revert|fail)/);
|
|
48
|
+
return match ? match[1] : 'unknown';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the project root from cwd.
|
|
53
|
+
* Walks up to find the .deepflow directory, or falls back to cwd itself.
|
|
54
|
+
*/
|
|
55
|
+
function resolveProjectRoot(cwd) {
|
|
56
|
+
if (!cwd) return process.cwd();
|
|
57
|
+
// If inside a worktree, strip down to the project root
|
|
58
|
+
const worktreeMatch = cwd.match(/^(.*?)(?:\/\.deepflow\/worktrees\/[^/]+)/);
|
|
59
|
+
if (worktreeMatch) return worktreeMatch[1];
|
|
60
|
+
return cwd;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Read all stdin, then process
|
|
64
|
+
let raw = '';
|
|
65
|
+
process.stdin.setEncoding('utf8');
|
|
66
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
67
|
+
process.stdin.on('end', () => {
|
|
68
|
+
try {
|
|
69
|
+
const data = JSON.parse(raw);
|
|
70
|
+
|
|
71
|
+
// Only fire for Agent tool calls
|
|
72
|
+
if (data.tool_name !== 'Agent') {
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const prompt = (data.tool_input && data.tool_input.prompt) || '';
|
|
77
|
+
const taskId = extractTaskId(prompt);
|
|
78
|
+
|
|
79
|
+
// Only record if we have a task_id
|
|
80
|
+
if (!taskId) {
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const cwd = data.cwd || process.cwd();
|
|
85
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
86
|
+
const historyFile = path.join(projectRoot, '.deepflow', 'execution-history.jsonl');
|
|
87
|
+
|
|
88
|
+
const timestamp = new Date().toISOString();
|
|
89
|
+
const sessionId = data.session_id || null;
|
|
90
|
+
const spec = extractSpec(prompt);
|
|
91
|
+
const status = extractStatus(data.tool_response);
|
|
92
|
+
|
|
93
|
+
const startRecord = {
|
|
94
|
+
type: 'task_start',
|
|
95
|
+
task_id: taskId,
|
|
96
|
+
spec,
|
|
97
|
+
session_id: sessionId,
|
|
98
|
+
timestamp,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const endRecord = {
|
|
102
|
+
type: 'task_end',
|
|
103
|
+
task_id: taskId,
|
|
104
|
+
session_id: sessionId,
|
|
105
|
+
status,
|
|
106
|
+
timestamp,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const logDir = path.dirname(historyFile);
|
|
110
|
+
if (!fs.existsSync(logDir)) {
|
|
111
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fs.appendFileSync(historyFile, JSON.stringify(startRecord) + '\n');
|
|
115
|
+
fs.appendFileSync(historyFile, JSON.stringify(endRecord) + '\n');
|
|
116
|
+
} catch (_e) {
|
|
117
|
+
// Fail silently — never break tool execution (REQ-8)
|
|
118
|
+
}
|
|
119
|
+
process.exit(0);
|
|
120
|
+
});
|
|
@@ -1020,6 +1020,130 @@ function checkInvariants(diff, specContent, opts = {}) {
|
|
|
1020
1020
|
return { hard, advisory };
|
|
1021
1021
|
}
|
|
1022
1022
|
|
|
1023
|
+
// ── Hook entry point (REQ-5 AC-7) ────────────────────────────────────────────
|
|
1024
|
+
//
|
|
1025
|
+
// When called as a PostToolUse hook, Claude Code pipes a JSON payload on stdin:
|
|
1026
|
+
// { tool_name, tool_input, tool_response, cwd, ... }
|
|
1027
|
+
//
|
|
1028
|
+
// We fire after any Bash call that looks like a git commit, extract the diff
|
|
1029
|
+
// from HEAD~1, load the active spec from .deepflow/, and exit(1) on hard failures.
|
|
1030
|
+
//
|
|
1031
|
+
// Detection: if stdin is not a TTY we treat it as hook mode and attempt JSON parse.
|
|
1032
|
+
// If the payload is not a git-commit Bash call we exit(0) silently.
|
|
1033
|
+
|
|
1034
|
+
function loadActiveSpec(cwd) {
|
|
1035
|
+
const deepflowDir = path.join(cwd, '.deepflow');
|
|
1036
|
+
let specContent = null;
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
// Look for doing-*.md specs first (in-progress)
|
|
1040
|
+
const entries = fs.readdirSync(deepflowDir);
|
|
1041
|
+
const doingSpec = entries.find((e) => e.startsWith('doing-') && e.endsWith('.md'));
|
|
1042
|
+
if (doingSpec) {
|
|
1043
|
+
specContent = fs.readFileSync(path.join(deepflowDir, doingSpec), 'utf8');
|
|
1044
|
+
return specContent;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Fall back to specs/ subdirectory
|
|
1048
|
+
const specsDir = path.join(cwd, 'specs');
|
|
1049
|
+
if (fs.existsSync(specsDir)) {
|
|
1050
|
+
const specEntries = fs.readdirSync(specsDir);
|
|
1051
|
+
const doingInSpecs = specEntries.find((e) => e.startsWith('doing-') && e.endsWith('.md'));
|
|
1052
|
+
if (doingInSpecs) {
|
|
1053
|
+
specContent = fs.readFileSync(path.join(specsDir, doingInSpecs), 'utf8');
|
|
1054
|
+
return specContent;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
} catch (_) {
|
|
1058
|
+
// Cannot read .deepflow or specs dir — return null
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function extractDiffFromLastCommit(cwd) {
|
|
1065
|
+
try {
|
|
1066
|
+
return execSync('git diff HEAD~1 HEAD', {
|
|
1067
|
+
encoding: 'utf8',
|
|
1068
|
+
cwd,
|
|
1069
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1070
|
+
});
|
|
1071
|
+
} catch (_) {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function isGitCommitBash(toolName, toolInput) {
|
|
1077
|
+
if (toolName !== 'Bash') return false;
|
|
1078
|
+
const cmd = (toolInput && (toolInput.command || toolInput.cmd || '')) || '';
|
|
1079
|
+
return /git\s+commit\b/.test(cmd);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Run hook mode when stdin is not a TTY (i.e., piped payload from Claude Code)
|
|
1083
|
+
if (!process.stdin.isTTY) {
|
|
1084
|
+
let raw = '';
|
|
1085
|
+
process.stdin.setEncoding('utf8');
|
|
1086
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
1087
|
+
process.stdin.on('end', () => {
|
|
1088
|
+
let data;
|
|
1089
|
+
try {
|
|
1090
|
+
data = JSON.parse(raw);
|
|
1091
|
+
} catch (_) {
|
|
1092
|
+
// Not valid JSON — not a hook payload, exit silently
|
|
1093
|
+
process.exit(0);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
try {
|
|
1097
|
+
const toolName = data.tool_name || '';
|
|
1098
|
+
const toolInput = data.tool_input || {};
|
|
1099
|
+
|
|
1100
|
+
// Only run after a git commit bash call
|
|
1101
|
+
if (!isGitCommitBash(toolName, toolInput)) {
|
|
1102
|
+
process.exit(0);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const cwd = data.cwd || process.cwd();
|
|
1106
|
+
|
|
1107
|
+
const diff = extractDiffFromLastCommit(cwd);
|
|
1108
|
+
if (!diff) {
|
|
1109
|
+
// No diff available (e.g. initial commit) — pass through
|
|
1110
|
+
process.exit(0);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const specContent = loadActiveSpec(cwd);
|
|
1114
|
+
if (!specContent) {
|
|
1115
|
+
// No active spec found — not a deepflow project or no spec in progress
|
|
1116
|
+
process.exit(0);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const results = checkInvariants(diff, specContent, { mode: 'auto', taskType: 'implementation', projectRoot: cwd });
|
|
1120
|
+
|
|
1121
|
+
if (results.hard.length > 0) {
|
|
1122
|
+
console.error('[df-invariant-check] Hard invariant failures detected:');
|
|
1123
|
+
const outputLines = formatOutput(results);
|
|
1124
|
+
for (const line of outputLines) {
|
|
1125
|
+
if (results.hard.some((v) => formatViolation(v) === line)) {
|
|
1126
|
+
console.error(` ${line}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (results.advisory.length > 0) {
|
|
1133
|
+
console.warn('[df-invariant-check] Advisory warnings:');
|
|
1134
|
+
for (const v of results.advisory) {
|
|
1135
|
+
console.warn(` ${formatViolation(v)}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
process.exit(0);
|
|
1140
|
+
} catch (_err) {
|
|
1141
|
+
// Unexpected error — fail open so we never break non-deepflow projects
|
|
1142
|
+
process.exit(0);
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
} else {
|
|
1146
|
+
|
|
1023
1147
|
// ── CLI entry point (REQ-6) ───────────────────────────────────────────────────
|
|
1024
1148
|
if (require.main === module) {
|
|
1025
1149
|
const args = process.argv.slice(2);
|
|
@@ -1091,6 +1215,8 @@ if (require.main === module) {
|
|
|
1091
1215
|
process.exit(results.hard.length > 0 ? 1 : 0);
|
|
1092
1216
|
}
|
|
1093
1217
|
|
|
1218
|
+
} // end else (TTY / CLI mode)
|
|
1219
|
+
|
|
1094
1220
|
module.exports = {
|
|
1095
1221
|
checkInvariants,
|
|
1096
1222
|
checkLspAvailability,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deepflow worktree guard
|
|
4
|
+
* PostToolUse hook: blocks Write/Edit to main-branch files when a df/* worktree exists.
|
|
5
|
+
*
|
|
6
|
+
* REQ-3 AC-4: exit(1) to block the tool call when all conditions hold:
|
|
7
|
+
* 1. tool_name is Write or Edit
|
|
8
|
+
* 2. current branch is main (or master)
|
|
9
|
+
* 3. a df/* worktree branch exists
|
|
10
|
+
* 4. file_path is NOT on the allowlist (.deepflow/, PLAN.md, specs/)
|
|
11
|
+
*
|
|
12
|
+
* REQ-3 AC-5: allowlisted paths always pass through (no false positives).
|
|
13
|
+
*
|
|
14
|
+
* Exits silently (code 0) on parse errors or git failures — never breaks tool
|
|
15
|
+
* execution in non-deepflow projects.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
// Paths that are always allowed regardless of worktree state
|
|
23
|
+
const ALLOWLIST = [
|
|
24
|
+
/(?:^|\/)\.deepflow\//,
|
|
25
|
+
/(?:^|\/)PLAN\.md$/,
|
|
26
|
+
/(?:^|\/)specs\//,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function isAllowlisted(filePath) {
|
|
30
|
+
return ALLOWLIST.some(re => re.test(filePath));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function currentBranch(cwd) {
|
|
34
|
+
try {
|
|
35
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
cwd,
|
|
38
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
39
|
+
}).trim();
|
|
40
|
+
} catch (_) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function dfWorktreeExists(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
const out = execFileSync('git', ['branch', '--list', 'df/*'], {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
cwd,
|
|
50
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
51
|
+
});
|
|
52
|
+
return out.trim().length > 0;
|
|
53
|
+
} catch (_) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let raw = '';
|
|
59
|
+
process.stdin.setEncoding('utf8');
|
|
60
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
61
|
+
process.stdin.on('end', () => {
|
|
62
|
+
try {
|
|
63
|
+
const data = JSON.parse(raw);
|
|
64
|
+
const toolName = data.tool_name || '';
|
|
65
|
+
|
|
66
|
+
// Only guard Write and Edit
|
|
67
|
+
if (toolName !== 'Write' && toolName !== 'Edit') {
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const filePath = (data.tool_input && data.tool_input.file_path) || '';
|
|
72
|
+
const cwd = data.cwd || process.cwd();
|
|
73
|
+
|
|
74
|
+
// Allowlisted paths always pass
|
|
75
|
+
if (isAllowlisted(filePath)) {
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const branch = currentBranch(cwd);
|
|
80
|
+
|
|
81
|
+
// Only guard when on main/master
|
|
82
|
+
if (branch !== 'main' && branch !== 'master') {
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Block only when a df/* worktree branch exists
|
|
87
|
+
if (!dfWorktreeExists(cwd)) {
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// All conditions met — block the write
|
|
92
|
+
console.error(
|
|
93
|
+
`[df-worktree-guard] Blocked ${toolName} to "${filePath}" on main branch ` +
|
|
94
|
+
`while df/* worktree exists. Make changes inside the worktree branch instead.`
|
|
95
|
+
);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
} catch (_e) {
|
|
98
|
+
// Parse or unexpected error — fail open so we never break non-deepflow projects
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: df:dashboard
|
|
3
|
+
description: View deepflow dashboard in team mode (via URL) or local mode (via CLI server)
|
|
4
|
+
allowed-tools: [Read, Bash]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /df:dashboard — Deepflow Dashboard
|
|
8
|
+
|
|
9
|
+
View the deepflow dashboard in team or local mode.
|
|
10
|
+
|
|
11
|
+
**NEVER:** Spawn agents, use Task tool, use AskUserQuestion, run git, EnterPlanMode, ExitPlanMode
|
|
12
|
+
|
|
13
|
+
**ONLY:** Read config, run npx deepflow-dashboard, open browser
|
|
14
|
+
|
|
15
|
+
## Behavior
|
|
16
|
+
|
|
17
|
+
1. **Check config mode**
|
|
18
|
+
- Read `.deepflow/config.yaml`
|
|
19
|
+
- If `dashboard_url` key exists and is non-empty: TEAM MODE
|
|
20
|
+
- Else: LOCAL MODE
|
|
21
|
+
|
|
22
|
+
2. **TEAM MODE** (dashboard_url configured)
|
|
23
|
+
- Display: `Dashboard URL configured: {dashboard_url}`
|
|
24
|
+
- Open URL in browser via `open "{dashboard_url}"` (macOS) or appropriate command for OS
|
|
25
|
+
|
|
26
|
+
3. **LOCAL MODE** (no dashboard_url)
|
|
27
|
+
- Display: `Starting local deepflow dashboard server...`
|
|
28
|
+
- Run: `npx deepflow-dashboard`
|
|
29
|
+
- Instruct user to open http://localhost:3000 (or configured port) in browser
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
- Gracefully handle missing config.yaml (treat as LOCAL MODE)
|
|
34
|
+
- If dashboard_url exists but is empty string, treat as LOCAL MODE
|
|
35
|
+
- Always confirm mode and action before executing
|
|
@@ -21,10 +21,11 @@ Each task = one background agent. **NEVER use TaskOutput** (100KB+ transcripts e
|
|
|
21
21
|
2. STOP. End turn. Do NOT poll.
|
|
22
22
|
3. On EACH notification:
|
|
23
23
|
a. Ratchet check (§5.5)
|
|
24
|
-
b. Passed → TaskUpdate(status: "completed"), update PLAN.md [x] + commit hash
|
|
25
|
-
c. Failed → partial salvage (§5.5). Salvaged →
|
|
26
|
-
d.
|
|
27
|
-
e.
|
|
24
|
+
b. Passed → wave test agent (§5.6). Tests pass → re-snapshot (§5.6) → TaskUpdate(status: "completed"), update PLAN.md [x] + commit hash
|
|
25
|
+
c. Failed → partial salvage (§5.5). Salvaged → wave test agent (§5.6). Not → git revert, TaskUpdate(status: "pending")
|
|
26
|
+
d. Wave test agent failed after max attempts → revert ALL task commits, TaskUpdate(status: "pending")
|
|
27
|
+
e. Report ONE line: "✓ T1: ratchet+tests passed (abc123)" or "⚕ T1: salvaged+tested (abc124)" or "✗ T1: reverted" or "✗ T1: test agent failed, reverted"
|
|
28
|
+
f. NOT all done → end turn, wait | ALL done → next wave or finish
|
|
28
29
|
4. Between waves: context ≥50% → checkpoint and exit.
|
|
29
30
|
5. Repeat until: all done, all blocked, or context ≥50%.
|
|
30
31
|
```
|
|
@@ -53,7 +54,26 @@ git -C ${WORKTREE_PATH} ls-files | grep -E '\.(test|spec)\.[^/]+$|^test_|_test\.
|
|
|
53
54
|
|
|
54
55
|
### 1.7. NO-TESTS BOOTSTRAP
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
<!-- AC-1: zero test files triggers bootstrap before wave 1 -->
|
|
58
|
+
<!-- AC-2: bootstrap success re-snapshots auto-snapshot.txt; subsequent tasks use updated snapshot -->
|
|
59
|
+
<!-- AC-3: bootstrap failure with default model retries with Opus; double failure halts with specific message -->
|
|
60
|
+
|
|
61
|
+
**Gate:** After §1.6 snapshot, check `auto-snapshot.txt`:
|
|
62
|
+
```bash
|
|
63
|
+
SNAPSHOT_COUNT=$(wc -l < .deepflow/auto-snapshot.txt | tr -d ' ')
|
|
64
|
+
```
|
|
65
|
+
If `SNAPSHOT_COUNT` is `0` (zero test files found), MUST spawn bootstrap agent before wave 1. No implementation tasks may start until bootstrap completes successfully.
|
|
66
|
+
|
|
67
|
+
**Bootstrap flow:**
|
|
68
|
+
1. Spawn `Agent(model="{default_model}", ...)` with Bootstrap prompt (§6). End turn, wait for notification.
|
|
69
|
+
2. **On success (TASK_STATUS:pass):** Re-snapshot immediately:
|
|
70
|
+
```bash
|
|
71
|
+
git -C ${WORKTREE_PATH} ls-files | grep -E '\.(test|spec)\.[^/]+$|^test_|_test\.[^/]+$|^tests/|__tests__/' > .deepflow/auto-snapshot.txt
|
|
72
|
+
```
|
|
73
|
+
All subsequent tasks use this updated snapshot as their ratchet baseline. Proceed to wave 1.
|
|
74
|
+
3. **On failure (TASK_STATUS:fail) with default model:** Retry ONCE with `Agent(model="opus", ...)` using the same Bootstrap prompt.
|
|
75
|
+
- Opus success → re-snapshot (same command above) → proceed to wave 1.
|
|
76
|
+
- Opus failure → halt with message: `"Bootstrap failed with both default and Opus — manual intervention required"`. Do not proceed.
|
|
57
77
|
|
|
58
78
|
### 2. LOAD PLAN
|
|
59
79
|
|
|
@@ -115,6 +135,46 @@ Omit if context.json/token-history.jsonl/awk unavailable. Never fail ratchet for
|
|
|
115
135
|
1. Lint/typecheck-only (build+tests passed): spawn `Agent(model="haiku")` to fix. Re-ratchet. Fail → revert both.
|
|
116
136
|
2. Build/test failure → `git revert HEAD --no-edit` (no salvage).
|
|
117
137
|
|
|
138
|
+
### 5.6. WAVE TEST AGENT
|
|
139
|
+
|
|
140
|
+
<!-- AC-8: After wave ratchet passes, Opus test agent spawns and writes unit tests -->
|
|
141
|
+
<!-- AC-9: Test failures trigger implementer re-spawn with failure feedback; max 3 attempts then revert -->
|
|
142
|
+
<!-- AC-12: auto-snapshot.txt re-generated after wave test agent commits; wave N+1 ratchet includes wave N tests -->
|
|
143
|
+
|
|
144
|
+
**Trigger:** After ratchet check passes (or after successful salvage) for a task.
|
|
145
|
+
|
|
146
|
+
**Attempt tracking:** Initialize `attempt_count = 1` and `failure_feedback = ""` per task when first spawned. Max 3 total attempts (1 initial + 2 retries).
|
|
147
|
+
|
|
148
|
+
**Flow:**
|
|
149
|
+
1. Capture the implementation diff: `git -C ${WORKTREE_PATH} diff HEAD~1` → store as `IMPL_DIFF`.
|
|
150
|
+
2. Spawn `Agent(model="opus")` with Wave Test prompt (§6). `run_in_background=true`. End turn, wait.
|
|
151
|
+
3. On notification:
|
|
152
|
+
a. Run ratchet check (§5.5) — all new + pre-existing tests must pass.
|
|
153
|
+
b. **Tests pass** → commit stands. **Re-snapshot** immediately so wave N+1 ratchet includes wave N tests:
|
|
154
|
+
```bash
|
|
155
|
+
git -C ${WORKTREE_PATH} ls-files | grep -E '\.(test|spec)\.[^/]+$|^test_|_test\.[^/]+$|^tests/|__tests__/' > .deepflow/auto-snapshot.txt
|
|
156
|
+
```
|
|
157
|
+
Task complete. Report: `"✓ T{n}: ratchet+tests passed ({hash})"`.
|
|
158
|
+
c. **Tests fail** →
|
|
159
|
+
- If `attempt_count < 3`:
|
|
160
|
+
- `git revert HEAD --no-edit` (revert test commit)
|
|
161
|
+
- `git revert HEAD --no-edit` (revert implementation commit)
|
|
162
|
+
- Accumulate failure output: `failure_feedback += "Attempt {N}: {truncated_test_output}\n"`
|
|
163
|
+
- `attempt_count += 1`
|
|
164
|
+
- Re-spawn implementer agent with original prompt + failure feedback appendix:
|
|
165
|
+
```
|
|
166
|
+
PREVIOUS FAILURES (attempt {N-1} of 3):
|
|
167
|
+
{failure_feedback}
|
|
168
|
+
Fix the issues above. Do NOT repeat the same mistakes.
|
|
169
|
+
```
|
|
170
|
+
- On implementer notification: ratchet check (§5.5). Passed → goto step 1 (spawn test agent again). Failed → same retry logic.
|
|
171
|
+
- If `attempt_count >= 3`:
|
|
172
|
+
- Revert ALL commits back to pre-task state: `git -C ${WORKTREE_PATH} reset --hard {pre_task_commit}`
|
|
173
|
+
- `TaskUpdate(status: "pending")`
|
|
174
|
+
- Report: `"✗ T{n}: test agent failed after 3 attempts, reverted"`
|
|
175
|
+
|
|
176
|
+
**Output truncation for failure feedback:** Test failures → test names + last 30 lines of output. Build failures → last 15 lines. Cap total `failure_feedback` at 200 lines.
|
|
177
|
+
|
|
118
178
|
### 5.7. PARALLEL SPIKE PROBES
|
|
119
179
|
|
|
120
180
|
Trigger: ≥2 [SPIKE] tasks with same blocker or identical hypothesis.
|
|
@@ -186,19 +246,48 @@ REPEAT:
|
|
|
186
246
|
--- START ---
|
|
187
247
|
{task_id}: {description} Files: {files} Spec: {spec}
|
|
188
248
|
{If reverted: DO NOT repeat: - Cycle {N}: "{reason}"}
|
|
249
|
+
{If spike insights exist:
|
|
250
|
+
spike_results:
|
|
251
|
+
hypothesis: {hypothesis from spike_insights}
|
|
252
|
+
outcome: {outcome}
|
|
253
|
+
edge_cases: {edge_cases}
|
|
254
|
+
insight: {insight from probe_learnings}
|
|
255
|
+
}
|
|
189
256
|
Success criteria: {ACs from spec relevant to this task}
|
|
190
257
|
--- MIDDLE (omit for low effort; omit deps for medium) ---
|
|
191
258
|
Impact: Callers: {file} ({why}) | Duplicates: [active→consolidate] [dead→DELETE] | Data flow: {consumers}
|
|
192
259
|
Prior tasks: {dep_id}: {summary}
|
|
193
260
|
Steps: 1. chub search/get for APIs 2. LSP findReferences, add unlisted callers 3. Read all Impact files 4. Implement 5. Commit
|
|
194
261
|
--- END ---
|
|
195
|
-
Spike results: {winner learnings}
|
|
196
262
|
Duplicates: [active]→consolidate [dead]→DELETE. ONLY job: code+commit. No merge/rename/checkout.
|
|
263
|
+
Last line of your response MUST be: TASK_STATUS:pass (if successful) or TASK_STATUS:fail (if failed) or TASK_STATUS:revert (if reverted)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**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`
|
|
267
|
+
|
|
268
|
+
**Wave Test** (`Agent(model="opus")`):
|
|
197
269
|
```
|
|
270
|
+
--- START ---
|
|
271
|
+
You are a QA engineer. Write unit tests for the following code changes.
|
|
272
|
+
Use {test_framework}. Test behavioral correctness, not implementation details.
|
|
273
|
+
Spec: {spec}. Task: {task_id}.
|
|
274
|
+
|
|
275
|
+
Implementation diff:
|
|
276
|
+
{IMPL_DIFF}
|
|
198
277
|
|
|
199
|
-
|
|
278
|
+
--- MIDDLE ---
|
|
279
|
+
Files changed: {changed_files}
|
|
280
|
+
Existing test patterns: {test_file_examples from auto-snapshot.txt, first 3}
|
|
200
281
|
|
|
201
|
-
|
|
282
|
+
--- END ---
|
|
283
|
+
Write thorough unit tests covering: happy paths, edge cases, error handling.
|
|
284
|
+
Follow existing test conventions in the codebase.
|
|
285
|
+
Commit as: test({spec}): wave-{N} unit tests
|
|
286
|
+
Do NOT modify implementation files. ONLY add/edit test files.
|
|
287
|
+
Last line of your response MUST be: TASK_STATUS:pass or TASK_STATUS:fail
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**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`
|
|
202
291
|
|
|
203
292
|
**Optimize Task** (`Agent(model="opus")`):
|
|
204
293
|
```
|
|
@@ -210,6 +299,7 @@ CONSTRAINT: ONE atomic change.
|
|
|
210
299
|
Last 5 cycles + failed hypotheses + Impact/deps.
|
|
211
300
|
--- END ---
|
|
212
301
|
{Learnings}. ONE change + commit. No metric run, no multiple changes.
|
|
302
|
+
Last line of your response MUST be: TASK_STATUS:pass or TASK_STATUS:fail or TASK_STATUS:revert
|
|
213
303
|
```
|
|
214
304
|
|
|
215
305
|
**Optimize Probe** (`Agent(model="opus")`):
|
|
@@ -224,11 +314,68 @@ Current/Target. Role instruction:
|
|
|
224
314
|
Full history + all failed hypotheses.
|
|
225
315
|
--- END ---
|
|
226
316
|
ONE atomic change. Commit. STOP.
|
|
317
|
+
Last line of your response MUST be: TASK_STATUS:pass or TASK_STATUS:fail or TASK_STATUS:revert
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Final Test** (`Agent(model="opus")`):
|
|
321
|
+
```
|
|
322
|
+
--- START ---
|
|
323
|
+
You are an independent QA engineer. You have ONLY the spec and exported interfaces below.
|
|
324
|
+
You cannot read implementation files — you must treat the system as a black box.
|
|
325
|
+
Write integration tests that verify EACH acceptance criterion from the spec.
|
|
326
|
+
|
|
327
|
+
Spec:
|
|
328
|
+
{SPEC_CONTENT}
|
|
329
|
+
|
|
330
|
+
Exported interfaces:
|
|
331
|
+
{EXPORTED_INTERFACES}
|
|
332
|
+
|
|
333
|
+
--- END ---
|
|
334
|
+
Write integration tests covering every AC in the spec.
|
|
335
|
+
Test through public interfaces only — no internal imports, no implementation details.
|
|
336
|
+
If an AC cannot be tested through exports alone, write a test stub with a TODO comment explaining why.
|
|
337
|
+
Commit as: test({spec}): integration tests
|
|
338
|
+
Do NOT read or modify implementation files. ONLY add/edit test files.
|
|
339
|
+
Last line of your response MUST be: TASK_STATUS:pass or TASK_STATUS:fail
|
|
227
340
|
```
|
|
228
341
|
|
|
229
342
|
### 8. COMPLETE SPECS
|
|
230
343
|
|
|
344
|
+
<!-- AC-10: After all waves, Opus black-box test agent spawns with spec + exports only (no implementation) -->
|
|
345
|
+
<!-- AC-11: Final integration tests must all pass before merge proceeds; failure blocks merge -->
|
|
346
|
+
|
|
231
347
|
All tasks done for `doing-*` spec:
|
|
348
|
+
|
|
349
|
+
**8.1. Final Test Agent (black-box integration tests):**
|
|
350
|
+
|
|
351
|
+
Before merge, spawn an independent Opus QA agent that sees ONLY the spec and exported interfaces — never implementation source.
|
|
352
|
+
|
|
353
|
+
1. Extract exported interfaces from the worktree (public API surface):
|
|
354
|
+
```bash
|
|
355
|
+
# Collect exported symbols — adapt pattern to language
|
|
356
|
+
git -C ${WORKTREE_PATH} diff main --name-only | xargs grep -h '^\(export\|pub \|func \|def \)' 2>/dev/null | head -100
|
|
357
|
+
```
|
|
358
|
+
Store result as `EXPORTED_INTERFACES`. Also load spec content: `cat specs/doing-{name}.md` → `SPEC_CONTENT`.
|
|
359
|
+
|
|
360
|
+
2. Spawn `Agent(model="opus")` with Final Test prompt (§6). `run_in_background=true`. End turn, wait.
|
|
361
|
+
|
|
362
|
+
3. On notification:
|
|
363
|
+
a. Run ratchet check (§5.5) — all integration tests must pass.
|
|
364
|
+
b. **Tests pass** → commit stands. Proceed to step 8.2 (merge).
|
|
365
|
+
c. **Tests fail** → **merge is blocked**. Do NOT retry. Report:
|
|
366
|
+
`"✗ Final integration tests failed for {spec} — merge blocked, requires human review"`
|
|
367
|
+
Leave worktree intact. Set all spec tasks back to `TaskUpdate(status: "pending")`.
|
|
368
|
+
Write failure details to `.deepflow/results/final-test-{spec}.yaml`:
|
|
369
|
+
```yaml
|
|
370
|
+
spec: {spec}
|
|
371
|
+
status: blocked
|
|
372
|
+
reason: "Final integration tests failed"
|
|
373
|
+
output: |
|
|
374
|
+
{truncated test output — last 30 lines}
|
|
375
|
+
```
|
|
376
|
+
STOP. Do not proceed to merge.
|
|
377
|
+
|
|
378
|
+
**8.2. Merge and cleanup:**
|
|
232
379
|
1. `skill: "df:verify", args: "doing-{name}"` — runs L0-L4 gates, merges, cleans worktree, renames doing→done, extracts decisions. Fail (fix tasks added) → stop; `--continue` picks them up.
|
|
233
380
|
2. Remove spec's ENTIRE section from PLAN.md. Recalculate Summary table.
|
|
234
381
|
|
|
@@ -280,4 +427,6 @@ Reverted task: `TaskUpdate(status: "pending")`, dependents stay blocked. Repeate
|
|
|
280
427
|
| Ratchet + metric both required | Keep only if both pass |
|
|
281
428
|
| Plateau → probes | 3 cycles <1% triggers probes |
|
|
282
429
|
| Circuit breaker = 3 reverts | Halts, needs human |
|
|
430
|
+
| Wave test after ratchet | Opus writes tests; 3 attempts then revert |
|
|
431
|
+
| Final test before merge | Opus black-box integration tests; failure blocks merge, no retry |
|
|
283
432
|
| Probe diversity | ≥1 contraditoria + ≥1 ingenua |
|
|
@@ -96,6 +96,16 @@ quality:
|
|
|
96
96
|
# Timeout in seconds to wait for the dev server to become ready (default: 30)
|
|
97
97
|
browser_timeout: 30
|
|
98
98
|
|
|
99
|
+
# deepflow-dashboard team mode settings
|
|
100
|
+
# dashboard_url: URL of the shared team server for POST ingestion
|
|
101
|
+
# Leave blank (or omit) to use local-only mode (no data is pushed)
|
|
102
|
+
# Example: http://team-server:3334
|
|
103
|
+
dashboard_url: ""
|
|
104
|
+
|
|
105
|
+
# Port for `npx deepflow-dashboard serve` (team server mode)
|
|
106
|
+
# Default: 3334 (3333 is reserved for local mode)
|
|
107
|
+
dashboard_port: 3334
|
|
108
|
+
|
|
99
109
|
# Recommended .gitignore entries
|
|
100
110
|
# Add these entries to your .gitignore to exclude instrumentation artifacts
|
|
101
111
|
gitignore_entries:
|