deepflow 0.1.104 → 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.
- package/bin/install.js +21 -0
- package/bin/install.test.js +205 -0
- package/hooks/df-command-usage.js +17 -26
- package/hooks/df-dashboard-push.js +4 -4
- package/hooks/df-dashboard-push.test.js +256 -0
- package/hooks/df-execution-history.js +43 -53
- package/hooks/df-explore-protocol.js +30 -39
- package/hooks/df-invariant-check.js +88 -61
- package/hooks/df-invariant-check.test.js +315 -0
- package/hooks/df-quota-logger.js +11 -23
- package/hooks/df-quota-logger.test.js +324 -0
- package/hooks/df-snapshot-guard.js +32 -40
- package/hooks/df-statusline.js +3 -12
- package/hooks/df-stdin-migration.test.js +106 -0
- package/hooks/df-subagent-registry.js +42 -48
- package/hooks/df-tool-usage-spike.js +15 -26
- package/hooks/df-tool-usage.js +37 -47
- package/hooks/df-worktree-guard.js +28 -36
- package/hooks/lib/hook-stdin.js +47 -0
- package/hooks/lib/hook-stdin.test.js +200 -0
- package/hooks/lib/lint-no-bare-stdin.js +68 -0
- package/hooks/lib/lint-no-bare-stdin.test.js +82 -0
- package/package.json +1 -1
- package/src/commands/df/execute.md +25 -174
- package/src/eval/git-memory.js +8 -8
- package/src/eval/git-memory.test.js +128 -1
- package/src/eval/loop.js +3 -3
- package/src/eval/loop.test.js +158 -0
- package/templates/config-template.yaml +0 -6
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
74
|
+
// Only record if we have a task_id
|
|
75
|
+
if (!taskId) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
114
78
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
const protocolPath = findProtocol(cwd || process.cwd());
|
|
51
|
+
if (!protocolPath) {
|
|
52
|
+
// No template found — allow without modification
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
const protocol = fs.readFileSync(protocolPath, 'utf8').trim();
|
|
57
|
+
const originalPrompt = tool_input.prompt || '';
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
});
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
|
-
const {
|
|
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
|
|
|
@@ -94,7 +95,7 @@ function detectLanguageServer(projectRoot, diffFilePaths) {
|
|
|
94
95
|
*/
|
|
95
96
|
function isBinaryAvailable(binary) {
|
|
96
97
|
try {
|
|
97
|
-
|
|
98
|
+
execFileSync('which', [binary], { stdio: 'ignore' });
|
|
98
99
|
return true;
|
|
99
100
|
} catch (_) {
|
|
100
101
|
return false;
|
|
@@ -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.
|
|
@@ -1064,7 +1107,7 @@ function loadActiveSpec(cwd) {
|
|
|
1064
1107
|
|
|
1065
1108
|
function extractDiffFromLastCommit(cwd) {
|
|
1066
1109
|
try {
|
|
1067
|
-
return
|
|
1110
|
+
return execFileSync('git', ['diff', 'HEAD~1', 'HEAD'], {
|
|
1068
1111
|
encoding: 'utf8',
|
|
1069
1112
|
cwd,
|
|
1070
1113
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -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
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
process.
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
const toolInput = data.tool_input || {};
|
|
1133
|
+
const toolName = data.tool_name || '';
|
|
1134
|
+
const toolInput = data.tool_input || {};
|
|
1100
1135
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1136
|
+
// Only run after a git commit bash call
|
|
1137
|
+
if (!isGitCommitBash(toolName, toolInput)) {
|
|
1138
|
+
process.exit(0);
|
|
1139
|
+
}
|
|
1105
1140
|
|
|
1106
|
-
|
|
1141
|
+
const cwd = data.cwd || process.cwd();
|
|
1107
1142
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1155
|
+
const results = checkInvariants(diff, specContent, { mode: 'auto', taskType: 'implementation', projectRoot: cwd });
|
|
1121
1156
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
}
|
|
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 (
|
|
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
|
};
|