anvil-dev-framework 0.1.6

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 (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Ralph Stop Hook Tests
3
+ *
4
+ * Tests for ralph_stop.sh checkpoint handling and loop control.
5
+ * Verifies iteration logic, completion detection, circuit breakers,
6
+ * and CCS checkpoint integration.
7
+ */
8
+
9
+ import { describe, test, expect, beforeEach, afterEach, beforeAll } from 'bun:test';
10
+ import { existsSync } from 'fs';
11
+ import {
12
+ TestStateManager,
13
+ runRalphStop,
14
+ isJqAvailable,
15
+ } from './test-utils';
16
+ import {
17
+ createMinimalRalphState,
18
+ createFullRalphState,
19
+ createCheckpointedState,
20
+ createLinearIntegratedState,
21
+ TRANSCRIPT_COMPLETE,
22
+ TRANSCRIPT_FATAL,
23
+ TRANSCRIPT_STUCK,
24
+ } from './fixtures';
25
+
26
+ describe('Ralph Stop Hook', () => {
27
+ let stateManager: TestStateManager;
28
+ let jqAvailable: boolean;
29
+
30
+ beforeAll(async () => {
31
+ jqAvailable = await isJqAvailable();
32
+ });
33
+
34
+ beforeEach(() => {
35
+ stateManager = new TestStateManager('ralph-stop');
36
+ stateManager.init();
37
+ });
38
+
39
+ afterEach(() => {
40
+ stateManager.cleanup();
41
+ });
42
+
43
+ describe('Non-Ralph Mode', () => {
44
+ test('exits silently without state file', async () => {
45
+ if (!jqAvailable) {
46
+ console.log('Skipping: jq not available');
47
+ return;
48
+ }
49
+
50
+ // Don't create any state file
51
+ const result = await runRalphStop(stateManager);
52
+
53
+ expect(result.exitCode).toBe(0);
54
+ });
55
+
56
+ test('does not create state file if not in Ralph mode', async () => {
57
+ if (!jqAvailable) {
58
+ console.log('Skipping: jq not available');
59
+ return;
60
+ }
61
+
62
+ await runRalphStop(stateManager);
63
+
64
+ expect(stateManager.stateExists()).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe('Iteration Tracking', () => {
69
+ test('increments iteration counter', async () => {
70
+ if (!jqAvailable) {
71
+ console.log('Skipping: jq not available');
72
+ return;
73
+ }
74
+
75
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
76
+
77
+ await runRalphStop(stateManager);
78
+
79
+ const state = stateManager.readState();
80
+ expect(state.iteration).toBe(6);
81
+ });
82
+
83
+ test('initializes state if missing fields', async () => {
84
+ if (!jqAvailable) {
85
+ console.log('Skipping: jq not available');
86
+ return;
87
+ }
88
+
89
+ // Write minimal state
90
+ stateManager.writeState({});
91
+
92
+ await runRalphStop(stateManager);
93
+
94
+ const state = stateManager.readState();
95
+ expect(state.iteration).toBeDefined();
96
+ });
97
+ });
98
+
99
+ describe('Max Iterations Safety', () => {
100
+ test('exits with code 0 at max iterations', async () => {
101
+ if (!jqAvailable) {
102
+ console.log('Skipping: jq not available');
103
+ return;
104
+ }
105
+
106
+ stateManager.writeState(createMinimalRalphState({ iteration: 49 }));
107
+
108
+ const result = await runRalphStop(stateManager, { maxIterations: 50 });
109
+
110
+ expect(result.exitCode).toBe(0);
111
+ expect(result.stderr).toContain('Maximum iterations');
112
+ });
113
+
114
+ test('cleans up state at max iterations', async () => {
115
+ if (!jqAvailable) {
116
+ console.log('Skipping: jq not available');
117
+ return;
118
+ }
119
+
120
+ stateManager.writeState(createMinimalRalphState({ iteration: 49 }));
121
+
122
+ await runRalphStop(stateManager, { maxIterations: 50 });
123
+
124
+ // State file should be cleaned up
125
+ expect(stateManager.stateExists()).toBe(false);
126
+ });
127
+
128
+ test('continues loop before max iterations', async () => {
129
+ if (!jqAvailable) {
130
+ console.log('Skipping: jq not available');
131
+ return;
132
+ }
133
+
134
+ stateManager.writeState(createMinimalRalphState({ iteration: 10 }));
135
+
136
+ const result = await runRalphStop(stateManager, { maxIterations: 50 });
137
+
138
+ expect(result.exitCode).toBe(1); // Block exit
139
+ expect(result.stderr).toContain('restarting');
140
+ });
141
+
142
+ test('respects custom max iterations', async () => {
143
+ if (!jqAvailable) {
144
+ console.log('Skipping: jq not available');
145
+ return;
146
+ }
147
+
148
+ stateManager.writeState(createMinimalRalphState({ iteration: 9 }));
149
+
150
+ const result = await runRalphStop(stateManager, { maxIterations: 10 });
151
+
152
+ expect(result.exitCode).toBe(0);
153
+ });
154
+ });
155
+
156
+ describe('Completion Promise Detection', () => {
157
+ test('exits with code 0 on completion promise', async () => {
158
+ if (!jqAvailable) {
159
+ console.log('Skipping: jq not available');
160
+ return;
161
+ }
162
+
163
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
164
+
165
+ const result = await runRalphStop(stateManager, {
166
+ transcript: TRANSCRIPT_COMPLETE,
167
+ });
168
+
169
+ expect(result.exitCode).toBe(0);
170
+ expect(result.stderr).toContain('complete');
171
+ });
172
+
173
+ test('cleans up state on completion', async () => {
174
+ if (!jqAvailable) {
175
+ console.log('Skipping: jq not available');
176
+ return;
177
+ }
178
+
179
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
180
+
181
+ await runRalphStop(stateManager, {
182
+ transcript: TRANSCRIPT_COMPLETE,
183
+ });
184
+
185
+ expect(stateManager.stateExists()).toBe(false);
186
+ });
187
+ });
188
+
189
+ describe('Fatal Error Detection', () => {
190
+ test('exits with code 0 on fatal error', async () => {
191
+ if (!jqAvailable) {
192
+ console.log('Skipping: jq not available');
193
+ return;
194
+ }
195
+
196
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
197
+
198
+ const result = await runRalphStop(stateManager, {
199
+ transcript: TRANSCRIPT_FATAL,
200
+ });
201
+
202
+ expect(result.exitCode).toBe(0);
203
+ expect(result.stderr).toContain('error');
204
+ });
205
+
206
+ test('cleans up state on fatal error', async () => {
207
+ if (!jqAvailable) {
208
+ console.log('Skipping: jq not available');
209
+ return;
210
+ }
211
+
212
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
213
+
214
+ await runRalphStop(stateManager, {
215
+ transcript: TRANSCRIPT_FATAL,
216
+ });
217
+
218
+ expect(stateManager.stateExists()).toBe(false);
219
+ });
220
+ });
221
+
222
+ describe('Stuck Signal Detection', () => {
223
+ test('logs warning on stuck signals', async () => {
224
+ if (!jqAvailable) {
225
+ console.log('Skipping: jq not available');
226
+ return;
227
+ }
228
+
229
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
230
+
231
+ const result = await runRalphStop(stateManager, {
232
+ transcript: TRANSCRIPT_STUCK,
233
+ });
234
+
235
+ // Should warn but not exit
236
+ expect(result.stderr).toContain('Stuck');
237
+ expect(result.exitCode).toBe(1); // Still continues
238
+ });
239
+ });
240
+
241
+ describe('CCS Checkpoint Integration', () => {
242
+ test('restarts with exit code 1 on active checkpoint', async () => {
243
+ if (!jqAvailable) {
244
+ console.log('Skipping: jq not available');
245
+ return;
246
+ }
247
+
248
+ stateManager.writeState(createCheckpointedState('L2', 87));
249
+
250
+ const result = await runRalphStop(stateManager);
251
+
252
+ expect(result.exitCode).toBe(1); // Restart loop
253
+ expect(result.stderr).toContain('checkpoint');
254
+ });
255
+
256
+ test('detects active checkpoint in state', async () => {
257
+ if (!jqAvailable) {
258
+ console.log('Skipping: jq not available');
259
+ return;
260
+ }
261
+
262
+ stateManager.writeState(createCheckpointedState('L2', 87));
263
+
264
+ const result = await runRalphStop(stateManager);
265
+
266
+ // The checkpoint is detected (log message confirms this)
267
+ expect(result.stderr).toContain('CCS checkpoint detected');
268
+ });
269
+
270
+ test('attempts PROMPT.md generation for checkpoint resume', async () => {
271
+ if (!jqAvailable) {
272
+ console.log('Skipping: jq not available');
273
+ return;
274
+ }
275
+
276
+ stateManager.writeState(createCheckpointedState('L2', 87, {
277
+ task_name: 'Test Task',
278
+ todo_items: ['Item 1', 'Item 2'],
279
+ }));
280
+
281
+ const result = await runRalphStop(stateManager);
282
+
283
+ // Hook attempts to handle checkpoint (success depends on Python generator path)
284
+ expect(result.stderr).toContain('checkpoint');
285
+ // Note: PROMPT.md generation may fail in test env due to relative path resolution
286
+ });
287
+
288
+ test('handles L3 checkpoint same as L2', async () => {
289
+ if (!jqAvailable) {
290
+ console.log('Skipping: jq not available');
291
+ return;
292
+ }
293
+
294
+ stateManager.writeState(createCheckpointedState('L3', 96));
295
+
296
+ const result = await runRalphStop(stateManager);
297
+
298
+ expect(result.exitCode).toBe(1);
299
+ expect(result.stderr).toContain('checkpoint');
300
+ });
301
+ });
302
+
303
+ describe('Circuit Breaker', () => {
304
+ // Note: Circuit breaker tests are limited in isolated test env because
305
+ // they rely on actual git diff hashes. These tests verify the logic
306
+ // runs without crashing; full circuit breaker testing requires a git repo.
307
+
308
+ test('does not crash when checking git diff in non-repo', async () => {
309
+ if (!jqAvailable) {
310
+ console.log('Skipping: jq not available');
311
+ return;
312
+ }
313
+
314
+ stateManager.writeState(createMinimalRalphState({
315
+ iteration: 10,
316
+ no_change_count: 2,
317
+ last_diff_hash: 'abc123',
318
+ }));
319
+
320
+ const result = await runRalphStop(stateManager);
321
+
322
+ // Should complete without crashing
323
+ expect(typeof result.exitCode).toBe('number');
324
+ });
325
+
326
+ test('continues iteration when git is unavailable', async () => {
327
+ if (!jqAvailable) {
328
+ console.log('Skipping: jq not available');
329
+ return;
330
+ }
331
+
332
+ stateManager.writeState(createMinimalRalphState({
333
+ iteration: 5,
334
+ no_change_count: 0,
335
+ }));
336
+
337
+ const result = await runRalphStop(stateManager);
338
+
339
+ // Should continue (exit code 1) since no completion/max iter/fatal
340
+ expect(result.exitCode).toBe(1);
341
+ });
342
+ });
343
+
344
+ describe('Linear Integration', () => {
345
+ test('handles state with Linear integration enabled', async () => {
346
+ if (!jqAvailable) {
347
+ console.log('Skipping: jq not available');
348
+ return;
349
+ }
350
+
351
+ stateManager.writeState(createLinearIntegratedState());
352
+
353
+ const result = await runRalphStop(stateManager);
354
+
355
+ expect(result.exitCode).toBe(1); // Normal restart
356
+ });
357
+
358
+ test('does not crash with missing Linear fields', async () => {
359
+ if (!jqAvailable) {
360
+ console.log('Skipping: jq not available');
361
+ return;
362
+ }
363
+
364
+ stateManager.writeState(createMinimalRalphState({
365
+ linear_integration: { enabled: true },
366
+ }));
367
+
368
+ const result = await runRalphStop(stateManager);
369
+
370
+ expect(result.exitCode).toBe(1); // Should still work
371
+ });
372
+ });
373
+
374
+ describe('Error Handling', () => {
375
+ test('handles corrupted state file gracefully', async () => {
376
+ if (!jqAvailable) {
377
+ console.log('Skipping: jq not available');
378
+ return;
379
+ }
380
+
381
+ // Write invalid JSON
382
+ stateManager.writeFile('.claude/ralph-state.json', 'not valid json');
383
+
384
+ // Should not crash, but may have unexpected behavior
385
+ // The important thing is it doesn't hang
386
+ const result = await runRalphStop(stateManager);
387
+ expect(typeof result.exitCode).toBe('number');
388
+ });
389
+ });
390
+
391
+ describe('Logging', () => {
392
+ test('creates log entries when logging enabled', async () => {
393
+ if (!jqAvailable) {
394
+ console.log('Skipping: jq not available');
395
+ return;
396
+ }
397
+
398
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
399
+
400
+ await runRalphStop(stateManager, { enableLogging: true });
401
+
402
+ // Check if log file was created
403
+ expect(stateManager.fileExists('.claude/logs/ralph.log')).toBe(true);
404
+ });
405
+
406
+ test('does not create logs when logging disabled', async () => {
407
+ if (!jqAvailable) {
408
+ console.log('Skipping: jq not available');
409
+ return;
410
+ }
411
+
412
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
413
+
414
+ await runRalphStop(stateManager, { enableLogging: false });
415
+
416
+ expect(stateManager.fileExists('.claude/logs/ralph.log')).toBe(false);
417
+ });
418
+ });
419
+ });
420
+
421
+ describe('Ralph Stop Hook - jq Dependency', () => {
422
+ let stateManager: TestStateManager;
423
+
424
+ beforeEach(() => {
425
+ stateManager = new TestStateManager('ralph-stop-jq');
426
+ stateManager.init();
427
+ });
428
+
429
+ afterEach(() => {
430
+ stateManager.cleanup();
431
+ });
432
+
433
+ test('hook script exists', async () => {
434
+ const hookPath = '/Users/alexandercahiz/Projects/anvil-dev-framework/global/hooks/ralph_stop.sh';
435
+ expect(existsSync(hookPath)).toBe(true);
436
+ });
437
+
438
+ // Note: Testing missing jq is tricky because we can't easily uninstall it
439
+ // The hook handles this case by printing an error and exiting 0
440
+ });
@@ -0,0 +1,252 @@
1
+ /**
2
+ * CCS Test Utilities
3
+ *
4
+ * Shared helpers for Context Checkpoint System (CCS) end-to-end testing.
5
+ * Provides subprocess execution for Python and Bash hooks, state file
6
+ * management, and context simulation.
7
+ */
8
+
9
+ import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { tmpdir } from 'os';
12
+
13
+ // Project paths
14
+ export const PROJECT_ROOT = join(__dirname, '../../../../../..');
15
+ export const HOOKS_DIR = join(PROJECT_ROOT, 'global/hooks');
16
+ export const LIB_DIR = join(PROJECT_ROOT, 'global/lib');
17
+
18
+ /**
19
+ * Result from running a hook subprocess
20
+ */
21
+ export interface HookResult {
22
+ stdout: string;
23
+ stderr: string;
24
+ exitCode: number;
25
+ }
26
+
27
+ /**
28
+ * Test directory manager for isolated state files
29
+ */
30
+ export class TestStateManager {
31
+ readonly baseDir: string;
32
+ readonly stateFile: string;
33
+ readonly claudeDir: string;
34
+
35
+ constructor(prefix = 'ccs-test') {
36
+ this.baseDir = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
37
+ this.claudeDir = join(this.baseDir, '.claude');
38
+ this.stateFile = join(this.claudeDir, 'ralph-state.json');
39
+ }
40
+
41
+ /**
42
+ * Initialize the test directory structure
43
+ */
44
+ init(): void {
45
+ mkdirSync(this.claudeDir, { recursive: true });
46
+ mkdirSync(join(this.claudeDir, 'handoffs'), { recursive: true });
47
+ mkdirSync(join(this.claudeDir, 'logs'), { recursive: true });
48
+ }
49
+
50
+ /**
51
+ * Write a Ralph state file
52
+ */
53
+ writeState(state: object): void {
54
+ writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
55
+ }
56
+
57
+ /**
58
+ * Read the current Ralph state file
59
+ */
60
+ readState(): Record<string, unknown> {
61
+ if (!existsSync(this.stateFile)) {
62
+ return {};
63
+ }
64
+ return JSON.parse(readFileSync(this.stateFile, 'utf-8'));
65
+ }
66
+
67
+ /**
68
+ * Check if state file exists
69
+ */
70
+ stateExists(): boolean {
71
+ return existsSync(this.stateFile);
72
+ }
73
+
74
+ /**
75
+ * Clean up all test files
76
+ */
77
+ cleanup(): void {
78
+ try {
79
+ rmSync(this.baseDir, { recursive: true, force: true });
80
+ } catch {
81
+ // Ignore cleanup errors
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Write a file relative to the test base directory
87
+ */
88
+ writeFile(relativePath: string, content: string): void {
89
+ const fullPath = join(this.baseDir, relativePath);
90
+ mkdirSync(join(fullPath, '..'), { recursive: true });
91
+ writeFileSync(fullPath, content);
92
+ }
93
+
94
+ /**
95
+ * Read a file relative to the test base directory
96
+ */
97
+ readFile(relativePath: string): string {
98
+ return readFileSync(join(this.baseDir, relativePath), 'utf-8');
99
+ }
100
+
101
+ /**
102
+ * Check if a file exists relative to the test base directory
103
+ */
104
+ fileExists(relativePath: string): boolean {
105
+ return existsSync(join(this.baseDir, relativePath));
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Run a Python hook as a subprocess
111
+ */
112
+ export async function runPythonHook(
113
+ hookPath: string,
114
+ input: object,
115
+ options: {
116
+ cwd?: string;
117
+ env?: Record<string, string>;
118
+ args?: string[];
119
+ } = {}
120
+ ): Promise<HookResult> {
121
+ const inputJson = JSON.stringify(input);
122
+ const cwd = options.cwd ?? PROJECT_ROOT;
123
+ const args = options.args ?? [];
124
+
125
+ const proc = Bun.spawn(['python3', hookPath, ...args], {
126
+ stdin: new Blob([inputJson]),
127
+ stdout: 'pipe',
128
+ stderr: 'pipe',
129
+ cwd,
130
+ env: { ...process.env, ...options.env },
131
+ });
132
+
133
+ const stdout = await new Response(proc.stdout).text();
134
+ const stderr = await new Response(proc.stderr).text();
135
+ await proc.exited;
136
+
137
+ return {
138
+ stdout: stdout.trim(),
139
+ stderr: stderr.trim(),
140
+ exitCode: proc.exitCode ?? 0,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Run a Bash hook as a subprocess
146
+ */
147
+ export async function runBashHook(
148
+ hookPath: string,
149
+ options: {
150
+ cwd?: string;
151
+ env?: Record<string, string>;
152
+ stdin?: string;
153
+ } = {}
154
+ ): Promise<HookResult> {
155
+ const cwd = options.cwd ?? PROJECT_ROOT;
156
+
157
+ const proc = Bun.spawn(['bash', hookPath], {
158
+ stdin: options.stdin ? new Blob([options.stdin]) : undefined,
159
+ stdout: 'pipe',
160
+ stderr: 'pipe',
161
+ cwd,
162
+ env: { ...process.env, ...options.env },
163
+ });
164
+
165
+ const stdout = await new Response(proc.stdout).text();
166
+ const stderr = await new Response(proc.stderr).text();
167
+ await proc.exited;
168
+
169
+ return {
170
+ stdout: stdout.trim(),
171
+ stderr: stderr.trim(),
172
+ exitCode: proc.exitCode ?? 0,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Run the context monitor Python hook
178
+ */
179
+ export async function runContextMonitor(
180
+ input: object,
181
+ stateManager: TestStateManager
182
+ ): Promise<HookResult> {
183
+ const hookPath = join(HOOKS_DIR, 'ralph_context_monitor.py');
184
+ return runPythonHook(hookPath, input, {
185
+ cwd: stateManager.baseDir,
186
+ env: {
187
+ PYTHONPATH: LIB_DIR,
188
+ },
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Run the ralph stop Bash hook
194
+ */
195
+ export async function runRalphStop(
196
+ stateManager: TestStateManager,
197
+ options: {
198
+ maxIterations?: number;
199
+ transcript?: string;
200
+ enableLogging?: boolean;
201
+ } = {}
202
+ ): Promise<HookResult> {
203
+ const hookPath = join(HOOKS_DIR, 'ralph_stop.sh');
204
+ const stateRelPath = '.claude/ralph-state.json';
205
+
206
+ return runBashHook(hookPath, {
207
+ cwd: stateManager.baseDir,
208
+ env: {
209
+ RALPH_STATE_FILE: stateRelPath,
210
+ RALPH_MAX_ITERATIONS: String(options.maxIterations ?? 50),
211
+ RALPH_ENABLE_LOGGING: options.enableLogging ? 'true' : 'false',
212
+ CLAUDE_TRANSCRIPT: options.transcript ?? '',
213
+ },
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Check if jq is available on the system
219
+ */
220
+ export async function isJqAvailable(): Promise<boolean> {
221
+ try {
222
+ const proc = Bun.spawn(['which', 'jq'], {
223
+ stdout: 'pipe',
224
+ stderr: 'pipe',
225
+ });
226
+ await proc.exited;
227
+ return proc.exitCode === 0;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Check if Python 3 is available on the system
235
+ */
236
+ export async function isPython3Available(): Promise<boolean> {
237
+ try {
238
+ const proc = Bun.spawn(['python3', '--version'], {
239
+ stdout: 'pipe',
240
+ stderr: 'pipe',
241
+ });
242
+ await proc.exited;
243
+ return proc.exitCode === 0;
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Alias for isPython3Available for convenience
251
+ */
252
+ export const isPythonAvailable = isPython3Available;