anvil-dev-framework 0.1.7 → 0.1.9

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 (143) hide show
  1. package/README.md +71 -22
  2. package/VERSION +1 -1
  3. package/docs/ANV-263-hook-logging-investigation.md +116 -0
  4. package/docs/command-reference.md +398 -17
  5. package/docs/session-workflow.md +62 -9
  6. package/docs/system-architecture.md +584 -0
  7. package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
  8. package/global/api/openapi.yaml +357 -0
  9. package/global/api/ralph_api.py +528 -0
  10. package/global/commands/anvil-settings.md +47 -19
  11. package/global/commands/audit.md +163 -0
  12. package/global/commands/checklist.md +180 -0
  13. package/global/commands/coderabbit-fix.md +282 -0
  14. package/global/commands/efficiency.md +356 -0
  15. package/global/commands/evidence.md +117 -33
  16. package/global/commands/hud.md +24 -0
  17. package/global/commands/insights.md +101 -3
  18. package/global/commands/orient.md +22 -21
  19. package/global/commands/patterns.md +115 -0
  20. package/global/commands/ralph.md +47 -1
  21. package/global/commands/token-budget.md +214 -0
  22. package/global/commands/weekly-review.md +21 -1
  23. package/global/config/notifications.yaml.template +50 -0
  24. package/global/hooks/ralph_stop.sh +33 -1
  25. package/global/hooks/statusline.sh +67 -2
  26. package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
  27. package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
  28. package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
  29. package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
  30. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  31. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  32. package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
  33. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  34. package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
  35. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  36. package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
  37. package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
  38. package/global/lib/coderabbit_metrics.py +647 -0
  39. package/global/lib/command_tracker.py +147 -0
  40. package/global/lib/context_optimizer.py +323 -0
  41. package/global/lib/linear_provider.py +210 -16
  42. package/global/lib/log_rotation.py +287 -0
  43. package/global/lib/optimization_applier.py +582 -0
  44. package/global/lib/ralph_events.py +398 -0
  45. package/global/lib/ralph_notifier.py +366 -0
  46. package/global/lib/ralph_state.py +264 -24
  47. package/global/lib/ralph_webhooks.py +470 -0
  48. package/global/lib/state_manager.py +121 -0
  49. package/global/lib/token_analyzer.py +1383 -0
  50. package/global/lib/token_metrics.py +919 -0
  51. package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
  52. package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
  53. package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
  54. package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  55. package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
  56. package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
  57. package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  58. package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  59. package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
  60. package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
  61. package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
  62. package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
  63. package/global/tests/test_command_tracker.py +172 -0
  64. package/global/tests/test_context_optimizer.py +321 -0
  65. package/global/tests/test_linear_filtering.py +319 -0
  66. package/global/tests/test_linear_provider.py +40 -1
  67. package/global/tests/test_optimization_applier.py +508 -0
  68. package/global/tests/test_token_analyzer.py +735 -0
  69. package/global/tests/test_token_analyzer_phase6.py +537 -0
  70. package/global/tests/test_token_metrics.py +829 -0
  71. package/global/tools/README.md +153 -0
  72. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  73. package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
  74. package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
  75. package/global/tools/anvil-hud.py +86 -1
  76. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
  77. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
  78. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
  79. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
  80. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
  81. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
  82. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
  83. package/global/tools/anvil-memory/src/commands/context.ts +322 -0
  84. package/global/tools/anvil-memory/src/db.ts +108 -0
  85. package/global/tools/anvil-memory/src/index.ts +2 -8
  86. package/global/tools/orient_linear.py +159 -0
  87. package/global/tools/ralph-watch +423 -0
  88. package/package.json +2 -1
  89. package/project/.anvil-project.yaml.template +93 -0
  90. package/project/CLAUDE.md.template +343 -0
  91. package/project/agents/README.md +119 -0
  92. package/project/agents/cross-layer-debugger.md +217 -0
  93. package/project/agents/security-code-reviewer.md +162 -0
  94. package/project/constitution.md.template +235 -0
  95. package/project/coordination.md +103 -0
  96. package/project/docs/background-tasks.md +258 -0
  97. package/project/docs/skills-frontmatter.md +243 -0
  98. package/project/examples/README.md +106 -0
  99. package/project/examples/api-route-template.ts +171 -0
  100. package/project/examples/component-template.tsx +110 -0
  101. package/project/examples/hook-template.ts +152 -0
  102. package/project/examples/service-template.ts +207 -0
  103. package/project/examples/test-template.test.tsx +249 -0
  104. package/project/hooks/README.md +491 -0
  105. package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
  106. package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
  107. package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
  108. package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  109. package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  110. package/project/hooks/notification.py +183 -0
  111. package/project/hooks/permission_request.py +438 -0
  112. package/project/hooks/post_tool_use.py +397 -0
  113. package/project/hooks/pre_compact.py +126 -0
  114. package/project/hooks/pre_tool_use.py +454 -0
  115. package/project/hooks/session_start.py +656 -0
  116. package/project/hooks/stop.py +356 -0
  117. package/project/hooks/subagent_start.py +223 -0
  118. package/project/hooks/subagent_stop.py +215 -0
  119. package/project/hooks/user_prompt_submit.py +110 -0
  120. package/project/hooks/utils/llm/anth.py +114 -0
  121. package/project/hooks/utils/llm/oai.py +114 -0
  122. package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
  123. package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
  124. package/project/hooks/utils/tts/openai_tts.py +92 -0
  125. package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
  126. package/project/linear.yaml.template +23 -0
  127. package/project/product.md.template +238 -0
  128. package/project/retros/README.md +126 -0
  129. package/project/rules/README.md +90 -0
  130. package/project/rules/debugging.md +139 -0
  131. package/project/rules/security-review.md +115 -0
  132. package/project/settings.yaml.template +185 -0
  133. package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
  134. package/project/templates/api-python/CLAUDE.md +547 -0
  135. package/project/templates/generic/CLAUDE.md +260 -0
  136. package/project/templates/saas/CLAUDE.md +478 -0
  137. package/project/tests/README.md +140 -0
  138. package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
  139. package/project/tests/fixtures/sample-transcript.jsonl +21 -0
  140. package/project/tests/test-hooks.sh +259 -0
  141. package/project/tests/test-lib.sh +248 -0
  142. package/project/tests/test-statusline.sh +165 -0
  143. package/project/tests/test_transcript_parser.py +323 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * CCS Test Utilities
3
+ *
4
+ * Shared helpers for Context Checkpoint System (CCS) end-to-end testing.
5
+ * Provides utilities for Ralph state management, hook execution, and fixtures.
6
+ */
7
+
8
+ import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { tmpdir } from 'os';
11
+
12
+ /**
13
+ * Project root directory (anvil-dev-framework)
14
+ */
15
+ export const PROJECT_ROOT = join(__dirname, '../../../../../..');
16
+
17
+ /**
18
+ * Path to the global hooks directory
19
+ */
20
+ export const HOOKS_DIR = join(PROJECT_ROOT, 'global/hooks');
21
+
22
+ /**
23
+ * Path to the global lib directory
24
+ */
25
+ export const LIB_DIR = join(PROJECT_ROOT, 'global/lib');
26
+
27
+ /**
28
+ * CCS threshold constants matching ralph_context_monitor.py
29
+ */
30
+ export const CCS_THRESHOLDS = {
31
+ L1: 70,
32
+ L2: 85,
33
+ L3: 95,
34
+ } as const;
35
+
36
+ /**
37
+ * Result from running a hook subprocess
38
+ */
39
+ export interface HookResult {
40
+ stdout: string;
41
+ stderr: string;
42
+ exitCode: number;
43
+ }
44
+
45
+ /**
46
+ * Context history entry structure
47
+ */
48
+ export interface ContextHistoryEntry {
49
+ iteration: number;
50
+ peak_percent: number;
51
+ checkpoint: boolean;
52
+ level?: string;
53
+ timestamp?: string;
54
+ }
55
+
56
+ /**
57
+ * Context checkpoint structure
58
+ */
59
+ export interface ContextCheckpoint {
60
+ active: boolean;
61
+ level: string;
62
+ percent_at_checkpoint: number;
63
+ timestamp: string;
64
+ handoff_file?: string;
65
+ }
66
+
67
+ /**
68
+ * Ralph state file structure
69
+ */
70
+ export interface RalphState {
71
+ mode: 'ralph' | 'manual';
72
+ session_id: string;
73
+ iteration: number;
74
+ started_at: string;
75
+ status: 'running' | 'checkpointed' | 'completed' | 'failed';
76
+ task_list?: string[];
77
+ completed_tasks?: string[];
78
+ checkpoint_active?: boolean;
79
+ context_history?: ContextHistoryEntry[];
80
+ context_checkpoint?: ContextCheckpoint;
81
+ handoff_file?: string;
82
+ linear_issue?: string;
83
+ // Circuit breaker fields (used by ralph_stop.sh)
84
+ no_change_count?: number;
85
+ last_diff_hash?: string;
86
+ }
87
+
88
+ /**
89
+ * Context window data structure from Claude Code
90
+ */
91
+ export interface ContextWindowData {
92
+ context_window: {
93
+ current_usage: {
94
+ input_tokens: number;
95
+ output_tokens: number;
96
+ cache_read_input_tokens: number;
97
+ cache_creation_input_tokens: number;
98
+ };
99
+ context_window_size: number;
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Creates a temporary test directory for CCS tests.
105
+ * Returns path and cleanup function.
106
+ */
107
+ export function createTestDir(prefix = 'ccs-test'): {
108
+ path: string;
109
+ cleanup: () => void;
110
+ } {
111
+ const path = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
112
+ mkdirSync(path, { recursive: true });
113
+
114
+ return {
115
+ path,
116
+ cleanup: () => {
117
+ try {
118
+ rmSync(path, { recursive: true, force: true });
119
+ } catch {
120
+ // Ignore cleanup errors
121
+ }
122
+ },
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Creates a Ralph state file in the specified directory
128
+ */
129
+ export function createRalphStateFile(dir: string, state: Partial<RalphState>): string {
130
+ const stateDir = join(dir, '.claude');
131
+ mkdirSync(stateDir, { recursive: true });
132
+
133
+ const statePath = join(stateDir, 'ralph-state.json');
134
+ const fullState: RalphState = {
135
+ mode: 'ralph',
136
+ session_id: `test-${Date.now()}`,
137
+ iteration: 1,
138
+ started_at: new Date().toISOString(),
139
+ status: 'running',
140
+ ...state,
141
+ };
142
+
143
+ writeFileSync(statePath, JSON.stringify(fullState, null, 2));
144
+ return statePath;
145
+ }
146
+
147
+ /**
148
+ * Reads a Ralph state file from the specified directory
149
+ */
150
+ export function readRalphStateFile(dir: string): RalphState | null {
151
+ const statePath = join(dir, '.claude', 'ralph-state.json');
152
+ if (!existsSync(statePath)) {
153
+ return null;
154
+ }
155
+ try {
156
+ return JSON.parse(readFileSync(statePath, 'utf-8'));
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Creates context window data at a specific percentage
164
+ */
165
+ export function createContextData(percent: number, windowSize = 200000): ContextWindowData {
166
+ // Calculate tokens to achieve target percentage
167
+ // percent = (input_tokens + cache_creation_input_tokens) / context_window_size * 100
168
+ const targetTokens = Math.floor((percent / 100) * windowSize);
169
+
170
+ return {
171
+ context_window: {
172
+ current_usage: {
173
+ input_tokens: targetTokens,
174
+ output_tokens: 0,
175
+ cache_read_input_tokens: 0,
176
+ cache_creation_input_tokens: 0,
177
+ },
178
+ context_window_size: windowSize,
179
+ },
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Runs a Python hook as a subprocess with stdin input
185
+ */
186
+ export async function runPythonHook(
187
+ hookPath: string,
188
+ input: ContextWindowData | Record<string, unknown>,
189
+ options?: {
190
+ cwd?: string;
191
+ env?: Record<string, string>;
192
+ }
193
+ ): Promise<HookResult> {
194
+ const inputJson = JSON.stringify(input);
195
+ const env = { ...process.env, ...options?.env };
196
+
197
+ const proc = Bun.spawn(['python3', hookPath], {
198
+ stdin: new Blob([inputJson]),
199
+ stdout: 'pipe',
200
+ stderr: 'pipe',
201
+ cwd: options?.cwd ?? PROJECT_ROOT,
202
+ env,
203
+ });
204
+
205
+ const stdout = await new Response(proc.stdout).text();
206
+ const stderr = await new Response(proc.stderr).text();
207
+ await proc.exited;
208
+
209
+ // Use 128 for signal termination (Unix convention), -1 for unknown failures
210
+ const exitCode = proc.exitCode ?? (proc.signalCode ? 128 : -1);
211
+ return {
212
+ stdout: stdout.trim(),
213
+ stderr: stderr.trim(),
214
+ exitCode,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Runs a Bash hook as a subprocess
220
+ */
221
+ export async function runBashHook(
222
+ hookPath: string,
223
+ options?: {
224
+ cwd?: string;
225
+ env?: Record<string, string>;
226
+ stdin?: string;
227
+ }
228
+ ): Promise<HookResult> {
229
+ const env = { ...process.env, ...options?.env };
230
+
231
+ const proc = Bun.spawn(['bash', hookPath], {
232
+ stdin: options?.stdin ? new Blob([options.stdin]) : undefined,
233
+ stdout: 'pipe',
234
+ stderr: 'pipe',
235
+ cwd: options?.cwd ?? PROJECT_ROOT,
236
+ env,
237
+ });
238
+
239
+ const stdout = await new Response(proc.stdout).text();
240
+ const stderr = await new Response(proc.stderr).text();
241
+ await proc.exited;
242
+
243
+ // Use 128 for signal termination (Unix convention), -1 for unknown failures
244
+ const exitCode = proc.exitCode ?? (proc.signalCode ? 128 : -1);
245
+ return {
246
+ stdout: stdout.trim(),
247
+ stderr: stderr.trim(),
248
+ exitCode,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Parses CCS output signals from hook stdout
254
+ *
255
+ * Signals have format: CCS_<TYPE>|<LEVEL>|<PERCENT>|<MESSAGE>
256
+ */
257
+ export function parseCCSSignal(stdout: string): {
258
+ type: 'WARNING' | 'CHECKPOINT_TRIGGERED' | 'EMERGENCY_STOP' | null;
259
+ level: string;
260
+ percent: number;
261
+ message: string;
262
+ } | null {
263
+ const signalPatterns = [
264
+ /CCS_WARNING\|(\w+)\|(\d+)\|(.+)/,
265
+ /CCS_CHECKPOINT_TRIGGERED\|(\w+)\|(\d+)\|(.+)/,
266
+ /CCS_EMERGENCY_STOP\|(\w+)\|(\d+)\|(.+)/,
267
+ ];
268
+
269
+ for (const line of stdout.split('\n')) {
270
+ for (const pattern of signalPatterns) {
271
+ const match = line.match(pattern);
272
+ if (match) {
273
+ const type = line.startsWith('CCS_WARNING')
274
+ ? 'WARNING'
275
+ : line.startsWith('CCS_CHECKPOINT')
276
+ ? 'CHECKPOINT_TRIGGERED'
277
+ : 'EMERGENCY_STOP';
278
+ return {
279
+ type,
280
+ level: match[1]!,
281
+ percent: parseInt(match[2]!, 10),
282
+ message: match[3]!,
283
+ };
284
+ }
285
+ }
286
+ }
287
+
288
+ return null;
289
+ }
290
+
291
+ /**
292
+ * Checks if jq is available on the system (needed for some bash hooks)
293
+ */
294
+ export async function isJqAvailable(): Promise<boolean> {
295
+ try {
296
+ const proc = Bun.spawn(['which', 'jq'], {
297
+ stdout: 'pipe',
298
+ stderr: 'pipe',
299
+ });
300
+ await proc.exited;
301
+ return proc.exitCode === 0;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Creates a sample handoff document for testing
309
+ */
310
+ export function createHandoffFile(dir: string, content?: string): string {
311
+ const handoffDir = join(dir, '.claude', 'handoffs');
312
+ mkdirSync(handoffDir, { recursive: true });
313
+
314
+ const timestamp = new Date().toISOString().slice(0, 16).replace('T', '-').replace(':', '');
315
+ const handoffPath = join(handoffDir, `${timestamp}.md`);
316
+
317
+ const defaultContent = `# Session Handoff
318
+
319
+ ## Context at Checkpoint
320
+ - Context: 87%
321
+ - Iteration: 3
322
+
323
+ ## Current Task
324
+ Working on feature implementation
325
+
326
+ ## Completed Items
327
+ - Item 1
328
+ - Item 2
329
+
330
+ ## Remaining Items
331
+ - Item 3
332
+ - Item 4
333
+
334
+ ## Next Steps
335
+ Continue with Item 3
336
+ `;
337
+
338
+ writeFileSync(handoffPath, content ?? defaultContent);
339
+ return handoffPath;
340
+ }
@@ -16,6 +16,7 @@ import { handleSearch, parseSearchArgs } from '../commands/search';
16
16
  import { handleGet, parseGetArgs } from '../commands/get';
17
17
  import { handleCheckpoint, parseCheckpointArgs } from '../commands/checkpoint';
18
18
  import { handleRalphIteration, parseRalphIterationArgs } from '../commands/ralph-iteration';
19
+ import { handleContext, parseContextArgs } from '../commands/context';
19
20
  import { AnvilMemoryDb } from '../db';
20
21
  import type { Observation, Session, Checkpoint, RalphIteration } from '../types';
21
22
 
@@ -57,6 +58,14 @@ interface RalphIterationResultData {
57
58
  observation?: Observation;
58
59
  }
59
60
 
61
+ interface ContextResultData {
62
+ count: number;
63
+ format: string;
64
+ estimatedTokens?: number;
65
+ observations?: Observation[];
66
+ content?: string;
67
+ }
68
+
60
69
  // Test database path
61
70
  const TEST_DB_DIR = join(tmpdir(), 'anvil-memory-cmd-tests');
62
71
  const getTestDbPath = () => join(TEST_DB_DIR, `cmd-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
@@ -866,6 +875,208 @@ describe('ralph-iteration command', () => {
866
875
  });
867
876
  });
868
877
 
878
+ describe('context command', () => {
879
+ let testDbPath: string;
880
+
881
+ beforeAll(async () => {
882
+ if (!existsSync(TEST_DB_DIR)) {
883
+ mkdirSync(TEST_DB_DIR, { recursive: true });
884
+ }
885
+ testDbPath = getTestDbPath();
886
+ await handleInit(['--path', testDbPath]);
887
+
888
+ // Add test observations
889
+ await handleObserve([
890
+ '--type', 'discovery',
891
+ '--title', 'SQLite FTS5 implementation',
892
+ '--content', 'Full-text search with Porter stemmer',
893
+ '--project', 'anvil-memory',
894
+ '--path', testDbPath,
895
+ ]);
896
+
897
+ await handleObserve([
898
+ '--type', 'bugfix',
899
+ '--title', 'Fixed database connection leak',
900
+ '--content', 'Resolved connection leak issue in production',
901
+ '--project', 'anvil-memory',
902
+ '--path', testDbPath,
903
+ ]);
904
+
905
+ await handleObserve([
906
+ '--type', 'feature',
907
+ '--title', 'Authentication system',
908
+ '--content', 'JWT-based auth implementation for API',
909
+ '--project', 'other-project',
910
+ '--path', testDbPath,
911
+ ]);
912
+
913
+ await handleObserve([
914
+ '--type', 'decision',
915
+ '--title', 'Chose SQLite over PostgreSQL',
916
+ '--content', 'Selected SQLite for simplicity and portability',
917
+ '--project', 'anvil-memory',
918
+ '--path', testDbPath,
919
+ ]);
920
+ });
921
+
922
+ afterAll(() => {
923
+ cleanupDb(testDbPath);
924
+ });
925
+
926
+ test('parseContextArgs parses --limit option', () => {
927
+ const opts = parseContextArgs(['--limit', '10']);
928
+ expect(opts.limit).toBe(10);
929
+ });
930
+
931
+ test('parseContextArgs parses --format option', () => {
932
+ const opts = parseContextArgs(['--format', 'markdown']);
933
+ expect(opts.format).toBe('markdown');
934
+ });
935
+
936
+ test('parseContextArgs parses --format json', () => {
937
+ const opts = parseContextArgs(['--format', 'json']);
938
+ expect(opts.format).toBe('json');
939
+ });
940
+
941
+ test('parseContextArgs parses --max-tokens option', () => {
942
+ const opts = parseContextArgs(['--max-tokens', '2000']);
943
+ expect(opts.maxTokens).toBe(2000);
944
+ });
945
+
946
+ test('parseContextArgs parses --project option', () => {
947
+ const opts = parseContextArgs(['--project', 'test-project']);
948
+ expect(opts.project).toBe('test-project');
949
+ });
950
+
951
+ test('parseContextArgs parses --types option', () => {
952
+ const opts = parseContextArgs(['--types', 'bugfix,feature,discovery']);
953
+ expect(opts.types).toEqual(['bugfix', 'feature', 'discovery']);
954
+ });
955
+
956
+ test('parseContextArgs parses --include-sessions option', () => {
957
+ const opts = parseContextArgs(['--include-sessions']);
958
+ expect(opts.includeSessions).toBe(true);
959
+ });
960
+
961
+ test('parseContextArgs defaults to inject format', () => {
962
+ const opts = parseContextArgs([]);
963
+ expect(opts.format).toBe('inject');
964
+ });
965
+
966
+ test('parseContextArgs defaults to limit 20', () => {
967
+ const opts = parseContextArgs([]);
968
+ expect(opts.limit).toBe(20);
969
+ });
970
+
971
+ test('handleContext returns observations in inject format by default', async () => {
972
+ const result = await handleContext(['--path', testDbPath]);
973
+
974
+ expect(result.success).toBe(true);
975
+ expect(result.message).toContain('Recent Context from Anvil Memory');
976
+ expect(result.message).toContain('Index');
977
+ expect(result.message).toContain('Legend');
978
+ });
979
+
980
+ test('handleContext inject format includes observation IDs', async () => {
981
+ const result = await handleContext(['--path', testDbPath]);
982
+
983
+ expect(result.success).toBe(true);
984
+ expect(result.message).toMatch(/#\d+/); // Should have IDs like #1, #2, etc.
985
+ });
986
+
987
+ test('handleContext inject format includes type emojis', async () => {
988
+ const result = await handleContext(['--path', testDbPath]);
989
+
990
+ expect(result.success).toBe(true);
991
+ // Should include at least one emoji from our test data
992
+ expect(result.message).toMatch(/🔴|🟣|🔵|⚖️/);
993
+ });
994
+
995
+ test('handleContext returns JSON format', async () => {
996
+ const result = await handleContext(['--format', 'json', '--path', testDbPath]);
997
+ const data = result.data as ContextResultData;
998
+
999
+ expect(result.success).toBe(true);
1000
+ expect(data?.count).toBeGreaterThan(0);
1001
+ expect(data?.format).toBe('json');
1002
+ expect(data?.observations).toBeDefined();
1003
+ expect(Array.isArray(data?.observations)).toBe(true);
1004
+ });
1005
+
1006
+ test('handleContext returns markdown format', async () => {
1007
+ const result = await handleContext(['--format', 'markdown', '--path', testDbPath]);
1008
+
1009
+ expect(result.success).toBe(true);
1010
+ expect(result.message).toContain('# Session Context');
1011
+ expect(result.message).toContain('Generated:');
1012
+ });
1013
+
1014
+ test('handleContext respects --limit option', async () => {
1015
+ const result = await handleContext(['--format', 'json', '--limit', '2', '--path', testDbPath]);
1016
+ const data = result.data as ContextResultData;
1017
+
1018
+ expect(result.success).toBe(true);
1019
+ expect(data?.observations?.length).toBeLessThanOrEqual(2);
1020
+ });
1021
+
1022
+ test('handleContext filters by --project', async () => {
1023
+ const result = await handleContext(['--format', 'json', '--project', 'anvil-memory', '--path', testDbPath]);
1024
+ const data = result.data as ContextResultData;
1025
+
1026
+ expect(result.success).toBe(true);
1027
+ expect(data?.count).toBeGreaterThan(0);
1028
+ expect(data?.observations?.every((o) => o.project === 'anvil-memory')).toBe(true);
1029
+ });
1030
+
1031
+ test('handleContext filters by --types', async () => {
1032
+ const result = await handleContext(['--format', 'json', '--types', 'bugfix', '--path', testDbPath]);
1033
+ const data = result.data as ContextResultData;
1034
+
1035
+ expect(result.success).toBe(true);
1036
+ if (data?.count > 0) {
1037
+ expect(data?.observations?.every((o) => o.type === 'bugfix')).toBe(true);
1038
+ }
1039
+ });
1040
+
1041
+ test('handleContext handles empty results', async () => {
1042
+ const result = await handleContext(['--format', 'json', '--types', 'handoff', '--path', testDbPath]);
1043
+ const data = result.data as ContextResultData;
1044
+
1045
+ expect(result.success).toBe(true);
1046
+ expect(data?.count).toBe(0);
1047
+ });
1048
+
1049
+ test('handleContext includes token estimate in inject format', async () => {
1050
+ const result = await handleContext(['--path', testDbPath]);
1051
+
1052
+ expect(result.success).toBe(true);
1053
+ expect(result.message).toMatch(/\d+ observations loaded/);
1054
+ expect(result.message).toMatch(/estimated \d+ tokens/);
1055
+ });
1056
+
1057
+ test('handleContext truncates with --max-tokens', async () => {
1058
+ // Get full output first
1059
+ const fullResult = await handleContext(['--format', 'markdown', '--path', testDbPath]);
1060
+ const fullLength = (fullResult.message || '').length;
1061
+
1062
+ // Get truncated output with very small token limit
1063
+ const truncatedResult = await handleContext(['--format', 'markdown', '--max-tokens', '50', '--path', testDbPath]);
1064
+ const truncatedLength = (truncatedResult.message || '').length;
1065
+
1066
+ expect(truncatedResult.success).toBe(true);
1067
+ // Truncated should be smaller than full (50 tokens ≈ 200 chars)
1068
+ expect(truncatedLength).toBeLessThan(fullLength);
1069
+ expect(truncatedResult.message).toContain('Truncated');
1070
+ });
1071
+
1072
+ test('handleContext fails when database does not exist', async () => {
1073
+ const result = await handleContext(['--path', '/tmp/nonexistent-db-12345.db']);
1074
+
1075
+ expect(result.success).toBe(false);
1076
+ expect(result.error).toContain('Database not found');
1077
+ });
1078
+ });
1079
+
869
1080
  describe('Command error handling', () => {
870
1081
  test('observe fails when database does not exist', async () => {
871
1082
  const result = await handleObserve([
@@ -891,4 +1102,11 @@ describe('Command error handling', () => {
891
1102
  expect(result.success).toBe(false);
892
1103
  expect(result.error).toContain('Database not found');
893
1104
  });
1105
+
1106
+ test('context fails when database does not exist', async () => {
1107
+ const result = await handleContext(['--path', '/tmp/nonexistent-db-12345.db']);
1108
+
1109
+ expect(result.success).toBe(false);
1110
+ expect(result.error).toContain('Database not found');
1111
+ });
894
1112
  });