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,41 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // Copilot's own skills location (.github/skills/) is unchanged - it already
5
+ // has an established home. Only bookkeeping that never had a home (runtime
6
+ // state, install manifest) converges into .github/harness-everything/.
7
+ module.exports = {
8
+ name: 'copilot',
9
+ label: 'GitHub Copilot',
10
+ getHarnessDir(workspaceRoot) {
11
+ return path.join(workspaceRoot, '.github', 'harness-everything');
12
+ },
13
+ getStateDir(workspaceRoot) {
14
+ return path.join(this.getHarnessDir(workspaceRoot), 'state');
15
+ },
16
+ getSkillsDir(workspaceRoot) {
17
+ return path.join(workspaceRoot, '.github', 'skills');
18
+ },
19
+ getIgnorePatterns(workspaceRoot) {
20
+ const patterns = ['.github/harness-everything/'];
21
+ if (fs.existsSync(path.join(workspaceRoot, '.github', 'skills'))) {
22
+ patterns.push('.github/skills/');
23
+ }
24
+ return patterns;
25
+ },
26
+ isMatch(pattern, trimmedLine) {
27
+ if (pattern === '.github/harness-everything/') {
28
+ return trimmedLine === '.github/' ||
29
+ trimmedLine === '.github' ||
30
+ trimmedLine === '.github/harness-everything' ||
31
+ trimmedLine === '.github/harness-everything/';
32
+ }
33
+ return trimmedLine === pattern || trimmedLine === pattern.slice(0, -1);
34
+ },
35
+ isInstalled(workspaceRoot, userHome, isGlobal) {
36
+ if (isGlobal) {
37
+ return false; // Handled via global userPromptsDir check
38
+ }
39
+ return fs.existsSync(path.join(workspaceRoot, '.github', 'copilot-instructions.md'));
40
+ }
41
+ };
@@ -0,0 +1,41 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // Cursor's own skills location (.cursor/skills/) is unchanged - it already
5
+ // has an established home. Only bookkeeping that never had a home (runtime
6
+ // state, install manifest) converges into .cursor/harness-everything/.
7
+ module.exports = {
8
+ name: 'cursor',
9
+ label: 'Cursor',
10
+ getHarnessDir(workspaceRoot) {
11
+ return path.join(workspaceRoot, '.cursor', 'harness-everything');
12
+ },
13
+ getStateDir(workspaceRoot) {
14
+ return path.join(this.getHarnessDir(workspaceRoot), 'state');
15
+ },
16
+ getSkillsDir(workspaceRoot) {
17
+ return path.join(workspaceRoot, '.cursor', 'skills');
18
+ },
19
+ getIgnorePatterns(workspaceRoot) {
20
+ const patterns = ['.cursor/harness-everything/'];
21
+ if (fs.existsSync(path.join(workspaceRoot, '.cursor', 'skills'))) {
22
+ patterns.push('.cursor/skills/');
23
+ }
24
+ return patterns;
25
+ },
26
+ isMatch(pattern, trimmedLine) {
27
+ if (pattern === '.cursor/harness-everything/') {
28
+ return trimmedLine === '.cursor/' ||
29
+ trimmedLine === '.cursor' ||
30
+ trimmedLine === '.cursor/harness-everything' ||
31
+ trimmedLine === '.cursor/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, '.cursorrules'));
38
+ }
39
+ return fs.existsSync(path.join(workspaceRoot, '.cursorrules'));
40
+ }
41
+ };
@@ -0,0 +1,32 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ module.exports = {
5
+ name: 'hermes',
6
+ label: 'Hermes Agent',
7
+ getStateDir(workspaceRoot) {
8
+ return path.join(workspaceRoot, '.hermes', 'harness-state');
9
+ },
10
+ getIgnorePatterns(workspaceRoot) {
11
+ const patterns = [];
12
+ if (fs.existsSync(path.join(workspaceRoot, '.hermes', 'harness-state'))) {
13
+ patterns.push('.hermes/harness-state/');
14
+ }
15
+ return patterns;
16
+ },
17
+ isMatch(pattern, trimmedLine) {
18
+ if (pattern === '.hermes/harness-state/') {
19
+ return trimmedLine === '.hermes/' ||
20
+ trimmedLine === '.hermes' ||
21
+ trimmedLine === '.hermes/harness-state' ||
22
+ trimmedLine === '.hermes/harness-state/';
23
+ }
24
+ return trimmedLine === pattern || trimmedLine === pattern.slice(0, -1);
25
+ },
26
+ isInstalled(workspaceRoot, userHome, isGlobal) {
27
+ if (isGlobal) {
28
+ return false;
29
+ }
30
+ return fs.existsSync(path.join(workspaceRoot, '.hermes.md'));
31
+ }
32
+ };
@@ -0,0 +1,17 @@
1
+ const claude = require('./claude');
2
+ const cursor = require('./cursor');
3
+ const copilot = require('./copilot');
4
+ const codex = require('./codex');
5
+ const _continue = require('./continue');
6
+ const hermes = require('./hermes');
7
+ const worktrees = require('./worktrees');
8
+
9
+ module.exports = [
10
+ claude,
11
+ cursor,
12
+ copilot,
13
+ codex,
14
+ _continue,
15
+ hermes,
16
+ worktrees
17
+ ];
@@ -0,0 +1,20 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ module.exports = {
5
+ name: 'worktrees',
6
+ label: 'Git Worktrees',
7
+ getIgnorePatterns(workspaceRoot) {
8
+ const patterns = [];
9
+ if (fs.existsSync(path.join(workspaceRoot, '.worktrees'))) {
10
+ patterns.push('.worktrees/');
11
+ }
12
+ if (fs.existsSync(path.join(workspaceRoot, 'worktrees'))) {
13
+ patterns.push('worktrees/');
14
+ }
15
+ return patterns;
16
+ },
17
+ isInstalled() {
18
+ return false; // Not a standalone platform hook/settings installer
19
+ }
20
+ };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // Manual escape hatch for the Rule of 3 circuit breaker. Run this directly
3
+ // in your own terminal (not through the agent — Bash is blocked while the
4
+ // breaker is tripped) to clear it without waiting for a new session.
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { getWorkspaceRoot, getSessionDir, readCurrentSession } = require('./lib/harness-state');
8
+
9
+ // This is run directly by the human, not through the agent, so there's no
10
+ // hook payload to read a session_id from - resolve "the session that's
11
+ // currently blocked" via the pointer bootstrap.js wrote at its last
12
+ // SessionStart.
13
+ const root = getWorkspaceRoot();
14
+ const harnessDir = getSessionDir(root, readCurrentSession(root));
15
+ const stateFile = path.join(harnessDir, 'rule-of-3-state.json');
16
+ const reportFile = path.join(harnessDir, 'zoom-out-report.md');
17
+
18
+ if (fs.existsSync(stateFile)) {
19
+ fs.unlinkSync(stateFile);
20
+ console.log('Rule of 3 circuit breaker cleared.');
21
+ } else {
22
+ console.log('Rule of 3 circuit breaker is not currently tripped.');
23
+ }
24
+
25
+ // A human reset is a full clear: the stale reflection report belongs to the
26
+ // loop that was just abandoned.
27
+ if (fs.existsSync(reportFile)) {
28
+ fs.unlinkSync(reportFile);
29
+ console.log('Stale zoom-out report removed.');
30
+ }
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
6
+
7
+ // PostToolUse hooks in Claude Code typically receive the tool output via stdin
8
+ let inputData = '';
9
+
10
+ process.stdin.on('data', chunk => {
11
+ inputData += chunk;
12
+ });
13
+
14
+ process.stdin.on('end', () => {
15
+ try {
16
+ const payload = JSON.parse(inputData);
17
+ // Claude Code's documented PostToolUse payload nests tool output under
18
+ // tool_response rather than at the top level, and does not guarantee a
19
+ // numeric exit code field. Treating a missing/unknown exit code as a
20
+ // failure (the old `exitCode !== 0` check) flags every successful call
21
+ // as an error. Only count a call as a failure when we have positive
22
+ // evidence of one.
23
+ const toolResponse = payload.tool_response || {};
24
+ const stdout = toolResponse.stdout ?? payload.stdout ?? '';
25
+ const stderr = toolResponse.stderr ?? payload.stderr ?? '';
26
+ const rawExitCode = toolResponse.exitCode ?? toolResponse.exit_code ?? payload.exitCode;
27
+ const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
28
+
29
+ const explicitFailure = exitCode !== undefined && exitCode !== 0;
30
+ const explicitSuccess = exitCode === 0;
31
+ const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
32
+ // Without an exit code, bare stderr is not enough evidence: plenty of
33
+ // tools chat on stderr while succeeding (git, npm warnings, progress
34
+ // bars), and three identical warnings must not trip the breaker. Require
35
+ // the text to actually look like a failure.
36
+ const looksLikeError = /\b(error|fail|failed|failure|exception|fatal|panic|traceback|denied|refused|cannot|unable)\b/i.test(stderr);
37
+ const isFailure = explicitFailure || (exitCode === undefined && stderrSignal && looksLikeError);
38
+ const errorText = (stderrSignal ? stderr : stdout) || '';
39
+
40
+ const stateFile = path.join(getSessionDir(getWorkspaceRoot(), payload.session_id), 'rule-of-3-state.json');
41
+
42
+ let state = { count: 0, lastHash: null, zoomOutResolved: false, zoomOutCycles: 0, lastFailureAt: 0 };
43
+ if (fs.existsSync(stateFile)) {
44
+ state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
45
+ }
46
+
47
+ if (isFailure && errorText) {
48
+ // Hash the tail of the error to identify identical loops. Normalize
49
+ // first: timestamps, line numbers, addresses and counters vary between
50
+ // otherwise-identical failures, and an ever-changing hash would hide a
51
+ // genuine loop from the breaker.
52
+ const normalized = errorText.slice(-400)
53
+ .toLowerCase()
54
+ .replace(/0x[0-9a-f]+/g, '#')
55
+ .replace(/\d+/g, '#')
56
+ .replace(/\s+/g, ' ')
57
+ .trim();
58
+ const hash = crypto.createHash('md5').update(normalized.slice(-200)).digest('hex');
59
+
60
+ if (state.lastHash === hash) {
61
+ state.count += 1;
62
+ } else {
63
+ state.count = 1;
64
+ state.lastHash = hash;
65
+ // New signature = new problem: the zoom-out cycle budget starts over.
66
+ state.zoomOutCycles = 0;
67
+ }
68
+ // Any fresh failure re-arms the breaker. Without this, a signature that
69
+ // failed again after a success (or after a zoom-out release) would keep
70
+ // zoomOutResolved=true and the breaker could never trip on it again.
71
+ state.zoomOutResolved = false;
72
+ // rule-of-3.js only honors a zoom-out report written AFTER this moment,
73
+ // so a stale report can't unlock a later trip.
74
+ state.lastFailureAt = Date.now();
75
+
76
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
77
+ } else if (explicitSuccess) {
78
+ // Only reset on a *confirmed* zero exit code, not merely "not a failure".
79
+ if (state.count > 0 || state.zoomOutCycles > 0) {
80
+ state.count = 0;
81
+ state.zoomOutResolved = true;
82
+ state.zoomOutCycles = 0;
83
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
84
+ }
85
+ }
86
+
87
+ } catch (err) {
88
+ // silently fail
89
+ }
90
+ process.exit(0);
91
+ });
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // Rule of 3 circuit breaker (PreToolUse: Bash|PowerShell|Edit|Write).
3
+ //
4
+ // Tripping the breaker forces REFLECTION, not surrender. The most common
5
+ // agent failure mode is drilling deeper on one unverified assumption, so the
6
+ // breaker locks mutating tools and demands a zoom-out fact-check first
7
+ // (read-only tools stay available). A valid reflection report - written to
8
+ // the session's zoom-out-report.md (path given in the block message below) -
9
+ // releases the breaker again. Only a SECOND trip on the same failure
10
+ // signature - proof that reflection alone couldn't crack it - hard-locks the
11
+ // session until the Human Partner decides.
12
+ //
13
+ // State is scoped per Claude Code session_id (see lib/harness-state.js) so
14
+ // two sessions open on the same repo never share a trip count. The common
15
+ // case - nobody's breaker tripped anywhere - never has to read stdin to find
16
+ // that out; only once some session IS tripped do we pay to read the payload
17
+ // and check whether it's this one.
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { getWorkspaceRoot, getSessionDir, listSessionDirs } = require('./lib/harness-state');
21
+
22
+ const REQUIRED_SECTIONS = [
23
+ '## Goal',
24
+ '## Failed Attempts',
25
+ '## Verified Facts',
26
+ '## Diagnosis',
27
+ '## Decision'
28
+ ];
29
+
30
+ function readState(stateFile) {
31
+ if (!fs.existsSync(stateFile)) return null;
32
+ try { return JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch (err) { return null; }
33
+ }
34
+
35
+ function isTripped(state) {
36
+ return !!state && state.count >= 3 && !state.zoomOutResolved;
37
+ }
38
+
39
+ function anySessionTripped(root) {
40
+ return listSessionDirs(root).some(dir => isTripped(readState(path.join(dir, 'rule-of-3-state.json'))));
41
+ }
42
+
43
+ // The one mutation allowed while tripped: writing the reflection report.
44
+ function isReportWrite(payload, reportFile) {
45
+ if (!payload) return false;
46
+ if (payload.tool_name !== 'Write' && payload.tool_name !== 'Edit') return false;
47
+ const target = payload.tool_input && payload.tool_input.file_path;
48
+ if (!target) return false;
49
+ return path.resolve(target).toLowerCase() === path.resolve(reportFile).toLowerCase();
50
+ }
51
+
52
+ // A report only counts if it was written AFTER the failure that tripped the
53
+ // breaker (a stale report from an earlier loop can't unlock a new one), has
54
+ // every required section, and actually committed to a RESUME/ESCALATE call.
55
+ function reportIsValid(state, reportFile) {
56
+ if (!fs.existsSync(reportFile)) return false;
57
+ if (fs.statSync(reportFile).mtimeMs < (state.lastFailureAt || 0)) return false;
58
+ const text = fs.readFileSync(reportFile, 'utf8');
59
+ if (!REQUIRED_SECTIONS.every(h => text.includes(h))) return false;
60
+ return /\b(RESUME|ESCALATE)\b/.test(text);
61
+ }
62
+
63
+ function blockForReflection(state, reportFile) {
64
+ console.error(`[CRITICAL] RULE OF 3 CIRCUIT BREAKER TRIGGERED!`);
65
+ console.error(`The same failure signature has now occurred 3 times.`);
66
+ console.error(`Error Signature: ${state.lastHash}`);
67
+ console.error(`\nMutating tools (Bash/PowerShell/Edit/Write) are locked - not because you`);
68
+ console.error(`must wait for a human, but because you must stop drilling and rebuild the`);
69
+ console.error(`full picture first. Do NOT ask the Human Partner yet.`);
70
+ console.error(`\nACTION REQUIRED (reflect first):`);
71
+ console.error(`1. CEASE FIRE. No new fix attempts, no variations of the last one.`);
72
+ console.error(`2. Load the 'zoom-out' skill and follow its Reflect & Fact-Check protocol.`);
73
+ console.error(`3. Using READ-ONLY tools (Read/Grep/Glob - still available), re-verify the`);
74
+ console.error(` assumption behind each failed attempt against the actual code/logs/docs.`);
75
+ console.error(`4. Write your findings to '${reportFile}' (the ONLY write`);
76
+ console.error(` allowed right now) with sections: ## Goal, ## Failed Attempts,`);
77
+ console.error(` ## Verified Facts, ## Diagnosis, ## Decision (ending RESUME: or ESCALATE:).`);
78
+ console.error(`\nA valid report releases this breaker automatically. Escalate to the Human`);
79
+ console.error(`Partner ONLY if your Decision concludes a genuinely human call is needed`);
80
+ console.error(`(requirement conflict, architecture trade-off, destructive action, missing`);
81
+ console.error(`access) - and then present options with a recommendation, not apologies.`);
82
+ // For PreToolUse, only exit code 2 actually blocks the tool call.
83
+ // Exit 1 is a non-blocking error and Claude Code proceeds anyway.
84
+ process.exit(2);
85
+ }
86
+
87
+ function blockForHumanDecision(state) {
88
+ console.error(`[CRITICAL] RULE OF 3 CIRCUIT BREAKER TRIGGERED! (repeat trip - hard lock)`);
89
+ console.error(`Error Signature: ${state.lastHash}`);
90
+ console.error(`\nYou already completed a zoom-out reflection for this same failure signature`);
91
+ console.error(`and the fresh diagnosis failed too. This is now past reflect-and-retry:`);
92
+ console.error(`STOP and hand the decision to your Human Partner.`);
93
+ console.error(`\nPresent a DECISION REQUEST, not a plea: the goal, the verified facts, every`);
94
+ console.error(`path already falsified (including the post-reflection diagnosis), 2-3`);
95
+ console.error(`concrete options with trade-offs, and your recommendation.`);
96
+ console.error(`\nThis lock persists until the Human Partner clears it by running`);
97
+ console.error(`"npm run harness:reset" in their own terminal, or by starting a new`);
98
+ console.error(`session / running /clear (which resets it automatically on SessionStart).`);
99
+ process.exit(2);
100
+ }
101
+
102
+ function main(payload) {
103
+ try {
104
+ const root = getWorkspaceRoot();
105
+ const sessionDir = getSessionDir(root, payload && payload.session_id);
106
+ const stateFile = path.join(sessionDir, 'rule-of-3-state.json');
107
+ const reportFile = path.join(sessionDir, 'zoom-out-report.md');
108
+
109
+ const state = readState(stateFile);
110
+ if (!isTripped(state)) {
111
+ process.exit(0);
112
+ }
113
+
114
+ if (isReportWrite(payload, reportFile)) {
115
+ process.exit(0);
116
+ }
117
+
118
+ if (reportIsValid(state, reportFile)) {
119
+ state.count = 0;
120
+ state.zoomOutResolved = true;
121
+ state.zoomOutCycles = (state.zoomOutCycles || 0) + 1;
122
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
123
+ console.log(
124
+ `[Rule of 3] Zoom-out report accepted - breaker released. Follow your ` +
125
+ `report's Decision: RESUME means one fresh run at the NEW diagnosis; ` +
126
+ `ESCALATE means present the decision request and WAIT for the Human ` +
127
+ `Partner before mutating anything. If this same signature trips the ` +
128
+ `breaker again, it hard-locks for a human decision.`
129
+ );
130
+ process.exit(0);
131
+ }
132
+
133
+ if ((state.zoomOutCycles || 0) >= 1) {
134
+ blockForHumanDecision(state);
135
+ }
136
+ blockForReflection(state, reportFile);
137
+ } catch (err) {
138
+ // Fail open if state tracking breaks
139
+ process.exit(0);
140
+ }
141
+ }
142
+
143
+ // Fast path: skip stdin entirely unless some session's breaker is actually
144
+ // tripped (also keeps manual terminal runs instant).
145
+ let tripped = false;
146
+ try {
147
+ tripped = anySessionTripped(getWorkspaceRoot());
148
+ } catch (err) {
149
+ tripped = false; // fail open
150
+ }
151
+
152
+ if (!tripped) {
153
+ process.exit(0);
154
+ }
155
+
156
+ if (process.stdin.isTTY) {
157
+ // Manual run in a terminal - no payload is coming.
158
+ main(null);
159
+ } else {
160
+ let raw = '';
161
+ let finished = false;
162
+ const finish = () => {
163
+ if (finished) return;
164
+ finished = true;
165
+ let payload = null;
166
+ try { payload = JSON.parse(raw); } catch (err) { /* no payload */ }
167
+ main(payload);
168
+ };
169
+ // Claude Code closes stdin right after writing the payload; the timer is a
170
+ // safety net so an odd caller that never closes stdin can't hang the hook.
171
+ const timer = setTimeout(finish, 300);
172
+ process.stdin.setEncoding('utf8');
173
+ process.stdin.on('data', chunk => { raw += chunk; });
174
+ process.stdin.on('end', () => { clearTimeout(timer); finish(); });
175
+ process.stdin.on('error', () => { clearTimeout(timer); finish(); });
176
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
5
+
6
+ // Commands that count as "verification ran" for the Stop gate
7
+ // (hooks/scripts/stop-gate.js). Recall over precision: under-blocking the
8
+ // gate is the correct failure direction, so a broad net is fine.
9
+ const VERIFY_COMMAND_RE = /\b(test|spec|jest|vitest|mocha|pytest|rspec|phpunit|tsc|eslint|lint|build|compile|verify|check)\b/i;
10
+
11
+ let inputData = '';
12
+ const timeout = setTimeout(() => {
13
+ processState(null);
14
+ }, 200);
15
+
16
+ process.stdin.on('data', chunk => {
17
+ inputData += chunk;
18
+ });
19
+
20
+ process.stdin.on('end', () => {
21
+ clearTimeout(timeout);
22
+ try {
23
+ const payload = JSON.parse(inputData.trim());
24
+ processState(payload);
25
+ } catch (err) {
26
+ processState(null);
27
+ }
28
+ });
29
+
30
+ function processState(payload) {
31
+ try {
32
+ const sessionId = payload && payload.session_id;
33
+ const stateFile = path.join(getSessionDir(getWorkspaceRoot(), sessionId), 'handoff-state.json');
34
+
35
+ // Merge into the existing state rather than replacing it, so the
36
+ // failure/idle status and the Stop-gate milestones can coexist.
37
+ let state = { status: 'idle', timestamp: new Date().toISOString() };
38
+ if (fs.existsSync(stateFile)) {
39
+ try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch (e) { /* keep default */ }
40
+ }
41
+
42
+ if (payload) {
43
+ // Claude Code nests tool output under tool_response, not at the top
44
+ // level, and uses tool_name rather than tool. A numeric exit code
45
+ // also isn't guaranteed to be present at all - only trust it when
46
+ // it's actually a number; fall back to a stderr signal otherwise.
47
+ const toolResponse = payload.tool_response || {};
48
+ const stdout = toolResponse.stdout ?? payload.stdout ?? '';
49
+ const stderr = toolResponse.stderr ?? payload.stderr ?? '';
50
+ const rawExitCode = toolResponse.exitCode ?? toolResponse.exit_code ?? payload.exitCode;
51
+ const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
52
+ const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
53
+ const isFailed = (exitCode !== undefined && exitCode !== 0) || (exitCode === undefined && stderrSignal);
54
+ const toolName = payload.tool_name || payload.tool || 'command';
55
+
56
+ if (isFailed) {
57
+ // Truncate output to avoid state bloat
58
+ let output = (stderrSignal ? stderr : stdout) || payload.output || '';
59
+ if (output.length > 500) {
60
+ output = '...' + output.slice(-500);
61
+ }
62
+
63
+ state.status = 'failed';
64
+ state.timestamp = new Date().toISOString();
65
+ state.tool = toolName;
66
+ state.exitCode = exitCode;
67
+ state.errorSummary = output.trim();
68
+ } else if (exitCode === 0 && state.status === 'failed') {
69
+ // Clear failed state or mark as idle/resolved
70
+ state.lastResolved = { tool: state.tool, timestamp: state.timestamp };
71
+ state.status = 'idle';
72
+ state.timestamp = new Date().toISOString();
73
+ delete state.exitCode;
74
+ delete state.errorSummary;
75
+ }
76
+
77
+ // Milestones for the Stop gate: when did the last mutation happen, and
78
+ // has any verification-ish command succeeded since.
79
+ if (toolName === 'Edit' || toolName === 'Write') {
80
+ state.lastEditAt = Date.now();
81
+ } else if ((toolName === 'Bash' || toolName === 'PowerShell') && !isFailed) {
82
+ const command = (payload.tool_input && payload.tool_input.command) || '';
83
+ if (VERIFY_COMMAND_RE.test(command)) {
84
+ state.lastVerifyAt = Date.now();
85
+ }
86
+ }
87
+
88
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
89
+ } else {
90
+ // No standard input payload, or invalid JSON. Just maintain a heartbeat timestamp
91
+ if (!fs.existsSync(stateFile)) {
92
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
93
+ }
94
+ }
95
+ } catch (err) {
96
+ // Fail silently in hooks
97
+ }
98
+ process.exit(0);
99
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop Gate (Stop hook - Claude Code only)
4
+ * The pre-delivery gate (verification-loop) was prose-only, and prose decays
5
+ * as context grows. This makes the one check that matters mechanical: ending
6
+ * the turn with edits that were never followed by a successful
7
+ * verification-ish command (test/build/lint - tracked by state-persist.js)
8
+ * while uncommitted changes sit in the tree gets bounced back ONCE per edit
9
+ * batch. Objective facts only; fails open on any error.
10
+ */
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
15
+
16
+ function decide(payload) {
17
+ try {
18
+ // Loop guard: if this stop already resulted from a Stop-hook block,
19
+ // always let it through - one bounce is the contract, never a cage.
20
+ if (payload && payload.stop_hook_active) {
21
+ process.exit(0);
22
+ }
23
+
24
+ const root = getWorkspaceRoot();
25
+ const sessionDir = getSessionDir(root, payload && payload.session_id);
26
+ const handoffFile = path.join(sessionDir, 'handoff-state.json');
27
+ const gateFile = path.join(sessionDir, 'stop-gate-state.json');
28
+
29
+ if (!fs.existsSync(handoffFile)) process.exit(0);
30
+ const handoff = JSON.parse(fs.readFileSync(handoffFile, 'utf8'));
31
+ const lastEditAt = handoff.lastEditAt || 0;
32
+ const lastVerifyAt = handoff.lastVerifyAt || 0;
33
+
34
+ // No edits this session, or verification already ran after the last edit.
35
+ if (!lastEditAt || lastVerifyAt >= lastEditAt) process.exit(0);
36
+
37
+ // One bounce per edit batch: if we already blocked for this exact edit
38
+ // timestamp, the model either verified (which moves lastVerifyAt) or
39
+ // explicitly chose not to - don't nag twice for the same batch.
40
+ let gate = {};
41
+ if (fs.existsSync(gateFile)) {
42
+ try { gate = JSON.parse(fs.readFileSync(gateFile, 'utf8')); } catch (e) { /* ignore */ }
43
+ }
44
+ if (gate.blockedForEditAt === lastEditAt) process.exit(0);
45
+
46
+ // Only relevant when unverified work is actually sitting in the tree.
47
+ const dirty = execSync('git status --porcelain', {
48
+ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']
49
+ }).trim();
50
+ if (!dirty) process.exit(0);
51
+
52
+ fs.writeFileSync(gateFile, JSON.stringify({ blockedForEditAt: lastEditAt }, null, 2), 'utf8');
53
+
54
+ console.error(`[Stop Gate] Turn is ending with uncommitted edits and no successful verification command (test/build/lint) since the last edit.`);
55
+ console.error(`Before delivering: run the relevant check from 'verification-loop' - even a single targeted test beats none.`);
56
+ console.error(`If verification is genuinely not applicable (docs-only change, analysis-only turn, the Human Partner told you to stop), state that explicitly in your reply and stop again.`);
57
+ console.error(`This gate bounces at most once per edit batch.`);
58
+ process.exit(2);
59
+ } catch (err) {
60
+ // Fail open - a broken gate must never trap the session.
61
+ process.exit(0);
62
+ }
63
+ }
64
+
65
+ let inputData = '';
66
+ const timeout = setTimeout(() => {
67
+ decide(null);
68
+ }, 200);
69
+
70
+ process.stdin.on('data', chunk => { inputData += chunk; });
71
+ process.stdin.on('end', () => {
72
+ clearTimeout(timeout);
73
+ let payload = null;
74
+ try { payload = JSON.parse(inputData.trim()); } catch (err) { /* no payload */ }
75
+ decide(payload);
76
+ });
77
+ process.stdin.on('error', () => {
78
+ clearTimeout(timeout);
79
+ decide(null);
80
+ });