harness-everything 0.2.0

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 (169) hide show
  1. package/AGENTS.md +2 -0
  2. package/BENCHMARK_SOP.md +62 -0
  3. package/CHANGELOG.md +107 -0
  4. package/Dockerfile +18 -0
  5. package/LICENSE +201 -0
  6. package/README.md +197 -0
  7. package/VERIFICATION.md +347 -0
  8. package/bin/cli.js +97 -0
  9. package/build-multi-agent-system/AGENTS.md +50 -0
  10. package/build-multi-agent-system/README.md +63 -0
  11. package/build-multi-agent-system/SKILL.md +68 -0
  12. package/create-agent-launcher/SKILL.md +48 -0
  13. package/create-agent-launcher/examples/csharp-monorepo.md +123 -0
  14. package/create-agent-launcher/examples/typescript-cli.md +126 -0
  15. package/create-agent-launcher/guidelines/platform-claude.md +77 -0
  16. package/create-agent-launcher/guidelines/platform-codex.md +146 -0
  17. package/create-agent-launcher/guidelines/platform-copilot.md +74 -0
  18. package/create-agent-launcher/guidelines/platform-gemini.md +101 -0
  19. package/create-agent-launcher/guidelines/role-capabilities.md +108 -0
  20. package/create-agent-launcher/templates/README.md +17 -0
  21. package/create-agent-launcher/templates/agent-launcher.md +68 -0
  22. package/create-agent-launcher/templates/backend-developer-csharp.md +56 -0
  23. package/create-agent-launcher/templates/backend-developer-polyglot.md +62 -0
  24. package/create-agent-launcher/templates/backend-developer-ts.md +57 -0
  25. package/create-agent-launcher/templates/memory-keeper.md +37 -0
  26. package/create-agent-launcher/templates/requirement-analyzer.md +66 -0
  27. package/create-agent-launcher/templates/specialist-agent.md +34 -0
  28. package/create-agent-launcher/templates/task-verifier.md +56 -0
  29. package/create-agent-launcher/templates/tool-maker.md +33 -0
  30. package/create-agent-launcher/workflows/01-init.md +22 -0
  31. package/create-agent-launcher/workflows/02-analysis.md +26 -0
  32. package/create-agent-launcher/workflows/03-generation.md +29 -0
  33. package/create-agent-launcher/workflows/04-launcher.md +20 -0
  34. package/docker-verify.sh +266 -0
  35. package/docs/architecture.md +95 -0
  36. package/docs/philosophy.md +45 -0
  37. package/docs/reflection.md +34 -0
  38. package/docs/routing.md +45 -0
  39. package/docs/workflows/build-multi-agent-system.md +62 -0
  40. package/docs/workflows/create-agent-launcher.md +62 -0
  41. package/docs/workflows/environment-detection.md +63 -0
  42. package/docs/workflows/eval-harness.md +57 -0
  43. package/docs/workflows/fable-discipline.md +59 -0
  44. package/docs/workflows/fable-mode.md +32 -0
  45. package/docs/workflows/git-commit.md +62 -0
  46. package/docs/workflows/grill-me.md +62 -0
  47. package/docs/workflows/grill-with-docs.md +62 -0
  48. package/docs/workflows/harness-everything.md +92 -0
  49. package/docs/workflows/improve-codebase-architecture.md +60 -0
  50. package/docs/workflows/install-cognitive-os.md +60 -0
  51. package/docs/workflows/repo-docs.md +59 -0
  52. package/docs/workflows/rewrite-commits.md +60 -0
  53. package/docs/workflows/security-review.md +58 -0
  54. package/docs/workflows/self-evolve.md +59 -0
  55. package/docs/workflows/skill-creator.md +75 -0
  56. package/docs/workflows/skill-style.md +64 -0
  57. package/docs/workflows/tdd.md +39 -0
  58. package/docs/workflows/todo-driven-workflow.md +36 -0
  59. package/docs/workflows/using-git-worktrees.md +63 -0
  60. package/docs/workflows/verification-loop.md +69 -0
  61. package/docs/workflows/verify-before-claim.md +60 -0
  62. package/docs/workflows/zoom-out.md +62 -0
  63. package/environment-detection/SKILL.md +77 -0
  64. package/environment-detection/scripts/preflight.js +93 -0
  65. package/eval-framework/behavioral-test.js +105 -0
  66. package/eval-framework/cases/case1.json +5 -0
  67. package/eval-framework/cases/case2.json +5 -0
  68. package/eval-framework/cases/case3.json +5 -0
  69. package/eval-framework/cases/case4-zh-tier2.json +5 -0
  70. package/eval-framework/cases/case5-zh-tier3.json +5 -0
  71. package/eval-framework/mechanism-test.js +251 -0
  72. package/eval-framework/runner.js +56 -0
  73. package/eval-harness/SKILL.md +59 -0
  74. package/eval-harness/scripts/evaluate.js +61 -0
  75. package/fable-discipline/SKILL.md +38 -0
  76. package/fable-mode/CONTRACT-FORMAT.md +51 -0
  77. package/fable-mode/SKILL.md +51 -0
  78. package/fable-mode/agents/fable-orchestrator.md +92 -0
  79. package/fable-mode/agents/fable-verifier.md +32 -0
  80. package/fable-mode/agents/fable-worker-haiku.md +48 -0
  81. package/fable-mode/agents/fable-worker-sonnet.md +49 -0
  82. package/fable-mode/execution-guardrails/SKILL.md +87 -0
  83. package/fable-mode/fable-haiku/SKILL.md +48 -0
  84. package/git-commit/SKILL.md +55 -0
  85. package/git-commit/guides/ANGULAR_STYLE.md +61 -0
  86. package/git-commit/guides/COMMIT_GENERATION.md +24 -0
  87. package/git-commit/guides/LANGUAGE_DETECTION.md +16 -0
  88. package/git-commit/guides/MAIN_REPO.md +15 -0
  89. package/git-commit/guides/SUBMODULES.md +27 -0
  90. package/grill-me/SKILL.md +36 -0
  91. package/grill-with-docs/ADR-FORMAT.md +47 -0
  92. package/grill-with-docs/CONTEXT-FORMAT.md +63 -0
  93. package/grill-with-docs/SKILL.md +99 -0
  94. package/harness-everything/SKILL.md +141 -0
  95. package/harness-everything/examples/README.md +9 -0
  96. package/harness-everything/examples/todo-workflow.md +22 -0
  97. package/harness-everything/scripts/bootstrap.js +140 -0
  98. package/harness-everything/scripts/self-heal.js +189 -0
  99. package/harness-everything/scripts/tier-router.js +219 -0
  100. package/harness-everything/scripts/todo-cli.js +157 -0
  101. package/harness-everything/scripts/verify-gate.js +31 -0
  102. package/hooks/hooks.json +155 -0
  103. package/hooks/scripts/atomic-commit-check.js +68 -0
  104. package/hooks/scripts/boundary-guard.js +55 -0
  105. package/hooks/scripts/context-compact.js +114 -0
  106. package/hooks/scripts/contract-test.js +79 -0
  107. package/hooks/scripts/depth-guard.js +65 -0
  108. package/hooks/scripts/lib/harness-state.js +202 -0
  109. package/hooks/scripts/lib/platforms/claude.js +42 -0
  110. package/hooks/scripts/lib/platforms/codex.js +46 -0
  111. package/hooks/scripts/lib/platforms/continue.js +41 -0
  112. package/hooks/scripts/lib/platforms/copilot.js +41 -0
  113. package/hooks/scripts/lib/platforms/cursor.js +41 -0
  114. package/hooks/scripts/lib/platforms/hermes.js +32 -0
  115. package/hooks/scripts/lib/platforms/index.js +17 -0
  116. package/hooks/scripts/lib/platforms/worktrees.js +20 -0
  117. package/hooks/scripts/reset-circuit-breaker.js +30 -0
  118. package/hooks/scripts/rule-of-3-tracker.js +91 -0
  119. package/hooks/scripts/rule-of-3.js +176 -0
  120. package/hooks/scripts/state-persist.js +99 -0
  121. package/hooks/scripts/stop-gate.js +80 -0
  122. package/hooks/scripts/subagent-scope-guard.js +88 -0
  123. package/improve-codebase-architecture/SKILL.md +44 -0
  124. package/improve-codebase-architecture/guides/DEEPENING.md +37 -0
  125. package/improve-codebase-architecture/guides/HTML-REPORT.md +123 -0
  126. package/improve-codebase-architecture/guides/INTERFACE-DESIGN.md +44 -0
  127. package/improve-codebase-architecture/guides/LANGUAGE.md +53 -0
  128. package/install-cognitive-os/SKILL.md +95 -0
  129. package/package.json +22 -0
  130. package/repo-docs/SKILL.md +45 -0
  131. package/repo-docs/templates/agents-template.md +264 -0
  132. package/repo-docs/templates/knowledge-agents-template.md +50 -0
  133. package/repo-docs/templates/knowledge-readme-template.md +83 -0
  134. package/repo-docs/templates/manual-recon.md +117 -0
  135. package/repo-docs/templates/multi-skills-readme-template.md +72 -0
  136. package/repo-docs/templates/product-agents-template.md +78 -0
  137. package/repo-docs/templates/product-readme-template.md +95 -0
  138. package/repo-docs/templates/readme-template.md +136 -0
  139. package/repo-docs/templates/single-skill-readme-template.md +70 -0
  140. package/repo-docs/templates/skills-agents-template.md +60 -0
  141. package/rewrite-commits/SKILL.md +32 -0
  142. package/scripts/installer.js +649 -0
  143. package/scripts/lib/advisory-text.js +163 -0
  144. package/scripts/lib/claude-hooks.js +71 -0
  145. package/scripts/lib/gitignore.js +58 -0
  146. package/scripts/lib/manifest.js +82 -0
  147. package/scripts/lib/prompts.js +241 -0
  148. package/scripts/lib/skills.js +172 -0
  149. package/scripts/lib/workspace.js +28 -0
  150. package/security-review/SKILL.md +511 -0
  151. package/self-evolve/SKILL.md +54 -0
  152. package/self-evolve/scripts/persist-memory.js +40 -0
  153. package/self-evolve/scripts/self-regression.js +113 -0
  154. package/skill-creator/SKILL.md +91 -0
  155. package/skill-creator/references/quality-principles.md +81 -0
  156. package/skill-creator/references/testing-workflow.md +32 -0
  157. package/skill-style/SKILL.md +48 -0
  158. package/skill-style/STYLE.md +37 -0
  159. package/tdd/SKILL.md +53 -0
  160. package/tdd/guides/deep-modules.md +33 -0
  161. package/tdd/guides/interface-design.md +31 -0
  162. package/tdd/guides/mocking.md +59 -0
  163. package/tdd/guides/refactoring.md +10 -0
  164. package/tdd/guides/tests.md +61 -0
  165. package/todo-driven-workflow/SKILL.md +54 -0
  166. package/using-git-worktrees/SKILL.md +161 -0
  167. package/verification-loop/SKILL.md +138 -0
  168. package/verify-before-claim/SKILL.md +86 -0
  169. package/zoom-out/SKILL.md +90 -0
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Boundary Guard (PreToolUse: Grep|Glob|Read)
4
+ * Hard-blocks two concrete sources of context bloat / hallucination risk:
5
+ * 1. Reading a large file whole instead of a scoped chunk (offset/limit).
6
+ * 2. Searching inside known noise directories (node_modules, .git, build
7
+ * output, etc.) that produce huge, low-signal result sets.
8
+ * Fails open on any parse/lookup error - never blocks due to a bug here.
9
+ */
10
+ const fs = require('fs');
11
+
12
+ const MAX_BYTES_WITHOUT_LIMIT = 512 * 1024; // 512KB
13
+ const NOISE_DIRS = ['node_modules', '.git', 'dist', 'build', 'out', '.next', 'target', 'vendor', '__pycache__', '.venv', 'coverage'];
14
+
15
+ let inputData = '';
16
+ process.stdin.on('data', chunk => { inputData += chunk; });
17
+ process.stdin.on('end', () => {
18
+ try {
19
+ const payload = JSON.parse(inputData);
20
+ const toolName = payload.tool_name;
21
+ const input = payload.tool_input || {};
22
+
23
+ if (toolName === 'Read') {
24
+ const filePath = input.file_path;
25
+ if (filePath && !input.limit && fs.existsSync(filePath)) {
26
+ const stat = fs.statSync(filePath);
27
+ if (stat.isFile() && stat.size > MAX_BYTES_WITHOUT_LIMIT) {
28
+ console.error(`[Boundary Guard] BLOCKED: "${filePath}" is ${(stat.size / 1024).toFixed(0)}KB.`);
29
+ console.error(`Reading a file this large in one call risks context bloat and "lost in the middle" degradation.`);
30
+ console.error(`Re-issue the Read with an explicit "offset"/"limit" to pull a targeted slice, or use Grep to locate the relevant section first.`);
31
+ process.exit(2);
32
+ }
33
+ }
34
+ }
35
+
36
+ if (toolName === 'Grep' || toolName === 'Glob') {
37
+ const searchPath = input.path || '';
38
+ const pattern = input.pattern || '';
39
+ const hitsNoiseDir = NOISE_DIRS.some(dir => {
40
+ const asPathSegment = new RegExp(`(^|[\\\\/])${dir}($|[\\\\/])`);
41
+ return asPathSegment.test(searchPath) || asPathSegment.test(pattern);
42
+ });
43
+ if (hitsNoiseDir) {
44
+ console.error(`[Boundary Guard] BLOCKED: search targets a noise directory (${NOISE_DIRS.join(', ')}).`);
45
+ console.error(`These produce huge, low-signal result sets and rarely contain what you're actually looking for.`);
46
+ console.error(`Scope the search to source directories instead, or state explicitly why this directory must be searched.`);
47
+ process.exit(2);
48
+ }
49
+ }
50
+
51
+ process.exit(0);
52
+ } catch (err) {
53
+ process.exit(0);
54
+ }
55
+ });
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ const { execSync } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
6
+
7
+ function main(payload) {
8
+ try {
9
+ // Let's run a quick Git check to estimate context size from modified files
10
+ const statusRaw = execSync('git status --porcelain', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
11
+ if (!statusRaw) {
12
+ process.exit(0);
13
+ }
14
+
15
+ const lines = statusRaw.split('\n').filter(Boolean);
16
+ const modifiedCount = lines.length;
17
+
18
+ let totalDiffLines = 0;
19
+ try {
20
+ const diffStat = execSync('git diff --numstat', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
21
+ if (diffStat) {
22
+ diffStat.split('\n').forEach(line => {
23
+ const [added, deleted] = line.split('\t');
24
+ if (added && added !== '-' && deleted && deleted !== '-') {
25
+ totalDiffLines += parseInt(added) + parseInt(deleted);
26
+ }
27
+ });
28
+ }
29
+ } catch (err) {
30
+ // Ignore git errors
31
+ }
32
+
33
+ // Thresholds for warning
34
+ const HIGH_FILE_LIMIT = 6;
35
+ const HIGH_LINE_LIMIT = 400;
36
+
37
+ const overThreshold = modifiedCount >= HIGH_FILE_LIMIT || totalDiffLines >= HIGH_LINE_LIMIT;
38
+
39
+ // Throttle: this hook fires on EVERY Bash/Edit/Write call, and a warning
40
+ // about context bloat that repeats on every call becomes context bloat
41
+ // itself (and trains the model to ignore hook output). Warn on the first
42
+ // crossing, then only every 10th call while still over - or when the diff
43
+ // has grown another 50% since the last warning.
44
+ const stateFile = path.join(getSessionDir(getWorkspaceRoot(), payload && payload.session_id), 'context-compact-state.json');
45
+ let state = { overCount: 0, lastWarnedLines: 0 };
46
+ try {
47
+ if (fs.existsSync(stateFile)) state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
48
+ } catch (err) { /* use default */ }
49
+
50
+ if (!overThreshold) {
51
+ if (state.overCount > 0) {
52
+ try {
53
+ fs.writeFileSync(stateFile, JSON.stringify({ overCount: 0, lastWarnedLines: 0 }), 'utf8');
54
+ } catch (err) { /* ignore */ }
55
+ }
56
+ process.exit(0);
57
+ }
58
+
59
+ state.overCount += 1;
60
+ const onBeat = state.overCount === 1 || state.overCount % 10 === 0;
61
+ const grewALot = totalDiffLines >= (state.lastWarnedLines || 0) * 1.5 && totalDiffLines > state.lastWarnedLines;
62
+ const shouldWarn = onBeat || grewALot;
63
+ if (shouldWarn) state.lastWarnedLines = totalDiffLines;
64
+ try {
65
+ fs.writeFileSync(stateFile, JSON.stringify(state), 'utf8');
66
+ } catch (err) { /* ignore */ }
67
+
68
+ if (shouldWarn) {
69
+ // Plain stdout on exit 0 is only surfaced to Claude for UserPromptSubmit/
70
+ // SessionStart - for PreToolUse it's written to a debug log Claude never
71
+ // sees. To actually get an advisory (non-blocking) message in front of
72
+ // Claude here, it has to go through hookSpecificOutput.additionalContext.
73
+ const message = [
74
+ `Harness OS - Context Bloat Warning`,
75
+ `- Modified Files: ${modifiedCount}`,
76
+ `- Accumulated Diff: ${totalDiffLines} lines`,
77
+ ``,
78
+ `Caution: Large number of modified files or big diffs will bloat the Agent context window, leading to:`,
79
+ ` 1. "Lost in the Middle" reasoning degradation`,
80
+ ` 2. Rapidly inflating token costs`,
81
+ ``,
82
+ `Recommended Action:`,
83
+ ` - Run atomic commits or stage completed files.`,
84
+ ` - Use targeted sub-folders rather than broad searches.`,
85
+ ` - Ask user to run '/compact' or '/clear' if supported.`
86
+ ].join('\n');
87
+
88
+ console.log(JSON.stringify({
89
+ hookSpecificOutput: {
90
+ hookEventName: "PreToolUse",
91
+ additionalContext: message
92
+ }
93
+ }));
94
+ }
95
+ } catch (err) {
96
+ // Fail open - never block user execution on compaction checker
97
+ }
98
+
99
+ process.exit(0);
100
+ }
101
+
102
+ let inputData = '';
103
+ const timeout = setTimeout(() => { main(null); }, 200);
104
+ process.stdin.on('data', chunk => { inputData += chunk; });
105
+ process.stdin.on('end', () => {
106
+ clearTimeout(timeout);
107
+ let payload = null;
108
+ try { payload = JSON.parse(inputData.trim()); } catch (err) { /* no payload */ }
109
+ main(payload);
110
+ });
111
+ process.stdin.on('error', () => {
112
+ clearTimeout(timeout);
113
+ main(null);
114
+ });
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Contract Test (PostToolUse: Bash|PowerShell)
4
+ * Resolves fable-mode stage contracts. fable-orchestrator's discipline says
5
+ * every stage check must be "re-run or spot-checked... before building on
6
+ * its output" - but a verbal claim of "I re-ran it" isn't auditable. So the
7
+ * orchestrator now writes .claude/harness-state/contracts/<stageId>.json with
8
+ * status:"pending" and the exact checkCommand BEFORE running it. This hook
9
+ * watches every shell call; when the command matches a pending contract's
10
+ * checkCommand verbatim, it records pass/fail into that manifest using the
11
+ * same exitCode-or-stderr heuristic as rule-of-3-tracker.js.
12
+ * Contracts are shared across sessions, not scoped to one - the orchestrator
13
+ * writes them proactively (not in response to a hook message) and has no
14
+ * clean way to learn its own session_id, so a collision only matters if two
15
+ * fable-mode orchestrations run in the same repo at once.
16
+ * Contracts are audit trail, not synchronous enforcement - EXCEPT when a
17
+ * check this hook just resolved comes back FAILED, it surfaces that loudly
18
+ * (exit 2) so silently building on a failed stage doesn't slip through.
19
+ * Fails open on any parse/lookup error.
20
+ */
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { getWorkspaceRoot, getStateRoot } = require('./lib/harness-state');
24
+
25
+ let inputData = '';
26
+ process.stdin.on('data', chunk => { inputData += chunk; });
27
+ process.stdin.on('end', () => {
28
+ try {
29
+ const payload = JSON.parse(inputData);
30
+ if (payload.tool_name !== 'Bash' && payload.tool_name !== 'PowerShell') process.exit(0);
31
+
32
+ const command = ((payload.tool_input && payload.tool_input.command) || '').trim();
33
+ if (!command) process.exit(0);
34
+
35
+ const contractsDir = path.join(getStateRoot(getWorkspaceRoot()), 'contracts');
36
+ if (!fs.existsSync(contractsDir)) process.exit(0);
37
+
38
+ const toolResponse = payload.tool_response || {};
39
+ const stdout = toolResponse.stdout ?? payload.stdout ?? '';
40
+ const stderr = toolResponse.stderr ?? payload.stderr ?? '';
41
+ const rawExitCode = toolResponse.exitCode ?? toolResponse.exit_code ?? payload.exitCode;
42
+ const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
43
+ const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
44
+ const failed = (exitCode !== undefined && exitCode !== 0) || (exitCode === undefined && stderrSignal);
45
+
46
+ const files = fs.readdirSync(contractsDir).filter(f => f.endsWith('.json'));
47
+ let firstFailure = null;
48
+
49
+ for (const file of files) {
50
+ const manifestPath = path.join(contractsDir, file);
51
+ let manifest;
52
+ try {
53
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
54
+ } catch (e) {
55
+ continue;
56
+ }
57
+ if (manifest.status !== 'pending') continue;
58
+ if (!manifest.checkCommand || manifest.checkCommand.trim() !== command) continue;
59
+
60
+ manifest.status = failed ? 'fail' : 'pass';
61
+ manifest.evidence = ((stderrSignal ? stderr : stdout) || '').slice(-500).trim();
62
+ manifest.verifiedAt = new Date().toISOString();
63
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
64
+
65
+ if (failed && !firstFailure) firstFailure = manifest;
66
+ }
67
+
68
+ if (firstFailure) {
69
+ console.error(`[Contract Test] Stage "${firstFailure.stageId}" FAILED its named check: ${firstFailure.checkCommand}`);
70
+ console.error(`Evidence: ${firstFailure.evidence || '(no output captured)'}`);
71
+ console.error(`Do not build on this stage's output until it passes. Artifact: ${firstFailure.outputPath || '(no path recorded)'}`);
72
+ process.exit(2);
73
+ }
74
+
75
+ process.exit(0);
76
+ } catch (err) {
77
+ process.exit(0);
78
+ }
79
+ });
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Depth Guard (PreToolUse: Edit|Write)
4
+ * Claude Code already refuses to Edit a file that wasn't Read first in the
5
+ * same session - that's native behavior, not something this hook needs to
6
+ * re-check. The gap is Write: it happily overwrites an existing file with
7
+ * no prior Read at all. This hook closes that gap: blocks Write on a file
8
+ * that already exists on disk unless a Read of that exact path shows up
9
+ * earlier in this session's transcript.
10
+ * Fails open on any parse/lookup error - never blocks due to a bug here.
11
+ */
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ let inputData = '';
16
+ process.stdin.on('data', chunk => { inputData += chunk; });
17
+ process.stdin.on('end', () => {
18
+ try {
19
+ const payload = JSON.parse(inputData);
20
+ if (payload.tool_name !== 'Write') {
21
+ process.exit(0);
22
+ }
23
+
24
+ const filePath = payload.tool_input && payload.tool_input.file_path;
25
+ if (!filePath || !fs.existsSync(filePath)) {
26
+ // New file - nothing to have read first.
27
+ process.exit(0);
28
+ }
29
+
30
+ const transcriptPath = payload.transcript_path;
31
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) {
32
+ // Can't verify either way - fail open.
33
+ process.exit(0);
34
+ }
35
+
36
+ const target = path.resolve(filePath).toLowerCase();
37
+ const lines = fs.readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean);
38
+
39
+ const wasRead = lines.some(line => {
40
+ try {
41
+ const entry = JSON.parse(line);
42
+ const content = entry && entry.message && entry.message.content;
43
+ if (!Array.isArray(content)) return false;
44
+ return content.some(block => {
45
+ if (!block || block.type !== 'tool_use' || block.name !== 'Read') return false;
46
+ const readPath = block.input && block.input.file_path;
47
+ return readPath && path.resolve(readPath).toLowerCase() === target;
48
+ });
49
+ } catch (e) {
50
+ return false;
51
+ }
52
+ });
53
+
54
+ if (!wasRead) {
55
+ console.error(`[Depth Guard] BLOCKED: "${filePath}" already exists but hasn't been Read in this session.`);
56
+ console.error(`Write() overwrites the whole file - doing that blind risks destroying content you never looked at.`);
57
+ console.error(`Read the file first (even a quick pass), then retry the Write, or use Edit for a targeted change.`);
58
+ process.exit(2);
59
+ }
60
+
61
+ process.exit(0);
62
+ } catch (err) {
63
+ process.exit(0);
64
+ }
65
+ });
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ // Shared path resolution for Harness runtime state (hook JSON, circuit-breaker
3
+ // counters, handoff/verification timestamps, etc). Lives under the active
4
+ // platform's own harness-everything/ subfolder (see each module's
5
+ // getStateDir() in hooks/scripts/lib/platforms/) - a sibling of that same
6
+ // platform's install manifest, and (for Claude only) its skill copies.
7
+ //
8
+ // For Claude Code that's `<repo root>/.claude/harness-everything/state/`,
9
+ // keyed by session_id under `sessions/<id>/` so two sessions open on the same
10
+ // repo never share (and stomp) each other's edit/verify timestamps or
11
+ // breaker counts. Invocations with no session_id (manual terminal runs,
12
+ // VERIFICATION.md recipes) fall into a fixed `sessions/default/` bucket
13
+ // rather than a random one, so manual testing stays predictable.
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const CURRENT_SESSION_FILE = 'current-session';
18
+ const DEFAULT_SESSION = 'default';
19
+
20
+ function getWorkspaceRoot() {
21
+ let dir = path.resolve(process.cwd());
22
+ while (dir !== path.parse(dir).root) {
23
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
24
+ dir = path.dirname(dir);
25
+ }
26
+ return process.cwd();
27
+ }
28
+
29
+ function detectActivePlatform(wsRoot) {
30
+ const root = wsRoot || getWorkspaceRoot();
31
+
32
+ // 1. Check explicit environment variables
33
+ if (process.env.CLAUDE === '1' || process.env.CLAUDE) return 'claude';
34
+ if (process.env.CURSOR === '1' || process.env.CURSOR) return 'cursor';
35
+ if (process.env.COPILOT === '1' || process.env.COPILOT) return 'copilot';
36
+ if (process.env.CONTINUE === '1' || process.env.CONTINUE) return 'continue';
37
+
38
+ // 2. Check parent/environment standard indicators
39
+ if (process.env.TERM_PROGRAM === 'vscode') {
40
+ if (fs.existsSync(path.join(root, '.cursorrules'))) {
41
+ return 'cursor';
42
+ }
43
+ if (fs.existsSync(path.join(root, '.github', 'copilot-instructions.md'))) {
44
+ return 'copilot';
45
+ }
46
+ return 'copilot'; // Default fallback for standard VS Code terminal
47
+ }
48
+
49
+ // 3. Fallback to detecting workspace configuration presence
50
+ if (fs.existsSync(path.join(root, '.claude', 'settings.json'))) return 'claude';
51
+ if (fs.existsSync(path.join(root, '.cursorrules'))) return 'cursor';
52
+ if (fs.existsSync(path.join(root, '.github', 'copilot-instructions.md'))) return 'copilot';
53
+ if (fs.existsSync(path.join(root, '.continue'))) return 'continue';
54
+ if (fs.existsSync(path.join(root, 'AGENTS.md'))) return 'codex';
55
+ if (fs.existsSync(path.join(root, '.hermes.md'))) return 'hermes';
56
+
57
+ return 'claude'; // Default global fallback
58
+ }
59
+
60
+ function ensureHarnessStateIgnored(rootPath) {
61
+ const wsRoot = rootPath || getWorkspaceRoot();
62
+ const gitignorePath = path.join(wsRoot, '.gitignore');
63
+
64
+ try {
65
+ let content = '';
66
+ if (fs.existsSync(gitignorePath)) {
67
+ content = fs.readFileSync(gitignorePath, 'utf8');
68
+ }
69
+
70
+ const lines = content.split(/\r?\n/);
71
+ const patternsToAdd = [];
72
+
73
+ // Load and collect patterns & matching rules dynamically from all platforms
74
+ const allPlatforms = require('./platforms');
75
+ const dynamicPatterns = [];
76
+ const patternToPlatformMap = new Map();
77
+
78
+ // Forcefully ensure active platform's state directory pattern is included
79
+ const activePlatformName = detectActivePlatform(wsRoot);
80
+ const activePlatform = allPlatforms.find(p => p.name === activePlatformName) || allPlatforms.find(p => p.name === 'claude');
81
+
82
+ if (activePlatform && typeof activePlatform.getStateDir === 'function') {
83
+ const activeStateDir = activePlatform.getStateDir(wsRoot);
84
+ const relativeStateDir = path.relative(wsRoot, activeStateDir).replace(/\\/g, '/') + '/';
85
+ dynamicPatterns.push(relativeStateDir);
86
+ patternToPlatformMap.set(relativeStateDir, activePlatform);
87
+ }
88
+
89
+ for (const platform of allPlatforms) {
90
+ const patterns = platform.getIgnorePatterns(wsRoot);
91
+ for (const pattern of patterns) {
92
+ if (!dynamicPatterns.includes(pattern)) {
93
+ dynamicPatterns.push(pattern);
94
+ }
95
+ patternToPlatformMap.set(pattern, platform);
96
+ }
97
+ }
98
+
99
+ for (const pattern of dynamicPatterns) {
100
+ const platform = patternToPlatformMap.get(pattern);
101
+ const isIgnored = lines.some(line => {
102
+ const trimmed = line.trim();
103
+ // Delegate matching logic to the specific platform module if available
104
+ if (platform && typeof platform.isMatch === 'function') {
105
+ return platform.isMatch(pattern, trimmed);
106
+ }
107
+ return trimmed === pattern || trimmed === pattern.slice(0, -1);
108
+ });
109
+
110
+ if (!isIgnored) {
111
+ patternsToAdd.push(pattern);
112
+ }
113
+ }
114
+
115
+ if (patternsToAdd.length > 0) {
116
+ const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
117
+ fs.appendFileSync(gitignorePath, separator + patternsToAdd.join('\n') + '\n', 'utf8');
118
+ }
119
+ } catch (err) {
120
+ // Fail silently to avoid breaking execution if .gitignore is write-locked
121
+ }
122
+ }
123
+
124
+ function getStateRoot(root) {
125
+ const wsRoot = root || getWorkspaceRoot();
126
+ const activePlatformName = detectActivePlatform(wsRoot);
127
+ const allPlatforms = require('./platforms');
128
+ const activePlatform = allPlatforms.find(p => p.name === activePlatformName) || allPlatforms.find(p => p.name === 'claude');
129
+
130
+ if (activePlatform && typeof activePlatform.getStateDir === 'function') {
131
+ return activePlatform.getStateDir(wsRoot);
132
+ }
133
+ return path.join(wsRoot, '.claude', 'harness-state');
134
+ }
135
+
136
+ function getSessionDir(root, sessionId) {
137
+ const resolvedRoot = root || getWorkspaceRoot();
138
+ ensureHarnessStateIgnored(resolvedRoot);
139
+ const dir = path.join(getStateRoot(resolvedRoot), 'sessions', sessionId || DEFAULT_SESSION);
140
+ fs.mkdirSync(dir, { recursive: true });
141
+ return dir;
142
+ }
143
+
144
+ function listSessionDirs(root) {
145
+ const sessionsRoot = path.join(getStateRoot(root), 'sessions');
146
+ try {
147
+ return fs.readdirSync(sessionsRoot, { withFileTypes: true })
148
+ .filter(e => e.isDirectory())
149
+ .map(e => path.join(sessionsRoot, e.name));
150
+ } catch (err) {
151
+ return [];
152
+ }
153
+ }
154
+
155
+ function writeCurrentSession(root, sessionId) {
156
+ if (!sessionId) return;
157
+ try {
158
+ const wsRoot = root || getWorkspaceRoot();
159
+ ensureHarnessStateIgnored(wsRoot);
160
+ const stateRoot = getStateRoot(wsRoot);
161
+ fs.mkdirSync(stateRoot, { recursive: true });
162
+ fs.writeFileSync(path.join(stateRoot, CURRENT_SESSION_FILE), sessionId, 'utf8');
163
+ } catch (err) {
164
+ // Best-effort - only the manual reset-circuit-breaker.js escape hatch
165
+ // depends on this, and it fails open too.
166
+ }
167
+ }
168
+
169
+ function readCurrentSession(root) {
170
+ try {
171
+ const id = fs.readFileSync(path.join(getStateRoot(root), CURRENT_SESSION_FILE), 'utf8').trim();
172
+ return id || null;
173
+ } catch (err) {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ // Bootstrap-time housekeeping: nothing purges stale session directories the
179
+ // way an OS temp dir would, so SessionStart drops ones untouched for a while.
180
+ function pruneStaleSessions(root, maxAgeMs = 14 * 24 * 60 * 60 * 1000) {
181
+ const now = Date.now();
182
+ for (const dir of listSessionDirs(root)) {
183
+ try {
184
+ if (now - fs.statSync(dir).mtimeMs > maxAgeMs) {
185
+ fs.rmSync(dir, { recursive: true, force: true });
186
+ }
187
+ } catch (err) {
188
+ // Ignore - worst case a stale dir lingers until the next prune.
189
+ }
190
+ }
191
+ }
192
+
193
+ module.exports = {
194
+ DEFAULT_SESSION,
195
+ getWorkspaceRoot,
196
+ getStateRoot,
197
+ getSessionDir,
198
+ listSessionDirs,
199
+ writeCurrentSession,
200
+ readCurrentSession,
201
+ pruneStaleSessions,
202
+ };
@@ -0,0 +1,42 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // Claude Code has no native project-skill directory this package can piggy
5
+ // back on, so everything Harness adds locally - runtime state AND the
6
+ // installed skill copies - lives under one self-contained subfolder of the
7
+ // platform's own directory: .claude/harness-everything/. That keeps it out
8
+ // of a self-invented top-level folder (the old .harness/) while still being
9
+ // exclusively ours - nothing else creates a directory literally named
10
+ // "harness-everything" inside .claude/, so it's trivial to add/remove
11
+ // without touching anything else Claude Code or the user put in .claude/.
12
+ module.exports = {
13
+ name: 'claude',
14
+ label: 'Claude Code',
15
+ getHarnessDir(workspaceRoot) {
16
+ return path.join(workspaceRoot, '.claude', 'harness-everything');
17
+ },
18
+ getStateDir(workspaceRoot) {
19
+ return path.join(this.getHarnessDir(workspaceRoot), 'state');
20
+ },
21
+ getSkillsDir(workspaceRoot) {
22
+ return path.join(this.getHarnessDir(workspaceRoot), 'skills');
23
+ },
24
+ getIgnorePatterns(workspaceRoot) {
25
+ return ['.claude/harness-everything/'];
26
+ },
27
+ isMatch(pattern, trimmedLine) {
28
+ if (pattern === '.claude/harness-everything/') {
29
+ return trimmedLine === '.claude/' ||
30
+ trimmedLine === '.claude' ||
31
+ trimmedLine === '.claude/harness-everything' ||
32
+ trimmedLine === '.claude/harness-everything/';
33
+ }
34
+ return trimmedLine === pattern || trimmedLine === pattern.slice(0, -1);
35
+ },
36
+ isInstalled(workspaceRoot, userHome, isGlobal) {
37
+ if (isGlobal) {
38
+ return fs.existsSync(path.join(userHome, '.claude', 'settings.json'));
39
+ }
40
+ return fs.existsSync(path.join(workspaceRoot, '.claude', 'settings.json'));
41
+ }
42
+ };
@@ -0,0 +1,46 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // Codex CLI's real project-scoped home is `.codex/` (it reads `.codex/skills/`
5
+ // for project skills and `.codex/config.toml` for CLI/sandbox config, walking
6
+ // up to the `.git` root exactly like AGENTS.md - see
7
+ // https://developers.openai.com/codex/config-advanced and
8
+ // https://www.agensi.io/learn/codex-cli-agents-md-complete-guide). Skills
9
+ // stay at that native `.codex/skills/` location; only bookkeeping that never
10
+ // had a home (runtime state, install manifest) converges into
11
+ // `.codex/harness-everything/`.
12
+ module.exports = {
13
+ name: 'codex',
14
+ label: 'Codex / Gemini',
15
+ getHarnessDir(workspaceRoot) {
16
+ return path.join(workspaceRoot, '.codex', 'harness-everything');
17
+ },
18
+ getStateDir(workspaceRoot) {
19
+ return path.join(this.getHarnessDir(workspaceRoot), 'state');
20
+ },
21
+ getSkillsDir(workspaceRoot) {
22
+ return path.join(workspaceRoot, '.codex', 'skills');
23
+ },
24
+ getIgnorePatterns(workspaceRoot) {
25
+ const patterns = ['.codex/harness-everything/'];
26
+ if (fs.existsSync(path.join(workspaceRoot, '.codex', 'skills'))) {
27
+ patterns.push('.codex/skills/');
28
+ }
29
+ return patterns;
30
+ },
31
+ isMatch(pattern, trimmedLine) {
32
+ if (pattern === '.codex/harness-everything/') {
33
+ return trimmedLine === '.codex/' ||
34
+ trimmedLine === '.codex' ||
35
+ trimmedLine === '.codex/harness-everything' ||
36
+ trimmedLine === '.codex/harness-everything/';
37
+ }
38
+ return trimmedLine === pattern || trimmedLine === pattern.slice(0, -1);
39
+ },
40
+ isInstalled(workspaceRoot, userHome, isGlobal) {
41
+ if (isGlobal) {
42
+ return false;
43
+ }
44
+ return fs.existsSync(path.join(workspaceRoot, 'AGENTS.md'));
45
+ }
46
+ };
@@ -0,0 +1,41 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // Continue's own skills location (.continue/skills/) is unchanged - it
5
+ // already has an established home. Only bookkeeping that never had a home
6
+ // (runtime state, install manifest) converges into .continue/harness-everything/.
7
+ module.exports = {
8
+ name: 'continue',
9
+ label: 'Continue.dev',
10
+ getHarnessDir(workspaceRoot) {
11
+ return path.join(workspaceRoot, '.continue', 'harness-everything');
12
+ },
13
+ getStateDir(workspaceRoot) {
14
+ return path.join(this.getHarnessDir(workspaceRoot), 'state');
15
+ },
16
+ getSkillsDir(workspaceRoot) {
17
+ return path.join(workspaceRoot, '.continue', 'skills');
18
+ },
19
+ getIgnorePatterns(workspaceRoot) {
20
+ const patterns = ['.continue/harness-everything/'];
21
+ if (fs.existsSync(path.join(workspaceRoot, '.continue', 'skills'))) {
22
+ patterns.push('.continue/skills/');
23
+ }
24
+ return patterns;
25
+ },
26
+ isMatch(pattern, trimmedLine) {
27
+ if (pattern === '.continue/harness-everything/') {
28
+ return trimmedLine === '.continue/' ||
29
+ trimmedLine === '.continue' ||
30
+ trimmedLine === '.continue/harness-everything' ||
31
+ trimmedLine === '.continue/harness-everything/';
32
+ }
33
+ return trimmedLine === pattern || trimmedLine === pattern.slice(0, -1);
34
+ },
35
+ isInstalled(workspaceRoot, userHome, isGlobal) {
36
+ if (isGlobal) {
37
+ return fs.existsSync(path.join(userHome, '.continue', 'rules', 'harness.md'));
38
+ }
39
+ return fs.existsSync(path.join(workspaceRoot, '.continue', 'rules', 'harness.md'));
40
+ }
41
+ };