@dommaker/harness 0.12.10 → 0.12.11
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dommaker/harness",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
4
4
|
"description": "通用工程约束框架 - 铁律系统、检查点验证、测试门控、拦截器",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"harness",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"dist",
|
|
43
|
-
"bin",
|
|
43
|
+
"bin/harness.js",
|
|
44
44
|
"templates",
|
|
45
45
|
"github-actions"
|
|
46
46
|
],
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Harness Knowledge Capture Hook
|
|
4
|
-
*
|
|
5
|
-
* Runs as an afterTurn hook in Claude Code.
|
|
6
|
-
* Detects deep analysis (≥3 source files read) without knowledge-doc output,
|
|
7
|
-
* and warns the user that knowledge may need to be captured.
|
|
8
|
-
*
|
|
9
|
-
* Usage (in settings.json):
|
|
10
|
-
* "hooks": {
|
|
11
|
-
* "afterTurn": [{
|
|
12
|
-
* "command": "node ~/projects/harness/bin/harness-knowledge-capture.js"
|
|
13
|
-
* }]
|
|
14
|
-
* }
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
const path = require('path');
|
|
19
|
-
|
|
20
|
-
// Read turn data from stdin
|
|
21
|
-
let input = '';
|
|
22
|
-
process.stdin.setEncoding('utf-8');
|
|
23
|
-
process.stdin.on('data', (chunk) => input += chunk);
|
|
24
|
-
|
|
25
|
-
process.stdin.on('end', () => {
|
|
26
|
-
try {
|
|
27
|
-
const turn = JSON.parse(input);
|
|
28
|
-
const result = checkTurn(turn);
|
|
29
|
-
if (result.shouldWarn) {
|
|
30
|
-
process.stderr.write(`\n${'='.repeat(60)}\n`);
|
|
31
|
-
process.stderr.write(' ⚠️ 可能遗漏了知识沉淀\n');
|
|
32
|
-
process.stderr.write(`${'='.repeat(60)}\n`);
|
|
33
|
-
process.stderr.write(` 本回合读取了 ${result.fileReads} 个文件\n`);
|
|
34
|
-
process.stderr.write(` 但未写入 .harness/knowledge-docs/\n`);
|
|
35
|
-
process.stderr.write(`\n`);
|
|
36
|
-
process.stderr.write(` 如果这是深度分析,请执行:\n`);
|
|
37
|
-
process.stderr.write(` npx harness knowledge upsert --scope <scope> --file <file> --source claude\n`);
|
|
38
|
-
process.stderr.write(`${'='.repeat(60)}\n\n`);
|
|
39
|
-
}
|
|
40
|
-
} catch (e) {
|
|
41
|
-
// Silently ignore parse errors — non-JSON input is fine
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
function checkTurn(turn) {
|
|
46
|
-
let fileReads = 0;
|
|
47
|
-
let knowledgeWrites = 0;
|
|
48
|
-
|
|
49
|
-
// Walk through messages looking for tool calls
|
|
50
|
-
const messages = turn.messages || [turn];
|
|
51
|
-
for (const msg of messages) {
|
|
52
|
-
// Claude Code format: tool_uses array
|
|
53
|
-
if (msg.role === 'assistant' && msg.tool_uses) {
|
|
54
|
-
for (const tool of msg.tool_uses) {
|
|
55
|
-
if (isFileRead(tool)) fileReads++;
|
|
56
|
-
if (isKnowledgeWrite(tool)) knowledgeWrites++;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Plain tool_calls format
|
|
60
|
-
if (msg.tool_calls) {
|
|
61
|
-
for (const tool of msg.tool_calls) {
|
|
62
|
-
if (isFileRead(tool)) fileReads++;
|
|
63
|
-
if (isKnowledgeWrite(tool)) knowledgeWrites++;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// Messages content with tool use markers
|
|
67
|
-
if (msg.content && typeof msg.content === 'string') {
|
|
68
|
-
const readMatches = msg.content.match(/\bRead\b.*/g);
|
|
69
|
-
if (readMatches) fileReads += readMatches.length;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
fileReads,
|
|
75
|
-
knowledgeWrites,
|
|
76
|
-
shouldWarn: fileReads >= 3 && knowledgeWrites === 0,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function isFileRead(tool) {
|
|
81
|
-
const name = (tool.name || tool.function?.name || '').toLowerCase();
|
|
82
|
-
const isReadTool = ['read', 'glob', 'grep', 'agent'].includes(name);
|
|
83
|
-
if (!isReadTool) return false;
|
|
84
|
-
|
|
85
|
-
const params = tool.parameters || tool.function?.arguments || {};
|
|
86
|
-
const filePath = params.file_path || params.path || params.pattern || '';
|
|
87
|
-
// Only count actual source file reads, not config/memory/docs
|
|
88
|
-
const isSource = /\/(src|packages|apps)\/|\.(ts|tsx|js|jsx|py|go|rs)$/.test(filePath);
|
|
89
|
-
return isSource;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function isKnowledgeWrite(tool) {
|
|
93
|
-
const name = (tool.name || tool.function?.name || '').toLowerCase();
|
|
94
|
-
if (name !== 'write') return false;
|
|
95
|
-
|
|
96
|
-
const params = tool.parameters || tool.function?.arguments || {};
|
|
97
|
-
const filePath = params.file_path || '';
|
|
98
|
-
return filePath.includes('.harness/knowledge-docs/') || filePath.includes('.harness/knowledge/');
|
|
99
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Stop hook — check if deep analysis happened without knowledge capture
|
|
4
|
-
*
|
|
5
|
-
* Reads state file written by harness-knowledge-track.js.
|
|
6
|
-
* If EnterPlanMode was called, or Agent(Explore) was spawned, or 10+ unique
|
|
7
|
-
* directories were Read — and no knowledge files were Written — warn.
|
|
8
|
-
*
|
|
9
|
-
* Output: JSON with systemMessage (shown to user)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const STATE_FILE = '/tmp/claude-knowledge-capture-state.json';
|
|
14
|
-
const LOG_FILE = '/tmp/claude-knowledge-hooks.log';
|
|
15
|
-
const UNIQUE_DIR_THRESHOLD = 10;
|
|
16
|
-
|
|
17
|
-
function log(level, message, data) {
|
|
18
|
-
try {
|
|
19
|
-
const entry = JSON.stringify({ ts: new Date().toISOString(), level, message, ...data }) + '\n';
|
|
20
|
-
fs.appendFileSync(LOG_FILE, entry, 'utf-8');
|
|
21
|
-
} catch {}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
if (!fs.existsSync(STATE_FILE)) process.exit(0);
|
|
26
|
-
|
|
27
|
-
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
28
|
-
const deepAnalysis = state.planned || state.explored || state.readDirs.length >= UNIQUE_DIR_THRESHOLD;
|
|
29
|
-
const missingCapture = !state.captured;
|
|
30
|
-
|
|
31
|
-
log('info', 'stop-check', {
|
|
32
|
-
planned: state.planned, explored: state.explored,
|
|
33
|
-
readDirsCount: state.readDirs.length, captured: state.captured,
|
|
34
|
-
deepAnalysis, missingCapture,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
if (deepAnalysis && missingCapture) {
|
|
38
|
-
const signals = [];
|
|
39
|
-
if (state.planned) signals.push('EnterPlanMode called');
|
|
40
|
-
if (state.explored) signals.push('Agent(Explore) spawned');
|
|
41
|
-
if (state.readDirs.length >= UNIQUE_DIR_THRESHOLD) signals.push(`${state.readDirs.length} unique directories Read`);
|
|
42
|
-
|
|
43
|
-
log('warn', 'missing-capture', { signals });
|
|
44
|
-
|
|
45
|
-
const message = [
|
|
46
|
-
'⚠️ Deep analysis detected but no knowledge captured.',
|
|
47
|
-
` Signals: ${signals.join(', ')}`,
|
|
48
|
-
'',
|
|
49
|
-
' To capture:',
|
|
50
|
-
' 1. Write .harness/knowledge-docs/<scope>.md',
|
|
51
|
-
' 2. npx harness knowledge upsert --scope <scope> --file <file> --source claude',
|
|
52
|
-
'',
|
|
53
|
-
' Or run `harness knowledge sync-status` to check staleness.',
|
|
54
|
-
].join('\n');
|
|
55
|
-
|
|
56
|
-
console.log(JSON.stringify({ systemMessage: message }));
|
|
57
|
-
}
|
|
58
|
-
} catch (e) {
|
|
59
|
-
// Silently ignore errors
|
|
60
|
-
} finally {
|
|
61
|
-
// Clean up state file for next session
|
|
62
|
-
try { fs.unlinkSync(STATE_FILE); } catch {}
|
|
63
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* PostToolUse hook — track deep analysis signals
|
|
4
|
-
*
|
|
5
|
-
* Updates state file with:
|
|
6
|
-
* - planned: EnterPlanMode was called
|
|
7
|
-
* - explored: Agent(Explore) was spawned
|
|
8
|
-
* - readDirs: Set of unique directories Read was called on
|
|
9
|
-
* - captured: Write to .harness/knowledge-docs/ or .harness/knowledge/
|
|
10
|
-
* - turnCount: session turn counter
|
|
11
|
-
*
|
|
12
|
-
* Usage: PostToolUse hook with matcher "EnterPlanMode|Agent|Read|Write|Write"
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
|
|
18
|
-
const STATE_FILE = '/tmp/claude-knowledge-capture-state.json';
|
|
19
|
-
const LOG_FILE = '/tmp/claude-knowledge-hooks.log';
|
|
20
|
-
|
|
21
|
-
function log(level, message, data) {
|
|
22
|
-
try {
|
|
23
|
-
const entry = JSON.stringify({ ts: new Date().toISOString(), level, message, ...data }) + '\n';
|
|
24
|
-
fs.appendFileSync(LOG_FILE, entry, 'utf-8');
|
|
25
|
-
} catch {}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Read hook input from stdin
|
|
29
|
-
let input = '';
|
|
30
|
-
process.stdin.setEncoding('utf-8');
|
|
31
|
-
process.stdin.on('data', (chunk) => input += chunk);
|
|
32
|
-
|
|
33
|
-
process.stdin.on('end', () => {
|
|
34
|
-
try {
|
|
35
|
-
const event = JSON.parse(input);
|
|
36
|
-
const state = loadState();
|
|
37
|
-
updateState(state, event);
|
|
38
|
-
saveState(state);
|
|
39
|
-
} catch (e) {
|
|
40
|
-
// Silently ignore parse errors
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
function loadState() {
|
|
45
|
-
try {
|
|
46
|
-
if (fs.existsSync(STATE_FILE)) {
|
|
47
|
-
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
48
|
-
}
|
|
49
|
-
} catch {}
|
|
50
|
-
return { planned: false, explored: false, readDirs: [], captured: false };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function updateState(state, event) {
|
|
54
|
-
const toolName = event.tool_name || '';
|
|
55
|
-
|
|
56
|
-
if (toolName === 'EnterPlanMode') {
|
|
57
|
-
state.planned = true;
|
|
58
|
-
log('info', 'planned', { tool: toolName });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (toolName === 'Agent') {
|
|
62
|
-
const input = event.tool_input || {};
|
|
63
|
-
if (input.subagent_type === 'Explore') {
|
|
64
|
-
state.explored = true;
|
|
65
|
-
log('info', 'explored', { tool: toolName, subagent_type: input.subagent_type });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (toolName === 'Read') {
|
|
70
|
-
const input = event.tool_input || {};
|
|
71
|
-
const filePath = input.file_path || '';
|
|
72
|
-
const dir = path.dirname(filePath);
|
|
73
|
-
if (dir && dir !== '.' && dir !== '/' && !state.readDirs.includes(dir)) {
|
|
74
|
-
state.readDirs.push(dir);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (toolName === 'Write') {
|
|
79
|
-
const input = event.tool_input || {};
|
|
80
|
-
const filePath = input.file_path || '';
|
|
81
|
-
if (filePath.includes('.harness/knowledge-docs/') || filePath.includes('.harness/knowledge/')) {
|
|
82
|
-
state.captured = true;
|
|
83
|
-
log('info', 'captured', { file: filePath });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function saveState(state) {
|
|
89
|
-
try {
|
|
90
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(state), 'utf-8');
|
|
91
|
-
} catch {}
|
|
92
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# PostToolUse hook — track deep analysis signals
|
|
3
|
-
# Replaces Node.js version. Bash: <1ms vs Node: 33ms cold start.
|
|
4
|
-
# Matcher: Read|Write (most frequent tools, covers all signals needed)
|
|
5
|
-
|
|
6
|
-
STATE_FILE=/tmp/claude-knowledge-capture-state.json
|
|
7
|
-
INPUT=$(cat)
|
|
8
|
-
|
|
9
|
-
tool_name=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
10
|
-
file_path=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
11
|
-
|
|
12
|
-
# Init state file if missing
|
|
13
|
-
[ -f "$STATE_FILE" ] || echo '{"planned":false,"explored":false,"readDirs":[],"captured":false}' > "$STATE_FILE"
|
|
14
|
-
|
|
15
|
-
case "$tool_name" in
|
|
16
|
-
EnterPlanMode)
|
|
17
|
-
jq '.planned = true' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
|
18
|
-
;;
|
|
19
|
-
Read)
|
|
20
|
-
if [ -n "$file_path" ]; then
|
|
21
|
-
dir=$(dirname "$file_path")
|
|
22
|
-
if [ "$dir" != "." ] && [ "$dir" != "/" ]; then
|
|
23
|
-
jq --arg d "$dir" '.readDirs += [$d] | .readDirs |= unique' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
|
24
|
-
fi
|
|
25
|
-
fi
|
|
26
|
-
;;
|
|
27
|
-
Write)
|
|
28
|
-
case "$file_path" in
|
|
29
|
-
*.harness/knowledge-docs/*|*.harness/knowledge/*)
|
|
30
|
-
jq '.captured = true' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
|
31
|
-
;;
|
|
32
|
-
esac
|
|
33
|
-
;;
|
|
34
|
-
esac
|