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,535 @@
1
+ /**
2
+ * Context Monitor Hook Tests
3
+ *
4
+ * Tests for ralph_context_monitor.py threshold detection and state updates.
5
+ * Verifies L0-L3 threshold behavior, checkpoint triggering, and error handling.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterEach, beforeAll } from 'bun:test';
9
+ import { join } from 'path';
10
+ import {
11
+ TestStateManager,
12
+ runPythonHook,
13
+ isPython3Available,
14
+ HOOKS_DIR,
15
+ } from './test-utils';
16
+ import {
17
+ CONTEXT_INPUTS,
18
+ createMinimalRalphState,
19
+ createCheckpointedState,
20
+ } from './fixtures';
21
+
22
+ describe('Context Monitor Hook', () => {
23
+ let stateManager: TestStateManager;
24
+ let python3Available: boolean;
25
+
26
+ beforeAll(async () => {
27
+ python3Available = await isPython3Available();
28
+ });
29
+
30
+ beforeEach(() => {
31
+ stateManager = new TestStateManager('context-monitor');
32
+ stateManager.init();
33
+ });
34
+
35
+ afterEach(() => {
36
+ stateManager.cleanup();
37
+ });
38
+
39
+ describe('L0: Normal Operation (< 70%)', () => {
40
+ test('produces no output at low context', async () => {
41
+ if (!python3Available) {
42
+ console.log('Skipping: Python 3 not available');
43
+ return;
44
+ }
45
+
46
+ // Create Ralph state file to enable monitoring
47
+ stateManager.writeState(createMinimalRalphState({ iteration: 1 }));
48
+
49
+ const result = await runPythonHook(
50
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
51
+ CONTEXT_INPUTS.L0_LOW,
52
+ { cwd: stateManager.baseDir }
53
+ );
54
+
55
+ expect(result.exitCode).toBe(0);
56
+ expect(result.stdout).toBe('');
57
+ });
58
+
59
+ test('updates context history without checkpoint', async () => {
60
+ if (!python3Available) {
61
+ console.log('Skipping: Python 3 not available');
62
+ return;
63
+ }
64
+
65
+ stateManager.writeState(createMinimalRalphState({ iteration: 3 }));
66
+
67
+ await runPythonHook(
68
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
69
+ CONTEXT_INPUTS.L0_MEDIUM,
70
+ { cwd: stateManager.baseDir }
71
+ );
72
+
73
+ const state = stateManager.readState();
74
+ const history = state.context_history as Array<{ iteration: number; peak_percent: number; checkpoint: boolean }>;
75
+
76
+ expect(history).toBeDefined();
77
+ expect(history.length).toBeGreaterThan(0);
78
+ expect(history[history.length - 1]!.checkpoint).toBe(false);
79
+ });
80
+
81
+ test('handles 68% context (just below L1)', async () => {
82
+ if (!python3Available) {
83
+ console.log('Skipping: Python 3 not available');
84
+ return;
85
+ }
86
+
87
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
88
+
89
+ const result = await runPythonHook(
90
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
91
+ CONTEXT_INPUTS.L0_HIGH,
92
+ { cwd: stateManager.baseDir }
93
+ );
94
+
95
+ expect(result.exitCode).toBe(0);
96
+ expect(result.stdout).not.toContain('CCS_WARNING');
97
+ expect(result.stdout).not.toContain('CCS_CHECKPOINT');
98
+ });
99
+ });
100
+
101
+ describe('L1: Warning Zone (70-84%)', () => {
102
+ test('outputs warning at 70% threshold', async () => {
103
+ if (!python3Available) {
104
+ console.log('Skipping: Python 3 not available');
105
+ return;
106
+ }
107
+
108
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
109
+
110
+ const result = await runPythonHook(
111
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
112
+ CONTEXT_INPUTS.L1_LOW,
113
+ { cwd: stateManager.baseDir }
114
+ );
115
+
116
+ expect(result.exitCode).toBe(0);
117
+ expect(result.stdout).toContain('CCS_WARNING');
118
+ expect(result.stdout).toContain('L1');
119
+ expect(result.stdout).toContain('70');
120
+ });
121
+
122
+ test('outputs warning at 77% (mid L1)', async () => {
123
+ if (!python3Available) {
124
+ console.log('Skipping: Python 3 not available');
125
+ return;
126
+ }
127
+
128
+ stateManager.writeState(createMinimalRalphState({ iteration: 7 }));
129
+
130
+ const result = await runPythonHook(
131
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
132
+ CONTEXT_INPUTS.L1_MEDIUM,
133
+ { cwd: stateManager.baseDir }
134
+ );
135
+
136
+ expect(result.exitCode).toBe(0);
137
+ expect(result.stdout).toContain('CCS_WARNING');
138
+ expect(result.stdout).toContain('L1');
139
+ });
140
+
141
+ test('updates history without triggering checkpoint', async () => {
142
+ if (!python3Available) {
143
+ console.log('Skipping: Python 3 not available');
144
+ return;
145
+ }
146
+
147
+ stateManager.writeState(createMinimalRalphState({ iteration: 6 }));
148
+
149
+ await runPythonHook(
150
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
151
+ CONTEXT_INPUTS.L1_HIGH,
152
+ { cwd: stateManager.baseDir }
153
+ );
154
+
155
+ const state = stateManager.readState();
156
+ expect(state.context_checkpoint).toBeUndefined();
157
+
158
+ const history = state.context_history as Array<{ checkpoint: boolean }>;
159
+ expect(history).toBeDefined();
160
+ expect(history[history.length - 1]!.checkpoint).toBe(false);
161
+ });
162
+ });
163
+
164
+ describe('L2: Checkpoint Zone (85-94%)', () => {
165
+ test('triggers checkpoint at 85% threshold', async () => {
166
+ if (!python3Available) {
167
+ console.log('Skipping: Python 3 not available');
168
+ return;
169
+ }
170
+
171
+ stateManager.writeState(createMinimalRalphState({ iteration: 10 }));
172
+
173
+ const result = await runPythonHook(
174
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
175
+ CONTEXT_INPUTS.L2_LOW,
176
+ { cwd: stateManager.baseDir }
177
+ );
178
+
179
+ expect(result.exitCode).toBe(0); // L2 doesn't exit with error
180
+ expect(result.stdout).toContain('CCS_CHECKPOINT_TRIGGERED');
181
+ expect(result.stdout).toContain('L2');
182
+ expect(result.stdout).toContain('85');
183
+ });
184
+
185
+ test('creates checkpoint state at 90%', async () => {
186
+ if (!python3Available) {
187
+ console.log('Skipping: Python 3 not available');
188
+ return;
189
+ }
190
+
191
+ stateManager.writeState(createMinimalRalphState({
192
+ iteration: 12,
193
+ todo_items: ['Current task', 'Next task'],
194
+ }));
195
+
196
+ await runPythonHook(
197
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
198
+ CONTEXT_INPUTS.L2_MEDIUM,
199
+ { cwd: stateManager.baseDir }
200
+ );
201
+
202
+ const state = stateManager.readState();
203
+ const checkpoint = state.context_checkpoint as Record<string, unknown>;
204
+
205
+ expect(checkpoint).toBeDefined();
206
+ expect(checkpoint.active).toBe(true);
207
+ expect(checkpoint.level).toBe('L2');
208
+ expect(checkpoint.percent_at_checkpoint).toBe(90);
209
+ expect(checkpoint.handoff_file).toContain('.claude/handoffs/');
210
+ expect(checkpoint.current_todo_item).toBe('Current task');
211
+ });
212
+
213
+ test('updates context history with checkpoint flag', async () => {
214
+ if (!python3Available) {
215
+ console.log('Skipping: Python 3 not available');
216
+ return;
217
+ }
218
+
219
+ stateManager.writeState(createMinimalRalphState({ iteration: 15 }));
220
+
221
+ await runPythonHook(
222
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
223
+ CONTEXT_INPUTS.L2_HIGH,
224
+ { cwd: stateManager.baseDir }
225
+ );
226
+
227
+ const state = stateManager.readState();
228
+ const history = state.context_history as Array<{
229
+ checkpoint: boolean;
230
+ level: string;
231
+ iteration: number;
232
+ }>;
233
+
234
+ expect(history).toBeDefined();
235
+ const lastEntry = history[history.length - 1]!;
236
+ expect(lastEntry.checkpoint).toBe(true);
237
+ expect(lastEntry.level).toBe('L2');
238
+ });
239
+ });
240
+
241
+ describe('L3: Emergency Zone (95%+)', () => {
242
+ test('triggers emergency stop at 95%', async () => {
243
+ if (!python3Available) {
244
+ console.log('Skipping: Python 3 not available');
245
+ return;
246
+ }
247
+
248
+ stateManager.writeState(createMinimalRalphState({ iteration: 20 }));
249
+
250
+ const result = await runPythonHook(
251
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
252
+ CONTEXT_INPUTS.L3_LOW,
253
+ { cwd: stateManager.baseDir }
254
+ );
255
+
256
+ expect(result.exitCode).toBe(1); // L3 exits with error
257
+ expect(result.stdout).toContain('CCS_EMERGENCY_STOP');
258
+ expect(result.stdout).toContain('L3');
259
+ expect(result.stdout).toContain('95');
260
+ });
261
+
262
+ test('triggers emergency stop at 99%', async () => {
263
+ if (!python3Available) {
264
+ console.log('Skipping: Python 3 not available');
265
+ return;
266
+ }
267
+
268
+ stateManager.writeState(createMinimalRalphState({ iteration: 25 }));
269
+
270
+ const result = await runPythonHook(
271
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
272
+ CONTEXT_INPUTS.L3_HIGH,
273
+ { cwd: stateManager.baseDir }
274
+ );
275
+
276
+ expect(result.exitCode).toBe(1);
277
+ expect(result.stdout).toContain('CCS_EMERGENCY_STOP');
278
+ expect(result.stdout).toContain('L3');
279
+ });
280
+
281
+ test('creates L3 checkpoint state', async () => {
282
+ if (!python3Available) {
283
+ console.log('Skipping: Python 3 not available');
284
+ return;
285
+ }
286
+
287
+ stateManager.writeState(createMinimalRalphState({ iteration: 30 }));
288
+
289
+ await runPythonHook(
290
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
291
+ CONTEXT_INPUTS.L3_MEDIUM,
292
+ { cwd: stateManager.baseDir }
293
+ );
294
+
295
+ const state = stateManager.readState();
296
+ const checkpoint = state.context_checkpoint as Record<string, unknown>;
297
+
298
+ expect(checkpoint).toBeDefined();
299
+ expect(checkpoint.active).toBe(true);
300
+ expect(checkpoint.level).toBe('L3');
301
+ });
302
+ });
303
+
304
+ describe('Checkpoint Already Active', () => {
305
+ test('does not re-trigger when checkpoint is active', async () => {
306
+ if (!python3Available) {
307
+ console.log('Skipping: Python 3 not available');
308
+ return;
309
+ }
310
+
311
+ // Create state with active checkpoint
312
+ stateManager.writeState(createCheckpointedState('L2', 87));
313
+
314
+ const result = await runPythonHook(
315
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
316
+ CONTEXT_INPUTS.L2_HIGH, // Would normally trigger checkpoint
317
+ { cwd: stateManager.baseDir }
318
+ );
319
+
320
+ expect(result.exitCode).toBe(0);
321
+ expect(result.stdout).toBe(''); // No output when checkpoint already active
322
+ });
323
+
324
+ test('does not upgrade L2 to L3 when checkpoint active', async () => {
325
+ if (!python3Available) {
326
+ console.log('Skipping: Python 3 not available');
327
+ return;
328
+ }
329
+
330
+ // Create state with active L2 checkpoint
331
+ stateManager.writeState(createCheckpointedState('L2', 87));
332
+
333
+ const result = await runPythonHook(
334
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
335
+ CONTEXT_INPUTS.L3_LOW, // Would normally trigger L3
336
+ { cwd: stateManager.baseDir }
337
+ );
338
+
339
+ expect(result.exitCode).toBe(0); // Should not exit with error
340
+ expect(result.stdout).not.toContain('CCS_EMERGENCY_STOP');
341
+
342
+ // Checkpoint should still be L2
343
+ const state = stateManager.readState();
344
+ const checkpoint = state.context_checkpoint as Record<string, unknown>;
345
+ expect(checkpoint.level).toBe('L2');
346
+ });
347
+ });
348
+
349
+ describe('Non-Ralph Mode (No State File)', () => {
350
+ test('exits silently without state file', async () => {
351
+ if (!python3Available) {
352
+ console.log('Skipping: Python 3 not available');
353
+ return;
354
+ }
355
+
356
+ // Don't create any state file
357
+ const result = await runPythonHook(
358
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
359
+ CONTEXT_INPUTS.L3_HIGH, // Would trigger emergency if active
360
+ { cwd: stateManager.baseDir }
361
+ );
362
+
363
+ expect(result.exitCode).toBe(0);
364
+ expect(result.stdout).toBe('');
365
+ });
366
+
367
+ test('does not create state file', async () => {
368
+ if (!python3Available) {
369
+ console.log('Skipping: Python 3 not available');
370
+ return;
371
+ }
372
+
373
+ await runPythonHook(
374
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
375
+ CONTEXT_INPUTS.L2_MEDIUM,
376
+ { cwd: stateManager.baseDir }
377
+ );
378
+
379
+ expect(stateManager.stateExists()).toBe(false);
380
+ });
381
+ });
382
+
383
+ describe('Error Handling', () => {
384
+ test('handles malformed JSON input gracefully', async () => {
385
+ if (!python3Available) {
386
+ console.log('Skipping: Python 3 not available');
387
+ return;
388
+ }
389
+
390
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
391
+
392
+ const hookPath = join(HOOKS_DIR, 'ralph_context_monitor.py');
393
+ const proc = Bun.spawn(['python3', hookPath], {
394
+ stdin: new Blob(['not valid json at all']),
395
+ stdout: 'pipe',
396
+ stderr: 'pipe',
397
+ cwd: stateManager.baseDir,
398
+ });
399
+
400
+ const stdout = await new Response(proc.stdout).text();
401
+ await proc.exited;
402
+
403
+ expect(proc.exitCode).toBe(0); // Should not crash
404
+ expect(stdout.trim()).toBe(''); // No output on error
405
+ });
406
+
407
+ test('handles empty input gracefully', async () => {
408
+ if (!python3Available) {
409
+ console.log('Skipping: Python 3 not available');
410
+ return;
411
+ }
412
+
413
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
414
+
415
+ const hookPath = join(HOOKS_DIR, 'ralph_context_monitor.py');
416
+ const proc = Bun.spawn(['python3', hookPath], {
417
+ stdin: new Blob(['']),
418
+ stdout: 'pipe',
419
+ stderr: 'pipe',
420
+ cwd: stateManager.baseDir,
421
+ });
422
+
423
+ await proc.exited;
424
+ expect(proc.exitCode).toBe(0);
425
+ });
426
+
427
+ test('handles missing context_window gracefully', async () => {
428
+ if (!python3Available) {
429
+ console.log('Skipping: Python 3 not available');
430
+ return;
431
+ }
432
+
433
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
434
+
435
+ const result = await runPythonHook(
436
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
437
+ { some_other_field: 'value' },
438
+ { cwd: stateManager.baseDir }
439
+ );
440
+
441
+ expect(result.exitCode).toBe(0);
442
+ // Should calculate 0% and produce no output
443
+ expect(result.stdout).toBe('');
444
+ });
445
+
446
+ test('handles zero context_window_size gracefully', async () => {
447
+ if (!python3Available) {
448
+ console.log('Skipping: Python 3 not available');
449
+ return;
450
+ }
451
+
452
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
453
+
454
+ const result = await runPythonHook(
455
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
456
+ {
457
+ context_window: {
458
+ current_usage: { input_tokens: 100 },
459
+ context_window_size: 0, // Would cause division by zero
460
+ },
461
+ },
462
+ { cwd: stateManager.baseDir }
463
+ );
464
+
465
+ expect(result.exitCode).toBe(0);
466
+ });
467
+ });
468
+
469
+ describe('Context History Accumulation', () => {
470
+ test('accumulates history across multiple calls', async () => {
471
+ if (!python3Available) {
472
+ console.log('Skipping: Python 3 not available');
473
+ return;
474
+ }
475
+
476
+ // Start with iteration 1
477
+ stateManager.writeState(createMinimalRalphState({ iteration: 1 }));
478
+
479
+ // First call at 30%
480
+ await runPythonHook(
481
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
482
+ CONTEXT_INPUTS.L0_LOW,
483
+ { cwd: stateManager.baseDir }
484
+ );
485
+
486
+ // Update iteration and call again at 50%
487
+ const state1 = stateManager.readState();
488
+ stateManager.writeState({ ...state1, iteration: 2 });
489
+
490
+ await runPythonHook(
491
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
492
+ CONTEXT_INPUTS.L0_MEDIUM,
493
+ { cwd: stateManager.baseDir }
494
+ );
495
+
496
+ // Check history has both entries
497
+ const finalState = stateManager.readState();
498
+ const history = finalState.context_history as Array<{ iteration: number }>;
499
+
500
+ expect(history.length).toBe(2);
501
+ expect(history[0]!.iteration).toBe(1);
502
+ expect(history[1]!.iteration).toBe(2);
503
+ });
504
+
505
+ test('updates peak_percent for same iteration', async () => {
506
+ if (!python3Available) {
507
+ console.log('Skipping: Python 3 not available');
508
+ return;
509
+ }
510
+
511
+ stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
512
+
513
+ // First call at 30%
514
+ await runPythonHook(
515
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
516
+ CONTEXT_INPUTS.L0_LOW,
517
+ { cwd: stateManager.baseDir }
518
+ );
519
+
520
+ // Second call at 50% (same iteration)
521
+ await runPythonHook(
522
+ join(HOOKS_DIR, 'ralph_context_monitor.py'),
523
+ CONTEXT_INPUTS.L0_MEDIUM,
524
+ { cwd: stateManager.baseDir }
525
+ );
526
+
527
+ const state = stateManager.readState();
528
+ const history = state.context_history as Array<{ peak_percent: number }>;
529
+
530
+ // Should have only one entry with the higher peak
531
+ expect(history.length).toBe(1);
532
+ expect(history[0]!.peak_percent).toBe(50);
533
+ });
534
+ });
535
+ });