deepflow 0.1.87 → 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 +73 -7
- 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-spec-lint.js +78 -4
- package/hooks/df-statusline.js +77 -5
- package/hooks/df-tool-usage-spike.js +41 -0
- package/hooks/df-tool-usage.js +86 -0
- package/hooks/df-worktree-guard.js +101 -0
- package/package.json +1 -1
- package/src/commands/df/auto-cycle.md +75 -558
- package/src/commands/df/auto.md +9 -48
- package/src/commands/df/consolidate.md +14 -38
- package/src/commands/df/dashboard.md +35 -0
- package/src/commands/df/debate.md +27 -156
- package/src/commands/df/discover.md +35 -181
- package/src/commands/df/execute.md +283 -563
- package/src/commands/df/note.md +37 -176
- package/src/commands/df/plan.md +80 -210
- package/src/commands/df/report.md +29 -184
- package/src/commands/df/resume.md +18 -101
- package/src/commands/df/spec.md +49 -145
- package/src/commands/df/verify.md +59 -606
- package/src/skills/browse-fetch/SKILL.md +32 -257
- package/src/skills/browse-verify/SKILL.md +40 -174
- package/src/skills/code-completeness/SKILL.md +2 -9
- package/src/skills/gap-discovery/SKILL.md +19 -86
- package/templates/config-template.yaml +10 -0
- package/templates/spec-template.md +12 -1
package/hooks/df-statusline.js
CHANGED
|
@@ -75,11 +75,14 @@ function buildContextMeter(contextWindow, data) {
|
|
|
75
75
|
percentage = Math.min(100, Math.round(percentage));
|
|
76
76
|
|
|
77
77
|
// Write context usage to file for deepflow commands
|
|
78
|
-
writeContextUsage(percentage);
|
|
78
|
+
writeContextUsage(percentage, data);
|
|
79
79
|
|
|
80
80
|
// Write token history for instrumentation
|
|
81
81
|
writeTokenHistory(contextWindow, data);
|
|
82
82
|
|
|
83
|
+
// Write cache history for cross-session persistence
|
|
84
|
+
writeCacheHistory(contextWindow, data);
|
|
85
|
+
|
|
83
86
|
// Build 10-segment bar
|
|
84
87
|
const segments = 10;
|
|
85
88
|
const filled = Math.round((percentage / 100) * segments);
|
|
@@ -112,9 +115,10 @@ function checkForUpdate() {
|
|
|
112
115
|
return null;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
function writeContextUsage(percentage) {
|
|
118
|
+
function writeContextUsage(percentage, data) {
|
|
116
119
|
try {
|
|
117
|
-
const
|
|
120
|
+
const baseDir = data?.workspace?.current_dir || process.cwd();
|
|
121
|
+
const deepflowDir = path.join(baseDir, '.deepflow');
|
|
118
122
|
if (!fs.existsSync(deepflowDir)) {
|
|
119
123
|
fs.mkdirSync(deepflowDir, { recursive: true });
|
|
120
124
|
}
|
|
@@ -130,7 +134,8 @@ function writeContextUsage(percentage) {
|
|
|
130
134
|
|
|
131
135
|
function writeTokenHistory(contextWindow, data) {
|
|
132
136
|
try {
|
|
133
|
-
const
|
|
137
|
+
const baseDir = data?.workspace?.current_dir || process.cwd();
|
|
138
|
+
const deepflowDir = path.join(baseDir, '.deepflow');
|
|
134
139
|
if (!fs.existsSync(deepflowDir)) {
|
|
135
140
|
fs.mkdirSync(deepflowDir, { recursive: true });
|
|
136
141
|
}
|
|
@@ -142,6 +147,9 @@ function writeTokenHistory(contextWindow, data) {
|
|
|
142
147
|
const contextWindowSize = contextWindow.context_window_size || 0;
|
|
143
148
|
const usedPercentage = contextWindow.used_percentage || 0;
|
|
144
149
|
|
|
150
|
+
const agentRole = process.env.DEEPFLOW_AGENT_ROLE || 'orchestrator';
|
|
151
|
+
const taskId = process.env.DEEPFLOW_TASK_ID || null;
|
|
152
|
+
|
|
145
153
|
const record = {
|
|
146
154
|
timestamp,
|
|
147
155
|
input_tokens: usage.input_tokens || 0,
|
|
@@ -150,7 +158,9 @@ function writeTokenHistory(contextWindow, data) {
|
|
|
150
158
|
context_window_size: contextWindowSize,
|
|
151
159
|
used_percentage: usedPercentage,
|
|
152
160
|
model,
|
|
153
|
-
session_id: sessionId
|
|
161
|
+
session_id: sessionId,
|
|
162
|
+
agent_role: agentRole,
|
|
163
|
+
task_id: taskId
|
|
154
164
|
};
|
|
155
165
|
|
|
156
166
|
const tokenHistoryPath = path.join(deepflowDir, 'token-history.jsonl');
|
|
@@ -159,3 +169,65 @@ function writeTokenHistory(contextWindow, data) {
|
|
|
159
169
|
// Fail silently
|
|
160
170
|
}
|
|
161
171
|
}
|
|
172
|
+
|
|
173
|
+
function writeCacheHistory(contextWindow, data) {
|
|
174
|
+
try {
|
|
175
|
+
const usage = contextWindow.current_usage || {};
|
|
176
|
+
const sessionId = data.session_id || 'unknown';
|
|
177
|
+
|
|
178
|
+
const inputTokens = usage.input_tokens || 0;
|
|
179
|
+
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
180
|
+
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
181
|
+
const totalTokens = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
182
|
+
|
|
183
|
+
// Compute cache hit ratio: cache_read / total
|
|
184
|
+
const cacheHitRatio = totalTokens > 0 ? cacheReadTokens / totalTokens : 0;
|
|
185
|
+
|
|
186
|
+
const model = data.model?.id || data.model?.display_name || 'unknown';
|
|
187
|
+
const agentRole = process.env.DEEPFLOW_AGENT_ROLE || 'orchestrator';
|
|
188
|
+
const taskId = process.env.DEEPFLOW_TASK_ID || null;
|
|
189
|
+
|
|
190
|
+
const cacheHistoryPath = path.join(os.homedir(), '.claude', 'cache-history.jsonl');
|
|
191
|
+
|
|
192
|
+
// Dedup: only write if session_id differs from last written record
|
|
193
|
+
let lastSessionId = null;
|
|
194
|
+
if (fs.existsSync(cacheHistoryPath)) {
|
|
195
|
+
const content = fs.readFileSync(cacheHistoryPath, 'utf8');
|
|
196
|
+
const lines = content.trimEnd().split('\n');
|
|
197
|
+
if (lines.length > 0) {
|
|
198
|
+
try {
|
|
199
|
+
const lastRecord = JSON.parse(lines[lines.length - 1]);
|
|
200
|
+
lastSessionId = lastRecord.session_id;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// Ignore parse errors on last line
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (sessionId === lastSessionId) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const record = {
|
|
212
|
+
timestamp: new Date().toISOString(),
|
|
213
|
+
session_id: sessionId,
|
|
214
|
+
cache_hit_ratio: Math.round(cacheHitRatio * 10000) / 10000,
|
|
215
|
+
total_tokens: totalTokens,
|
|
216
|
+
agent_breakdown: {
|
|
217
|
+
agent_role: agentRole,
|
|
218
|
+
task_id: taskId,
|
|
219
|
+
model
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Ensure ~/.claude directory exists
|
|
224
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
225
|
+
if (!fs.existsSync(claudeDir)) {
|
|
226
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fs.appendFileSync(cacheHistoryPath, JSON.stringify(record) + '\n');
|
|
230
|
+
} catch (e) {
|
|
231
|
+
// Fail silently
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Spike hook: capture raw PostToolUse stdin payload
|
|
4
|
+
* Writes the raw JSON to /tmp/df-posttooluse-payload.json for inspection.
|
|
5
|
+
* Safe to install temporarily — exits cleanly (code 0) always.
|
|
6
|
+
*
|
|
7
|
+
* Usage in ~/.claude/settings.json:
|
|
8
|
+
* "PostToolUse": [{ "hooks": [{ "type": "command", "command": "node /path/to/df-tool-usage-spike.js" }] }]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
let raw = '';
|
|
16
|
+
process.stdin.setEncoding('utf8');
|
|
17
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
18
|
+
process.stdin.on('end', () => {
|
|
19
|
+
try {
|
|
20
|
+
// Write raw payload for inspection
|
|
21
|
+
fs.writeFileSync('/tmp/df-posttooluse-payload.json', raw);
|
|
22
|
+
|
|
23
|
+
// Also append a minimal summary line for quick review
|
|
24
|
+
const data = JSON.parse(raw);
|
|
25
|
+
const summary = {
|
|
26
|
+
hook_event_name: data.hook_event_name,
|
|
27
|
+
tool_name: data.tool_name,
|
|
28
|
+
tool_use_id: data.tool_use_id,
|
|
29
|
+
session_id: data.session_id,
|
|
30
|
+
cwd: data.cwd,
|
|
31
|
+
permission_mode: data.permission_mode,
|
|
32
|
+
tool_input_keys: data.tool_input ? Object.keys(data.tool_input) : [],
|
|
33
|
+
tool_response_keys: data.tool_response ? Object.keys(data.tool_response) : [],
|
|
34
|
+
transcript_path: data.transcript_path,
|
|
35
|
+
};
|
|
36
|
+
fs.appendFileSync('/tmp/df-posttooluse-summary.jsonl', JSON.stringify(summary) + '\n');
|
|
37
|
+
} catch (_e) {
|
|
38
|
+
// Fail silently — never break tool execution
|
|
39
|
+
}
|
|
40
|
+
process.exit(0);
|
|
41
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deepflow tool usage logger
|
|
4
|
+
* Logs every PostToolUse event to ~/.claude/tool-usage.jsonl for token instrumentation.
|
|
5
|
+
* Exits silently (code 0) on all errors — never breaks tool execution.
|
|
6
|
+
*
|
|
7
|
+
* Output record fields (REQ-2):
|
|
8
|
+
* timestamp, session_id, tool_name, command, output_size_est_tokens,
|
|
9
|
+
* project, phase, task_id
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
|
|
18
|
+
const TOOL_USAGE_LOG = path.join(os.homedir(), '.claude', 'tool-usage.jsonl');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Infer phase from cwd.
|
|
22
|
+
* If cwd contains .deepflow/worktrees/, parse the worktree dir name for phase.
|
|
23
|
+
* Worktree dirs are named "execute", "verify", or task-specific names.
|
|
24
|
+
* Default: "manual"
|
|
25
|
+
*/
|
|
26
|
+
function inferPhase(cwd) {
|
|
27
|
+
if (!cwd) return 'manual';
|
|
28
|
+
const match = cwd.match(/\.deepflow[/\\]worktrees[/\\]([^/\\]+)/);
|
|
29
|
+
if (!match) return 'manual';
|
|
30
|
+
const worktreeName = match[1].toLowerCase();
|
|
31
|
+
if (worktreeName === 'execute') return 'execute';
|
|
32
|
+
if (worktreeName === 'verify') return 'verify';
|
|
33
|
+
// Could be a task-specific worktree — still inside worktrees/, treat as execute
|
|
34
|
+
return 'execute';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract task_id from worktree directory name.
|
|
39
|
+
* Pattern: T{n} prefix, e.g. "T3-feature" → "T3", "T12" → "T12"
|
|
40
|
+
* Returns null if not in a worktree or no task prefix found.
|
|
41
|
+
*/
|
|
42
|
+
function extractTaskId(cwd) {
|
|
43
|
+
if (!cwd) return null;
|
|
44
|
+
const match = cwd.match(/\.deepflow[/\\]worktrees[/\\]([^/\\]+)/);
|
|
45
|
+
if (!match) return null;
|
|
46
|
+
const worktreeName = match[1];
|
|
47
|
+
const taskMatch = worktreeName.match(/^(T\d+)/i);
|
|
48
|
+
return taskMatch ? taskMatch[1].toUpperCase() : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Read all stdin, then process
|
|
52
|
+
let raw = '';
|
|
53
|
+
process.stdin.setEncoding('utf8');
|
|
54
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
55
|
+
process.stdin.on('end', () => {
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(raw);
|
|
58
|
+
|
|
59
|
+
const toolName = data.tool_name || null;
|
|
60
|
+
const toolResponse = data.tool_response;
|
|
61
|
+
const cwd = data.cwd || '';
|
|
62
|
+
|
|
63
|
+
const record = {
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
session_id: data.session_id || null,
|
|
66
|
+
tool_name: toolName,
|
|
67
|
+
command: (toolName === 'Bash' && data.tool_input && data.tool_input.command != null)
|
|
68
|
+
? data.tool_input.command
|
|
69
|
+
: null,
|
|
70
|
+
output_size_est_tokens: Math.ceil(JSON.stringify(toolResponse).length / 4),
|
|
71
|
+
project: cwd ? path.basename(cwd) : null,
|
|
72
|
+
phase: inferPhase(cwd),
|
|
73
|
+
task_id: extractTaskId(cwd),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const logDir = path.dirname(TOOL_USAGE_LOG);
|
|
77
|
+
if (!fs.existsSync(logDir)) {
|
|
78
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fs.appendFileSync(TOOL_USAGE_LOG, JSON.stringify(record) + '\n');
|
|
82
|
+
} catch (_e) {
|
|
83
|
+
// Fail silently — never break tool execution
|
|
84
|
+
}
|
|
85
|
+
process.exit(0);
|
|
86
|
+
});
|
|
@@ -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
|
+
});
|