@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.10",
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