@dommaker/harness 0.12.5 → 0.12.9

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.
Files changed (64) hide show
  1. package/bin/harness-knowledge-capture.js +99 -0
  2. package/bin/harness-knowledge-check.js +63 -0
  3. package/bin/harness-knowledge-track.js +92 -0
  4. package/bin/harness-knowledge-track.sh +34 -0
  5. package/bin/harness.js +81 -0
  6. package/dist/cli/commands/analyze-sessions.d.ts +17 -0
  7. package/dist/cli/commands/analyze-sessions.d.ts.map +1 -0
  8. package/dist/cli/commands/analyze-sessions.js +445 -0
  9. package/dist/cli/commands/analyze-sessions.js.map +1 -0
  10. package/dist/cli/commands/index.d.ts +5 -1
  11. package/dist/cli/commands/index.d.ts.map +1 -1
  12. package/dist/cli/commands/index.js +12 -1
  13. package/dist/cli/commands/index.js.map +1 -1
  14. package/dist/cli/commands/init.js +28 -6
  15. package/dist/cli/commands/init.js.map +1 -1
  16. package/dist/cli/commands/knowledge.d.ts +23 -0
  17. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  18. package/dist/cli/commands/knowledge.js +164 -0
  19. package/dist/cli/commands/knowledge.js.map +1 -1
  20. package/dist/cli/commands/posteval-plan.d.ts +11 -0
  21. package/dist/cli/commands/posteval-plan.d.ts.map +1 -0
  22. package/dist/cli/commands/posteval-plan.js +76 -0
  23. package/dist/cli/commands/posteval-plan.js.map +1 -0
  24. package/dist/cli/commands/release.d.ts +13 -0
  25. package/dist/cli/commands/release.d.ts.map +1 -0
  26. package/dist/cli/commands/release.js +194 -0
  27. package/dist/cli/commands/release.js.map +1 -0
  28. package/dist/cli/commands/update-user-model.d.ts +21 -0
  29. package/dist/cli/commands/update-user-model.d.ts.map +1 -0
  30. package/dist/cli/commands/update-user-model.js +475 -0
  31. package/dist/cli/commands/update-user-model.js.map +1 -0
  32. package/dist/core/constraints/index.d.ts +1 -0
  33. package/dist/core/constraints/index.d.ts.map +1 -1
  34. package/dist/core/constraints/index.js +4 -1
  35. package/dist/core/constraints/index.js.map +1 -1
  36. package/dist/core/constraints/prompt-injection.d.ts +13 -0
  37. package/dist/core/constraints/prompt-injection.d.ts.map +1 -0
  38. package/dist/core/constraints/prompt-injection.js +67 -0
  39. package/dist/core/constraints/prompt-injection.js.map +1 -0
  40. package/dist/knowledge/doctor.d.ts +37 -0
  41. package/dist/knowledge/doctor.d.ts.map +1 -0
  42. package/dist/knowledge/doctor.js +81 -0
  43. package/dist/knowledge/doctor.js.map +1 -0
  44. package/dist/knowledge/index.d.ts +1 -0
  45. package/dist/knowledge/index.d.ts.map +1 -1
  46. package/dist/knowledge/index.js +3 -1
  47. package/dist/knowledge/index.js.map +1 -1
  48. package/dist/knowledge/ingest.js +1 -1
  49. package/dist/knowledge/ingest.js.map +1 -1
  50. package/dist/knowledge/lifecycle.d.ts +4 -2
  51. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  52. package/dist/knowledge/lifecycle.js +18 -3
  53. package/dist/knowledge/lifecycle.js.map +1 -1
  54. package/dist/knowledge/lint.d.ts +16 -0
  55. package/dist/knowledge/lint.d.ts.map +1 -1
  56. package/dist/knowledge/lint.js +84 -0
  57. package/dist/knowledge/lint.js.map +1 -1
  58. package/dist/knowledge/query.d.ts +3 -1
  59. package/dist/knowledge/query.d.ts.map +1 -1
  60. package/dist/knowledge/query.js +11 -1
  61. package/dist/knowledge/query.js.map +1 -1
  62. package/dist/knowledge/types.d.ts +2 -2
  63. package/dist/knowledge/types.d.ts.map +1 -1
  64. package/package.json +1 -1
@@ -0,0 +1,99 @@
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
+ }
@@ -0,0 +1,63 @@
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
+ }
@@ -0,0 +1,92 @@
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
+ }
@@ -0,0 +1,34 @@
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
package/bin/harness.js CHANGED
@@ -36,9 +36,16 @@ const {
36
36
  knowledgeImport,
37
37
  knowledgeDecay,
38
38
  knowledgeStats,
39
+ knowledgeUpsert,
40
+ knowledgeSyncStatus,
41
+ knowledgeSyncRag,
39
42
  failureList,
40
43
  failureStats,
41
44
  failureClear,
45
+ postevalPlan,
46
+ analyzeSessions,
47
+ updateUserModel,
48
+ release,
42
49
  } = require('../dist/cli/commands/index');
43
50
 
44
51
  const program = new Command();
@@ -353,6 +360,11 @@ program
353
360
  .option('--sources <sources>', '导入源(逗号分隔: code,git,docs)')
354
361
  .option('--limit <n>', '结果数量限制', '20')
355
362
  .option('--reset', '重置导入状态', false)
363
+ .option('--scope <scope>', '知识范围(用于 upsert 去重)')
364
+ .option('--title <title>', '知识标题(用于 upsert)')
365
+ .option('--content <content>', '知识内容 Markdown(用于 upsert)')
366
+ .option('--file <path>', '从文件读取内容(用于 upsert)')
367
+ .option('--source <source>', '知识来源 (analyst/cli/design)', 'cli')
356
368
  .option('--json', 'JSON 格式输出', false)
357
369
  .action(async (subcommand, arg, options) => {
358
370
  const opts = { projectPath: options.projectPath, json: options.json };
@@ -378,6 +390,24 @@ program
378
390
  case 'st':
379
391
  await knowledgeStats(opts);
380
392
  break;
393
+ case 'sync-rag':
394
+ await knowledgeSyncRag(opts);
395
+ break;
396
+ case 'sync-status':
397
+ case 'sync':
398
+ await knowledgeSyncStatus(opts);
399
+ break;
400
+ case 'upsert':
401
+ case 'up':
402
+ await knowledgeUpsert({
403
+ scope: options.scope || '',
404
+ title: options.title || '',
405
+ content: options.content || '',
406
+ file: options.file || '',
407
+ type: options.type || 'architecture',
408
+ source: options.source || 'cli',
409
+ });
410
+ break;
381
411
  default:
382
412
  // 无子命令时显示帮助
383
413
  if (!subcommand) {
@@ -424,5 +454,56 @@ program
424
454
  }
425
455
  });
426
456
 
457
+ // ========================================
458
+ // harness posteval-plan
459
+ // ========================================
460
+ program
461
+ .command('posteval-plan <planPath>')
462
+ .description('验证 plan 文件的 checklist items 是否都有对应的 staged diff')
463
+ .action(async (planPath) => {
464
+ await postevalPlan({ planPath });
465
+ });
466
+
467
+ // ========================================
468
+ // harness update-user-model
469
+ // ========================================
470
+ program
471
+ .command('update-user-model')
472
+ .description('从新对话中提取信号,更新用户思维模型(增量演化)')
473
+ .alias('uum')
474
+ .option('--json', 'JSON 格式输出', false)
475
+ .option('--dry-run', '只显示变化,不更新状态', false)
476
+ .action(async (options) => {
477
+ await updateUserModel({ json: options.json, dryRun: options.dryRun });
478
+ });
479
+
480
+ // ========================================
481
+ // harness release
482
+ // ========================================
483
+ program
484
+ .command('release')
485
+ .description('npm 发布流水线:tsc → dist 验证 → npm version → git push → npm publish → gh release。不依赖 Studio API。')
486
+ .option('--bump <type>', '版本递增类型', 'patch')
487
+ .option('--dry-run <bool>', '仅模拟执行', 'false')
488
+ .action(async (options) => {
489
+ await release({ bumpType: options.bump, dryRun: options.dryRun });
490
+ });
491
+
492
+ // ========================================
493
+ // harness analyze-sessions
494
+ // ========================================
495
+ program
496
+ .command('analyze-sessions')
497
+ .description('分析 Claude Code 对话,挖掘纠正模式和高频概念,生成规则候选')
498
+ .alias('analyze')
499
+ .option('-d, --days <n>', '分析最近 N 天的会话', '7')
500
+ .option('--json', 'JSON 格式输出', false)
501
+ .action(async (options) => {
502
+ await analyzeSessions({
503
+ days: parseInt(options.days, 10),
504
+ json: options.json,
505
+ });
506
+ });
507
+
427
508
  // 解析命令行参数
428
509
  program.parse();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * harness analyze-sessions — 对话模式发现引擎
3
+ *
4
+ * 扫描 transcript 文件,不预设关键词,挖掘:
5
+ * 1. 纠正信号(用户反复指出的问题)
6
+ * 2. 高频 N-gram(跨会话重复出现的概念)
7
+ * 3. 规则缺口(高频模式未被现有 memory 覆盖)
8
+ *
9
+ * 输出候选规则建议,供用户审核后写入 memory。
10
+ */
11
+ export interface AnalyzeSessionsOptions {
12
+ days?: number;
13
+ projectPath?: string;
14
+ json?: boolean;
15
+ }
16
+ export declare function analyzeSessions(options: AnalyzeSessionsOptions): Promise<void>;
17
+ //# sourceMappingURL=analyze-sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-sessions.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/analyze-sessions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAmDD,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCpF"}