deepflow 0.1.105 → 0.1.106

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.
@@ -18,24 +18,20 @@
18
18
 
19
19
  const fs = require('fs');
20
20
  const path = require('path');
21
+ const { readStdinIfMain } = require('./lib/hook-stdin');
21
22
 
22
23
  const event = process.env.CLAUDE_HOOK_EVENT || '';
23
24
 
24
- // Read stdin for hook payload
25
- let raw = '';
26
- process.stdin.setEncoding('utf8');
27
- process.stdin.on('data', d => raw += d);
28
- process.stdin.on('end', () => {
25
+ readStdinIfMain(module, (data) => {
29
26
  try {
30
- main();
27
+ main(data);
31
28
  } catch (_e) {
32
29
  // REQ-8: never break Claude Code
33
30
  }
34
- process.exit(0);
35
31
  });
36
32
 
37
- function main() {
38
- const baseDir = findProjectDir();
33
+ function main(data) {
34
+ const baseDir = findProjectDir(data);
39
35
  if (!baseDir) return;
40
36
 
41
37
  const deepflowDir = path.join(baseDir, '.deepflow');
@@ -44,20 +40,18 @@ function main() {
44
40
  const tokenHistoryPath = path.join(deepflowDir, 'token-history.jsonl');
45
41
 
46
42
  if (event === 'PreToolUse') {
47
- handlePreToolUse(deepflowDir, markerPath, usagePath, tokenHistoryPath);
43
+ handlePreToolUse(data, deepflowDir, markerPath, usagePath, tokenHistoryPath);
48
44
  } else if (event === 'PostToolUse') {
49
- handlePostToolUse(markerPath);
45
+ handlePostToolUse(data, markerPath);
50
46
  } else if (event === 'SessionStart') {
51
- handleSessionStart(markerPath, usagePath, tokenHistoryPath);
47
+ handleSessionStart(data, markerPath, usagePath, tokenHistoryPath);
52
48
  } else if (event === 'SessionEnd') {
53
49
  handleSessionEnd(deepflowDir, markerPath, usagePath, tokenHistoryPath);
54
50
  }
55
51
  }
56
52
 
57
- function handlePreToolUse(deepflowDir, markerPath, usagePath, tokenHistoryPath) {
58
- let payload;
59
- try { payload = JSON.parse(raw); } catch { return; }
60
-
53
+ function handlePreToolUse(data, deepflowDir, markerPath, usagePath, tokenHistoryPath) {
54
+ const payload = data;
61
55
  const toolName = payload.tool_name || '';
62
56
  const toolInput = payload.tool_input || {};
63
57
 
@@ -96,12 +90,11 @@ function handlePreToolUse(deepflowDir, markerPath, usagePath, tokenHistoryPath)
96
90
  safeWriteFile(markerPath, JSON.stringify(marker, null, 2));
97
91
  }
98
92
 
99
- function handlePostToolUse(markerPath) {
93
+ function handlePostToolUse(data, markerPath) {
100
94
  if (!safeExists(markerPath)) return;
101
95
 
102
96
  // Don't count the Skill call itself (the one that opened the marker)
103
- let payload;
104
- try { payload = JSON.parse(raw); } catch { return; }
97
+ const payload = data;
105
98
  const toolName = payload.tool_name || '';
106
99
  const toolInput = payload.tool_input || {};
107
100
  if (toolName === 'Skill' && (toolInput.skill || '').startsWith('df:')) return;
@@ -119,10 +112,9 @@ function handlePostToolUse(markerPath) {
119
112
  * On /clear or /compact, context resets — close any orphaned marker.
120
113
  * Only fires for source=clear|compact (not startup/resume).
121
114
  */
122
- function handleSessionStart(markerPath, usagePath, tokenHistoryPath) {
115
+ function handleSessionStart(data, markerPath, usagePath, tokenHistoryPath) {
123
116
  if (!safeExists(markerPath)) return;
124
- let payload;
125
- try { payload = JSON.parse(raw); } catch { return; }
117
+ const payload = data;
126
118
  const source = payload.source || '';
127
119
  if (source === 'clear' || source === 'compact') {
128
120
  closeCommand(markerPath, usagePath, tokenHistoryPath);
@@ -250,11 +242,10 @@ function parseTranscriptOutputTokens(transcriptPath, offset) {
250
242
  /**
251
243
  * Find the project directory from hook payload or environment.
252
244
  */
253
- function findProjectDir() {
245
+ function findProjectDir(data) {
254
246
  try {
255
- const payload = JSON.parse(raw);
256
- if (payload.cwd) return payload.cwd;
257
- if (payload.workspace && payload.workspace.current_dir) return payload.workspace.current_dir;
247
+ if (data && data.cwd) return data.cwd;
248
+ if (data && data.workspace && data.workspace.current_dir) return data.workspace.current_dir;
258
249
  } catch (_e) {
259
250
  // fall through
260
251
  }
@@ -14,6 +14,7 @@
14
14
 
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
+ const { readStdinIfMain } = require('./lib/hook-stdin');
17
18
 
18
19
  /**
19
20
  * Extract task_id from Agent prompt.
@@ -61,61 +62,50 @@ function resolveProjectRoot(cwd) {
61
62
  return cwd;
62
63
  }
63
64
 
64
- // Read all stdin, then process
65
- let raw = '';
66
- process.stdin.setEncoding('utf8');
67
- process.stdin.on('data', chunk => { raw += chunk; });
68
- process.stdin.on('end', () => {
69
- try {
70
- const data = JSON.parse(raw);
71
-
72
- // Only fire for Agent tool calls
73
- if (data.tool_name !== 'Agent') {
74
- process.exit(0);
75
- }
76
-
77
- const prompt = (data.tool_input && data.tool_input.prompt) || '';
78
- const taskId = extractTaskId(prompt);
79
-
80
- // Only record if we have a task_id
81
- if (!taskId) {
82
- process.exit(0);
83
- }
84
-
85
- const cwd = data.cwd || process.cwd();
86
- const projectRoot = resolveProjectRoot(cwd);
87
- const historyFile = path.join(projectRoot, '.deepflow', 'execution-history.jsonl');
88
-
89
- const timestamp = new Date().toISOString();
90
- const sessionId = data.session_id || null;
91
- const spec = extractSpec(prompt);
92
- const status = extractStatus(data.tool_response);
93
-
94
- const startRecord = {
95
- type: 'task_start',
96
- task_id: taskId,
97
- spec,
98
- session_id: sessionId,
99
- timestamp,
100
- };
65
+ readStdinIfMain(module, (data) => {
66
+ // Only fire for Agent tool calls
67
+ if (data.tool_name !== 'Agent') {
68
+ return;
69
+ }
101
70
 
102
- const endRecord = {
103
- type: 'task_end',
104
- task_id: taskId,
105
- session_id: sessionId,
106
- status,
107
- timestamp,
108
- };
71
+ const prompt = (data.tool_input && data.tool_input.prompt) || '';
72
+ const taskId = extractTaskId(prompt);
109
73
 
110
- const logDir = path.dirname(historyFile);
111
- if (!fs.existsSync(logDir)) {
112
- fs.mkdirSync(logDir, { recursive: true });
113
- }
74
+ // Only record if we have a task_id
75
+ if (!taskId) {
76
+ return;
77
+ }
114
78
 
115
- fs.appendFileSync(historyFile, JSON.stringify(startRecord) + '\n');
116
- fs.appendFileSync(historyFile, JSON.stringify(endRecord) + '\n');
117
- } catch (_e) {
118
- // Fail silently — never break tool execution (REQ-8)
79
+ const cwd = data.cwd || process.cwd();
80
+ const projectRoot = resolveProjectRoot(cwd);
81
+ const historyFile = path.join(projectRoot, '.deepflow', 'execution-history.jsonl');
82
+
83
+ const timestamp = new Date().toISOString();
84
+ const sessionId = data.session_id || null;
85
+ const spec = extractSpec(prompt);
86
+ const status = extractStatus(data.tool_response);
87
+
88
+ const startRecord = {
89
+ type: 'task_start',
90
+ task_id: taskId,
91
+ spec,
92
+ session_id: sessionId,
93
+ timestamp,
94
+ };
95
+
96
+ const endRecord = {
97
+ type: 'task_end',
98
+ task_id: taskId,
99
+ session_id: sessionId,
100
+ status,
101
+ timestamp,
102
+ };
103
+
104
+ const logDir = path.dirname(historyFile);
105
+ if (!fs.existsSync(logDir)) {
106
+ fs.mkdirSync(logDir, { recursive: true });
119
107
  }
120
- process.exit(0);
108
+
109
+ fs.appendFileSync(historyFile, JSON.stringify(startRecord) + '\n');
110
+ fs.appendFileSync(historyFile, JSON.stringify(endRecord) + '\n');
121
111
  });
@@ -18,6 +18,7 @@
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
20
  const os = require('os');
21
+ const { readStdinIfMain } = require('./lib/hook-stdin');
21
22
 
22
23
  /**
23
24
  * Locate the explore-protocol.md template.
@@ -34,50 +35,40 @@ function findProtocol(cwd) {
34
35
  return null;
35
36
  }
36
37
 
37
- let raw = '';
38
- process.stdin.setEncoding('utf8');
39
- process.stdin.on('data', chunk => raw += chunk);
40
- process.stdin.on('end', () => {
41
- try {
42
- const payload = JSON.parse(raw);
43
- const { tool_name, tool_input, cwd } = payload;
38
+ readStdinIfMain(module, (payload) => {
39
+ const { tool_name, tool_input, cwd } = payload;
44
40
 
45
- // Only intercept Agent calls with subagent_type "Explore"
46
- if (tool_name !== 'Agent') {
47
- process.exit(0);
48
- }
49
- const subagentType = (tool_input.subagent_type || '').toLowerCase();
50
- if (subagentType !== 'explore') {
51
- process.exit(0);
52
- }
41
+ // Only intercept Agent calls with subagent_type "Explore"
42
+ if (tool_name !== 'Agent') {
43
+ return;
44
+ }
45
+ const subagentType = (tool_input.subagent_type || '').toLowerCase();
46
+ if (subagentType !== 'explore') {
47
+ return;
48
+ }
53
49
 
54
- const protocolPath = findProtocol(cwd || process.cwd());
55
- if (!protocolPath) {
56
- // No template found — allow without modification
57
- process.exit(0);
58
- }
50
+ const protocolPath = findProtocol(cwd || process.cwd());
51
+ if (!protocolPath) {
52
+ // No template found — allow without modification
53
+ return;
54
+ }
59
55
 
60
- const protocol = fs.readFileSync(protocolPath, 'utf8').trim();
61
- const originalPrompt = tool_input.prompt || '';
56
+ const protocol = fs.readFileSync(protocolPath, 'utf8').trim();
57
+ const originalPrompt = tool_input.prompt || '';
62
58
 
63
- // Append protocol as a system-level suffix the agent must follow
64
- const updatedPrompt = `${originalPrompt}\n\n---\n## Search Protocol (auto-injected — MUST follow)\n\n${protocol}`;
59
+ // Append protocol as a system-level suffix the agent must follow
60
+ const updatedPrompt = `${originalPrompt}\n\n---\n## Search Protocol (auto-injected — MUST follow)\n\n${protocol}`;
65
61
 
66
- const result = {
67
- hookSpecificOutput: {
68
- hookEventName: 'PreToolUse',
69
- permissionDecision: 'allow',
70
- updatedInput: {
71
- ...tool_input,
72
- prompt: updatedPrompt,
73
- },
62
+ const result = {
63
+ hookSpecificOutput: {
64
+ hookEventName: 'PreToolUse',
65
+ permissionDecision: 'allow',
66
+ updatedInput: {
67
+ ...tool_input,
68
+ prompt: updatedPrompt,
74
69
  },
75
- };
70
+ },
71
+ };
76
72
 
77
- process.stdout.write(JSON.stringify(result));
78
- process.exit(0);
79
- } catch {
80
- // Never break Claude Code
81
- process.exit(0);
82
- }
73
+ process.stdout.write(JSON.stringify(result));
83
74
  });
@@ -18,6 +18,7 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { execFileSync } = require('child_process');
20
20
  const { extractSection } = require('./df-spec-lint');
21
+ const { readStdinIfMain } = require('./lib/hook-stdin');
21
22
 
22
23
  // ── LSP availability check (REQ-5, AC-11) ────────────────────────────────────
23
24
 
@@ -141,6 +142,7 @@ const TAGS = {
141
142
  STUB: 'STUB', // Incomplete stub left in production code
142
143
  PHANTOM: 'PHANTOM', // Reference to non-existent symbol/file/function
143
144
  SCOPE_GAP: 'SCOPE_GAP', // Implementation goes beyond or falls short of spec scope
145
+ CONFIG_GUARD: 'CONFIG_GUARD', // config.yaml/yml modified inside a worktree path
144
146
  };
145
147
 
146
148
  // ── Diff parsing ──────────────────────────────────────────────────────────────
@@ -947,6 +949,43 @@ function formatOutput(results) {
947
949
 
948
950
  // ── Core API ──────────────────────────────────────────────────────────────────
949
951
 
952
+ /**
953
+ * REQ-3: Guard against creation or modification of .deepflow/config.yaml (or config.yml)
954
+ * inside worktree paths. Agents must never alter project-level config from a worktree.
955
+ *
956
+ * Matches `+++ b/` diff header lines whose path contains `.deepflow/config.yaml` or
957
+ * `.deepflow/config.yml`, regardless of worktree depth.
958
+ *
959
+ * Always HARD severity — no advisory variant. Tag: [CONFIG_GUARD]
960
+ *
961
+ * @param {Array} files - Parsed diff files
962
+ * @param {string} specContent - Raw spec markdown (unused, kept for uniform signature)
963
+ * @param {string} taskType - Task type (unused, check always runs)
964
+ * @returns {Array<{ file: string, line: number, tag: string, description: string }>}
965
+ */
966
+ function checkConfigYamlGuard(files, specContent, taskType) { // eslint-disable-line no-unused-vars
967
+ const violations = [];
968
+
969
+ // Pattern matches .deepflow/config.yaml or .deepflow/config.yml anywhere in the path,
970
+ // which covers worktree sub-paths like .claude/worktrees/agent-xyz/.deepflow/config.yaml
971
+ const CONFIG_PATTERN = /\.deepflow\/config\.ya?ml$/;
972
+
973
+ for (const f of files) {
974
+ if (CONFIG_PATTERN.test(f.file)) {
975
+ violations.push({
976
+ file: f.file,
977
+ line: 1,
978
+ tag: TAGS.CONFIG_GUARD,
979
+ description:
980
+ `[CONFIG_GUARD] Modification of "${f.file}" detected inside a worktree. ` +
981
+ 'Agents must not create or modify .deepflow/config.yaml from within a worktree.',
982
+ });
983
+ }
984
+ }
985
+
986
+ return violations;
987
+ }
988
+
950
989
  /**
951
990
  * Check implementation diffs against spec invariants.
952
991
  *
@@ -1011,6 +1050,10 @@ function checkInvariants(diff, specContent, opts = {}) {
1011
1050
  const reqOnlyInTestsViolations = checkReqOnlyInTests(files, specContent, taskType);
1012
1051
  hard.push(...reqOnlyInTestsViolations);
1013
1052
 
1053
+ // REQ-3: config.yaml guard — always hard, no advisory variant
1054
+ const configGuardViolations = checkConfigYamlGuard(files, specContent, taskType);
1055
+ hard.push(...configGuardViolations);
1056
+
1014
1057
  // ── Auto-mode escalation (REQ-9) ─────────────────────────────────────────
1015
1058
  // In auto mode (non-interactive CI/hook runs), all advisory items are promoted
1016
1059
  // to hard failures so the pipeline blocks on any violation.
@@ -1080,78 +1123,63 @@ function isGitCommitBash(toolName, toolInput) {
1080
1123
  return /git\s+commit\b/.test(cmd);
1081
1124
  }
1082
1125
 
1083
- // Run hook mode when stdin is not a TTY (i.e., piped payload from Claude Code)
1084
- if (!process.stdin.isTTY) {
1085
- let raw = '';
1086
- process.stdin.setEncoding('utf8');
1087
- process.stdin.on('data', (chunk) => { raw += chunk; });
1088
- process.stdin.on('end', () => {
1089
- let data;
1090
- try {
1091
- data = JSON.parse(raw);
1092
- } catch (_) {
1093
- // Not valid JSON — not a hook payload, exit silently
1094
- process.exit(0);
1095
- }
1126
+ // Run hook mode when called as main module (stdin payload from Claude Code).
1127
+ // readStdinIfMain guards against hanging when required by tests.
1128
+ readStdinIfMain(module, (data) => {
1129
+ // CLI --invariants mode is handled separately below; if we're here,
1130
+ // we got a JSON payload on stdin (PostToolUse hook invocation).
1131
+ if (process.argv.includes('--invariants')) return;
1096
1132
 
1097
- try {
1098
- const toolName = data.tool_name || '';
1099
- const toolInput = data.tool_input || {};
1133
+ const toolName = data.tool_name || '';
1134
+ const toolInput = data.tool_input || {};
1100
1135
 
1101
- // Only run after a git commit bash call
1102
- if (!isGitCommitBash(toolName, toolInput)) {
1103
- process.exit(0);
1104
- }
1136
+ // Only run after a git commit bash call
1137
+ if (!isGitCommitBash(toolName, toolInput)) {
1138
+ process.exit(0);
1139
+ }
1105
1140
 
1106
- const cwd = data.cwd || process.cwd();
1141
+ const cwd = data.cwd || process.cwd();
1107
1142
 
1108
- const diff = extractDiffFromLastCommit(cwd);
1109
- if (!diff) {
1110
- // No diff available (e.g. initial commit) — pass through
1111
- process.exit(0);
1112
- }
1143
+ const diff = extractDiffFromLastCommit(cwd);
1144
+ if (!diff) {
1145
+ // No diff available (e.g. initial commit) — pass through
1146
+ process.exit(0);
1147
+ }
1113
1148
 
1114
- const specContent = loadActiveSpec(cwd);
1115
- if (!specContent) {
1116
- // No active spec found — not a deepflow project or no spec in progress
1117
- process.exit(0);
1118
- }
1149
+ const specContent = loadActiveSpec(cwd);
1150
+ if (!specContent) {
1151
+ // No active spec found — not a deepflow project or no spec in progress
1152
+ process.exit(0);
1153
+ }
1119
1154
 
1120
- const results = checkInvariants(diff, specContent, { mode: 'auto', taskType: 'implementation', projectRoot: cwd });
1155
+ const results = checkInvariants(diff, specContent, { mode: 'auto', taskType: 'implementation', projectRoot: cwd });
1121
1156
 
1122
- if (results.hard.length > 0) {
1123
- console.error('[df-invariant-check] Hard invariant failures detected:');
1124
- const outputLines = formatOutput(results);
1125
- for (const line of outputLines) {
1126
- if (results.hard.some((v) => formatViolation(v) === line)) {
1127
- console.error(` ${line}`);
1128
- }
1129
- }
1130
- process.exit(1);
1131
- }
1132
-
1133
- if (results.advisory.length > 0) {
1134
- console.warn('[df-invariant-check] Advisory warnings:');
1135
- for (const v of results.advisory) {
1136
- console.warn(` ${formatViolation(v)}`);
1137
- }
1157
+ if (results.hard.length > 0) {
1158
+ console.error('[df-invariant-check] Hard invariant failures detected:');
1159
+ const outputLines = formatOutput(results);
1160
+ for (const line of outputLines) {
1161
+ if (results.hard.some((v) => formatViolation(v) === line)) {
1162
+ console.error(` ${line}`);
1138
1163
  }
1164
+ }
1165
+ process.exit(1);
1166
+ }
1139
1167
 
1140
- process.exit(0);
1141
- } catch (_err) {
1142
- // Unexpected error fail open so we never break non-deepflow projects
1143
- process.exit(0);
1168
+ if (results.advisory.length > 0) {
1169
+ console.warn('[df-invariant-check] Advisory warnings:');
1170
+ for (const v of results.advisory) {
1171
+ console.warn(` ${formatViolation(v)}`);
1144
1172
  }
1145
- });
1146
- } else {
1173
+ }
1174
+ });
1147
1175
 
1148
1176
  // ── CLI entry point (REQ-6) ───────────────────────────────────────────────────
1149
- if (require.main === module) {
1177
+ if (require.main === module && process.argv.includes('--invariants')) {
1150
1178
  const args = process.argv.slice(2);
1151
1179
 
1152
1180
  // Parse --invariants <spec-path> <diff-file>
1153
1181
  const invariantsIdx = args.indexOf('--invariants');
1154
- if (invariantsIdx === -1 || args.length < invariantsIdx + 3) {
1182
+ if (args.length < invariantsIdx + 3) {
1155
1183
  console.error('Usage: df-invariant-check.js --invariants <spec-file.md> <diff-file>');
1156
1184
  console.error('');
1157
1185
  console.error('Options:');
@@ -1216,8 +1244,6 @@ if (require.main === module) {
1216
1244
  process.exit(results.hard.length > 0 ? 1 : 0);
1217
1245
  }
1218
1246
 
1219
- } // end else (TTY / CLI mode)
1220
-
1221
1247
  module.exports = {
1222
1248
  checkInvariants,
1223
1249
  checkLspAvailability,
@@ -1231,4 +1257,5 @@ module.exports = {
1231
1257
  checkReqOnlyInTests,
1232
1258
  checkPhantoms,
1233
1259
  checkScopeGaps,
1260
+ checkConfigYamlGuard,
1234
1261
  };