harness-everything 0.2.0 → 0.2.1

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 (70) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/build-multi-agent-system/SKILL.md +68 -68
  3. package/create-agent-launcher/SKILL.md +48 -48
  4. package/docs/workflows/build-multi-agent-system.md +62 -62
  5. package/docs/workflows/create-agent-launcher.md +62 -62
  6. package/docs/workflows/environment-detection.md +63 -63
  7. package/docs/workflows/eval-harness.md +57 -57
  8. package/docs/workflows/fable-discipline.md +59 -59
  9. package/docs/workflows/fable-mode.md +32 -32
  10. package/docs/workflows/git-commit.md +62 -62
  11. package/docs/workflows/grill-me.md +62 -62
  12. package/docs/workflows/grill-with-docs.md +62 -62
  13. package/docs/workflows/improve-codebase-architecture.md +60 -60
  14. package/docs/workflows/repo-docs.md +59 -59
  15. package/docs/workflows/rewrite-commits.md +60 -60
  16. package/docs/workflows/security-review.md +58 -58
  17. package/docs/workflows/self-evolve.md +59 -59
  18. package/docs/workflows/tdd.md +39 -39
  19. package/docs/workflows/todo-driven-workflow.md +36 -36
  20. package/docs/workflows/using-git-worktrees.md +63 -63
  21. package/docs/workflows/verification-loop.md +69 -69
  22. package/docs/workflows/verify-before-claim.md +60 -60
  23. package/docs/workflows/zoom-out.md +62 -62
  24. package/environment-detection/SKILL.md +77 -77
  25. package/eval-framework/behavioral-test.js +105 -105
  26. package/eval-framework/mechanism-test.js +251 -251
  27. package/eval-harness/SKILL.md +59 -59
  28. package/fable-discipline/SKILL.md +38 -38
  29. package/fable-mode/SKILL.md +51 -51
  30. package/git-commit/SKILL.md +55 -55
  31. package/grill-me/SKILL.md +36 -36
  32. package/grill-with-docs/SKILL.md +99 -99
  33. package/harness-everything/SKILL.md +141 -141
  34. package/harness-everything/examples/README.md +9 -9
  35. package/harness-everything/examples/todo-workflow.md +22 -22
  36. package/harness-everything/scripts/todo-cli.js +157 -157
  37. package/harness-everything/scripts/verify-gate.js +31 -31
  38. package/hooks/scripts/atomic-commit-check.js +68 -68
  39. package/hooks/scripts/context-compact.js +114 -114
  40. package/hooks/scripts/lib/harness-state.js +29 -3
  41. package/hooks/scripts/lib/platforms/claude.js +42 -42
  42. package/hooks/scripts/lib/platforms/codex.js +46 -46
  43. package/hooks/scripts/lib/platforms/continue.js +41 -41
  44. package/hooks/scripts/lib/platforms/copilot.js +41 -41
  45. package/hooks/scripts/lib/platforms/cursor.js +41 -41
  46. package/hooks/scripts/rule-of-3.js +176 -176
  47. package/improve-codebase-architecture/SKILL.md +44 -44
  48. package/install-cognitive-os/SKILL.md +95 -95
  49. package/package.json +1 -1
  50. package/repo-docs/SKILL.md +45 -45
  51. package/rewrite-commits/SKILL.md +32 -32
  52. package/scripts/installer.js +649 -649
  53. package/scripts/lib/advisory-text.js +163 -163
  54. package/scripts/lib/claude-hooks.js +71 -71
  55. package/scripts/lib/gitignore.js +80 -58
  56. package/scripts/lib/manifest.js +82 -82
  57. package/scripts/lib/prompts.js +241 -241
  58. package/scripts/lib/skills.js +172 -172
  59. package/scripts/lib/workspace.js +28 -28
  60. package/security-review/SKILL.md +510 -510
  61. package/self-evolve/SKILL.md +54 -54
  62. package/skill-creator/SKILL.md +91 -91
  63. package/skill-creator/references/testing-workflow.md +32 -32
  64. package/skill-style/SKILL.md +48 -48
  65. package/tdd/SKILL.md +53 -53
  66. package/todo-driven-workflow/SKILL.md +54 -54
  67. package/using-git-worktrees/SKILL.md +161 -161
  68. package/verification-loop/SKILL.md +138 -138
  69. package/verify-before-claim/SKILL.md +86 -86
  70. package/zoom-out/SKILL.md +90 -90
@@ -1,157 +1,157 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Harness Todo CLI
4
- * A strict state machine for task management.
5
- * Designed to slap the LLM with Exit Code 1 if it violates the Todo-Driven Workflow.
6
- */
7
- const fs = require('fs');
8
- const path = require('path');
9
- const { getWorkspaceRoot, getStateRoot } = require('../../hooks/scripts/lib/harness-state');
10
-
11
- // Shared across sessions (not scoped to one): todo-cli.js is invoked as a
12
- // plain CLI command by the agent, not a hook, so it never receives a
13
- // session_id to scope by.
14
- const harnessDir = getStateRoot(getWorkspaceRoot());
15
- const stateFile = path.join(harnessDir, 'todo-state.json');
16
-
17
- if (!fs.existsSync(harnessDir)) {
18
- fs.mkdirSync(harnessDir, { recursive: true });
19
- }
20
-
21
- function loadState() {
22
- if (fs.existsSync(stateFile)) {
23
- return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
24
- }
25
- return { tasks: [], activeId: null };
26
- }
27
-
28
- function saveState(state) {
29
- // Atomic write (temp file + rename): todo-state.json is shared across the
30
- // whole workspace, not scoped per session_id like the hook state files
31
- // are, so parallel subagent Task bursts can genuinely call `todo-cli.js`
32
- // around the same time. A plain writeFileSync lets a reader observe a
33
- // truncated/partial JSON file if two writes interleave; rename is atomic
34
- // at the filesystem level, so readers only ever see the old or the new
35
- // file in full, never a half-written one. This doesn't resolve the
36
- // separate lost-update race (last writer still wins) - that needs real
37
- // cross-process locking, out of scope for what a CLI state file needs.
38
- const tmpFile = `${stateFile}.tmp.${process.pid}`;
39
- fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2));
40
- fs.renameSync(tmpFile, stateFile);
41
- }
42
-
43
- function printList(state) {
44
- console.log('\n📋 Current Todo List:');
45
- if (state.tasks.length === 0) {
46
- console.log(' (Empty)');
47
- }
48
- state.tasks.forEach(t => {
49
- const status = t.id === state.activeId ? '[IN-PROGRESS]' : (t.completed ? '[COMPLETED]' : '[NOT-STARTED]');
50
- console.log(` ${t.id}. ${status} ${t.title}`);
51
- });
52
- console.log('');
53
- }
54
-
55
- const args = process.argv.slice(2);
56
- const command = args[0];
57
-
58
- if (!command) {
59
- console.error("❌ Error: Missing command. Use 'init', 'add', 'start', 'complete', or 'status'.");
60
- process.exit(1);
61
- }
62
-
63
- const state = loadState();
64
-
65
- switch (command) {
66
- case 'init':
67
- state.tasks = [];
68
- state.activeId = null;
69
- args.slice(1).forEach((title, idx) => {
70
- state.tasks.push({ id: idx + 1, title, completed: false });
71
- });
72
- saveState(state);
73
- console.log("✅ Todo list initialized.");
74
- printList(state);
75
- break;
76
-
77
- case 'add':
78
- const title = args.slice(1).join(' ');
79
- if (!title) {
80
- console.error("❌ Error: Missing task title.");
81
- process.exit(1);
82
- }
83
- const nextId = state.tasks.length > 0 ? Math.max(...state.tasks.map(t => t.id)) + 1 : 1;
84
- state.tasks.push({ id: nextId, title, completed: false });
85
- saveState(state);
86
- console.log(`✅ Added task ${nextId}.`);
87
- printList(state);
88
- break;
89
-
90
- case 'start':
91
- const startId = parseInt(args[1], 10);
92
- if (!startId) {
93
- console.error("❌ Error: Missing task ID. Usage: start <id>");
94
- process.exit(1);
95
- }
96
- if (state.activeId !== null && state.activeId !== startId) {
97
- console.error(`❌ [BLOCKED] Task ${state.activeId} is already IN-PROGRESS.`);
98
- console.error(`You MUST complete or un-start task ${state.activeId} before starting a new one. No multitasking.`);
99
- process.exit(1);
100
- }
101
- const taskToStart = state.tasks.find(t => t.id === startId);
102
- if (!taskToStart) {
103
- console.error(`❌ Error: Task ${startId} not found.`);
104
- process.exit(1);
105
- }
106
- if (taskToStart.completed) {
107
- console.error(`❌ Error: Task ${startId} is already completed.`);
108
- process.exit(1);
109
- }
110
- state.activeId = startId;
111
- saveState(state);
112
- console.log(`🚀 Started task ${startId}: ${taskToStart.title}`);
113
- break;
114
-
115
- case 'complete':
116
- const compId = parseInt(args[1], 10);
117
- if (!compId) {
118
- console.error("❌ Error: Missing task ID. Usage: complete <id>");
119
- process.exit(1);
120
- }
121
- if (state.activeId !== compId) {
122
- console.error(`❌ [BLOCKED] Task ${compId} is NOT currently in-progress.`);
123
- console.error(`You can only complete the active task. Active task is: ${state.activeId || 'None'}`);
124
- process.exit(1);
125
- }
126
-
127
- // ACTIONABLE RECOMMENDATION #1: Enforce State Transitions Programmatically
128
- // Do not rely on prompt obedience for running verify-gate.js. The CLI should block the complete command
129
- console.log("=== 🛡️ Executing Pre-Completion Verification Gate ===");
130
- try {
131
- const verifyScript = path.join(__dirname, 'verify-gate.js');
132
- if (fs.existsSync(verifyScript)) {
133
- require('child_process').execSync(`node "${verifyScript}"`, { stdio: 'inherit' });
134
- }
135
- } catch (err) {
136
- console.error(`\n❌ [BLOCKED] Verification Gate Failed (Exit Code 1).`);
137
- console.error(`You CANNOT complete this task. The code is broken.`);
138
- console.error(`Please fix the errors shown above and try again.`);
139
- process.exit(1);
140
- }
141
-
142
- const taskToComp = state.tasks.find(t => t.id === compId);
143
- taskToComp.completed = true;
144
- state.activeId = null;
145
- saveState(state);
146
- console.log(`✅ Completed task ${compId}: ${taskToComp.title}`);
147
- printList(state);
148
- break;
149
-
150
- case 'status':
151
- printList(state);
152
- break;
153
-
154
- default:
155
- console.error(`❌ Error: Unknown command '${command}'.`);
156
- process.exit(1);
157
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Harness Todo CLI
4
+ * A strict state machine for task management.
5
+ * Designed to slap the LLM with Exit Code 1 if it violates the Todo-Driven Workflow.
6
+ */
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { getWorkspaceRoot, getStateRoot } = require('../../hooks/scripts/lib/harness-state');
10
+
11
+ // Shared across sessions (not scoped to one): todo-cli.js is invoked as a
12
+ // plain CLI command by the agent, not a hook, so it never receives a
13
+ // session_id to scope by.
14
+ const harnessDir = getStateRoot(getWorkspaceRoot());
15
+ const stateFile = path.join(harnessDir, 'todo-state.json');
16
+
17
+ if (!fs.existsSync(harnessDir)) {
18
+ fs.mkdirSync(harnessDir, { recursive: true });
19
+ }
20
+
21
+ function loadState() {
22
+ if (fs.existsSync(stateFile)) {
23
+ return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
24
+ }
25
+ return { tasks: [], activeId: null };
26
+ }
27
+
28
+ function saveState(state) {
29
+ // Atomic write (temp file + rename): todo-state.json is shared across the
30
+ // whole workspace, not scoped per session_id like the hook state files
31
+ // are, so parallel subagent Task bursts can genuinely call `todo-cli.js`
32
+ // around the same time. A plain writeFileSync lets a reader observe a
33
+ // truncated/partial JSON file if two writes interleave; rename is atomic
34
+ // at the filesystem level, so readers only ever see the old or the new
35
+ // file in full, never a half-written one. This doesn't resolve the
36
+ // separate lost-update race (last writer still wins) - that needs real
37
+ // cross-process locking, out of scope for what a CLI state file needs.
38
+ const tmpFile = `${stateFile}.tmp.${process.pid}`;
39
+ fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2));
40
+ fs.renameSync(tmpFile, stateFile);
41
+ }
42
+
43
+ function printList(state) {
44
+ console.log('\n📋 Current Todo List:');
45
+ if (state.tasks.length === 0) {
46
+ console.log(' (Empty)');
47
+ }
48
+ state.tasks.forEach(t => {
49
+ const status = t.id === state.activeId ? '[IN-PROGRESS]' : (t.completed ? '[COMPLETED]' : '[NOT-STARTED]');
50
+ console.log(` ${t.id}. ${status} ${t.title}`);
51
+ });
52
+ console.log('');
53
+ }
54
+
55
+ const args = process.argv.slice(2);
56
+ const command = args[0];
57
+
58
+ if (!command) {
59
+ console.error("❌ Error: Missing command. Use 'init', 'add', 'start', 'complete', or 'status'.");
60
+ process.exit(1);
61
+ }
62
+
63
+ const state = loadState();
64
+
65
+ switch (command) {
66
+ case 'init':
67
+ state.tasks = [];
68
+ state.activeId = null;
69
+ args.slice(1).forEach((title, idx) => {
70
+ state.tasks.push({ id: idx + 1, title, completed: false });
71
+ });
72
+ saveState(state);
73
+ console.log("✅ Todo list initialized.");
74
+ printList(state);
75
+ break;
76
+
77
+ case 'add':
78
+ const title = args.slice(1).join(' ');
79
+ if (!title) {
80
+ console.error("❌ Error: Missing task title.");
81
+ process.exit(1);
82
+ }
83
+ const nextId = state.tasks.length > 0 ? Math.max(...state.tasks.map(t => t.id)) + 1 : 1;
84
+ state.tasks.push({ id: nextId, title, completed: false });
85
+ saveState(state);
86
+ console.log(`✅ Added task ${nextId}.`);
87
+ printList(state);
88
+ break;
89
+
90
+ case 'start':
91
+ const startId = parseInt(args[1], 10);
92
+ if (!startId) {
93
+ console.error("❌ Error: Missing task ID. Usage: start <id>");
94
+ process.exit(1);
95
+ }
96
+ if (state.activeId !== null && state.activeId !== startId) {
97
+ console.error(`❌ [BLOCKED] Task ${state.activeId} is already IN-PROGRESS.`);
98
+ console.error(`You MUST complete or un-start task ${state.activeId} before starting a new one. No multitasking.`);
99
+ process.exit(1);
100
+ }
101
+ const taskToStart = state.tasks.find(t => t.id === startId);
102
+ if (!taskToStart) {
103
+ console.error(`❌ Error: Task ${startId} not found.`);
104
+ process.exit(1);
105
+ }
106
+ if (taskToStart.completed) {
107
+ console.error(`❌ Error: Task ${startId} is already completed.`);
108
+ process.exit(1);
109
+ }
110
+ state.activeId = startId;
111
+ saveState(state);
112
+ console.log(`🚀 Started task ${startId}: ${taskToStart.title}`);
113
+ break;
114
+
115
+ case 'complete':
116
+ const compId = parseInt(args[1], 10);
117
+ if (!compId) {
118
+ console.error("❌ Error: Missing task ID. Usage: complete <id>");
119
+ process.exit(1);
120
+ }
121
+ if (state.activeId !== compId) {
122
+ console.error(`❌ [BLOCKED] Task ${compId} is NOT currently in-progress.`);
123
+ console.error(`You can only complete the active task. Active task is: ${state.activeId || 'None'}`);
124
+ process.exit(1);
125
+ }
126
+
127
+ // ACTIONABLE RECOMMENDATION #1: Enforce State Transitions Programmatically
128
+ // Do not rely on prompt obedience for running verify-gate.js. The CLI should block the complete command
129
+ console.log("=== 🛡️ Executing Pre-Completion Verification Gate ===");
130
+ try {
131
+ const verifyScript = path.join(__dirname, 'verify-gate.js');
132
+ if (fs.existsSync(verifyScript)) {
133
+ require('child_process').execSync(`node "${verifyScript}"`, { stdio: 'inherit' });
134
+ }
135
+ } catch (err) {
136
+ console.error(`\n❌ [BLOCKED] Verification Gate Failed (Exit Code 1).`);
137
+ console.error(`You CANNOT complete this task. The code is broken.`);
138
+ console.error(`Please fix the errors shown above and try again.`);
139
+ process.exit(1);
140
+ }
141
+
142
+ const taskToComp = state.tasks.find(t => t.id === compId);
143
+ taskToComp.completed = true;
144
+ state.activeId = null;
145
+ saveState(state);
146
+ console.log(`✅ Completed task ${compId}: ${taskToComp.title}`);
147
+ printList(state);
148
+ break;
149
+
150
+ case 'status':
151
+ printList(state);
152
+ break;
153
+
154
+ default:
155
+ console.error(`❌ Error: Unknown command '${command}'.`);
156
+ process.exit(1);
157
+ }
@@ -1,31 +1,31 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Verification Gatekeeper
4
- * Forces the AI to run this script to prove quality gates before declaring a task done.
5
- */
6
- const { execSync } = require('child_process');
7
-
8
- console.log("=== 🛡️ Harness Verification Gate ===");
9
-
10
- try {
11
- console.log("Running basic checks (simulated for now)...");
12
-
13
- // In a real project, this would run `npm run lint` and `npm test`
14
- // We'll simulate a failure if `.verify-fail.tmp` exists just to prove the mechanism.
15
- const fs = require('fs');
16
- if (fs.existsSync('.verify-fail.tmp')) {
17
- console.error("❌ Verification FAILED: Found broken tests or lint errors.");
18
- console.error("Please fix the code and run this gate again.");
19
- process.exit(1);
20
- }
21
-
22
- console.log("✅ Lint passed.");
23
- console.log("✅ Tests passed.");
24
- console.log("✅ Verification SUCCESS. You may now complete the task.");
25
- process.exit(0);
26
-
27
- } catch (err) {
28
- console.error("❌ Verification FAILED.");
29
- console.error(err.message);
30
- process.exit(1);
31
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verification Gatekeeper
4
+ * Forces the AI to run this script to prove quality gates before declaring a task done.
5
+ */
6
+ const { execSync } = require('child_process');
7
+
8
+ console.log("=== 🛡️ Harness Verification Gate ===");
9
+
10
+ try {
11
+ console.log("Running basic checks (simulated for now)...");
12
+
13
+ // In a real project, this would run `npm run lint` and `npm test`
14
+ // We'll simulate a failure if `.verify-fail.tmp` exists just to prove the mechanism.
15
+ const fs = require('fs');
16
+ if (fs.existsSync('.verify-fail.tmp')) {
17
+ console.error("❌ Verification FAILED: Found broken tests or lint errors.");
18
+ console.error("Please fix the code and run this gate again.");
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log("✅ Lint passed.");
23
+ console.log("✅ Tests passed.");
24
+ console.log("✅ Verification SUCCESS. You may now complete the task.");
25
+ process.exit(0);
26
+
27
+ } catch (err) {
28
+ console.error("❌ Verification FAILED.");
29
+ console.error(err.message);
30
+ process.exit(1);
31
+ }
@@ -1,68 +1,68 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Atomic Commit Check (PostToolUse: Edit|Write)
4
- * PostToolUse can't block - the edit/write already happened. So this counts
5
- * Edit/Write calls since the last commit and, once the count crosses a
6
- * threshold, feeds a strong reminder back to Claude (exit 2) to commit the
7
- * completed chunk before piling on more changes. Resets whenever HEAD moves.
8
- * Fails open on any error - this is a nudge, never a hard stop.
9
- */
10
- const { execSync } = require('child_process');
11
- const fs = require('fs');
12
- const path = require('path');
13
- const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
14
-
15
- const THRESHOLD = 6;
16
- const RENUDGE_EVERY = 3;
17
-
18
- function main(payload) {
19
- try {
20
- const root = getWorkspaceRoot();
21
- const head = execSync('git rev-parse HEAD', { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
22
-
23
- const stateFile = path.join(getSessionDir(root, payload && payload.session_id), 'atomic-commit-state.json');
24
-
25
- let state = { lastHead: head, editCount: 0 };
26
- if (fs.existsSync(stateFile)) {
27
- try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch (e) { /* use default */ }
28
- }
29
-
30
- if (state.lastHead !== head) {
31
- // A commit happened since we last checked - fresh start.
32
- state.lastHead = head;
33
- state.editCount = 0;
34
- }
35
-
36
- state.editCount += 1;
37
- fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
38
-
39
- const overThreshold = state.editCount >= THRESHOLD;
40
- const onNudgeBeat = overThreshold && (state.editCount - THRESHOLD) % RENUDGE_EVERY === 0;
41
-
42
- if (onNudgeBeat) {
43
- const statusRaw = execSync('git status --porcelain', { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
44
- const changedFiles = statusRaw ? statusRaw.split('\n').length : 0;
45
- console.error(`[Atomic Commit Check] ${state.editCount} Edit/Write calls since the last commit (${changedFiles} files currently changed).`);
46
- console.error(`If a logically complete chunk of work is done, commit it now before continuing - large uncommitted diffs are harder to review and harder to recover from.`);
47
- process.exit(2);
48
- }
49
-
50
- process.exit(0);
51
- } catch (err) {
52
- process.exit(0);
53
- }
54
- }
55
-
56
- let inputData = '';
57
- const timeout = setTimeout(() => { main(null); }, 200);
58
- process.stdin.on('data', chunk => { inputData += chunk; });
59
- process.stdin.on('end', () => {
60
- clearTimeout(timeout);
61
- let payload = null;
62
- try { payload = JSON.parse(inputData.trim()); } catch (err) { /* no payload */ }
63
- main(payload);
64
- });
65
- process.stdin.on('error', () => {
66
- clearTimeout(timeout);
67
- main(null);
68
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Atomic Commit Check (PostToolUse: Edit|Write)
4
+ * PostToolUse can't block - the edit/write already happened. So this counts
5
+ * Edit/Write calls since the last commit and, once the count crosses a
6
+ * threshold, feeds a strong reminder back to Claude (exit 2) to commit the
7
+ * completed chunk before piling on more changes. Resets whenever HEAD moves.
8
+ * Fails open on any error - this is a nudge, never a hard stop.
9
+ */
10
+ const { execSync } = require('child_process');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
14
+
15
+ const THRESHOLD = 6;
16
+ const RENUDGE_EVERY = 3;
17
+
18
+ function main(payload) {
19
+ try {
20
+ const root = getWorkspaceRoot();
21
+ const head = execSync('git rev-parse HEAD', { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
22
+
23
+ const stateFile = path.join(getSessionDir(root, payload && payload.session_id), 'atomic-commit-state.json');
24
+
25
+ let state = { lastHead: head, editCount: 0 };
26
+ if (fs.existsSync(stateFile)) {
27
+ try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch (e) { /* use default */ }
28
+ }
29
+
30
+ if (state.lastHead !== head) {
31
+ // A commit happened since we last checked - fresh start.
32
+ state.lastHead = head;
33
+ state.editCount = 0;
34
+ }
35
+
36
+ state.editCount += 1;
37
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
38
+
39
+ const overThreshold = state.editCount >= THRESHOLD;
40
+ const onNudgeBeat = overThreshold && (state.editCount - THRESHOLD) % RENUDGE_EVERY === 0;
41
+
42
+ if (onNudgeBeat) {
43
+ const statusRaw = execSync('git status --porcelain', { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
44
+ const changedFiles = statusRaw ? statusRaw.split('\n').length : 0;
45
+ console.error(`[Atomic Commit Check] ${state.editCount} Edit/Write calls since the last commit (${changedFiles} files currently changed).`);
46
+ console.error(`If a logically complete chunk of work is done, commit it now before continuing - large uncommitted diffs are harder to review and harder to recover from.`);
47
+ process.exit(2);
48
+ }
49
+
50
+ process.exit(0);
51
+ } catch (err) {
52
+ process.exit(0);
53
+ }
54
+ }
55
+
56
+ let inputData = '';
57
+ const timeout = setTimeout(() => { main(null); }, 200);
58
+ process.stdin.on('data', chunk => { inputData += chunk; });
59
+ process.stdin.on('end', () => {
60
+ clearTimeout(timeout);
61
+ let payload = null;
62
+ try { payload = JSON.parse(inputData.trim()); } catch (err) { /* no payload */ }
63
+ main(payload);
64
+ });
65
+ process.stdin.on('error', () => {
66
+ clearTimeout(timeout);
67
+ main(null);
68
+ });