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.
- package/README.md +71 -22
- package/VERSION +1 -1
- package/docs/ANV-263-hook-logging-investigation.md +116 -0
- package/docs/command-reference.md +398 -17
- package/docs/session-workflow.md +62 -9
- package/docs/system-architecture.md +584 -0
- package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
- package/global/api/openapi.yaml +357 -0
- package/global/api/ralph_api.py +528 -0
- package/global/commands/anvil-settings.md +47 -19
- package/global/commands/audit.md +163 -0
- package/global/commands/checklist.md +180 -0
- package/global/commands/coderabbit-fix.md +282 -0
- package/global/commands/efficiency.md +356 -0
- package/global/commands/evidence.md +117 -33
- package/global/commands/hud.md +24 -0
- package/global/commands/insights.md +101 -3
- package/global/commands/orient.md +22 -21
- package/global/commands/patterns.md +115 -0
- package/global/commands/ralph.md +47 -1
- package/global/commands/token-budget.md +214 -0
- package/global/commands/weekly-review.md +21 -1
- package/global/config/notifications.yaml.template +50 -0
- package/global/hooks/ralph_stop.sh +33 -1
- package/global/hooks/statusline.sh +67 -2
- package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
- package/global/lib/coderabbit_metrics.py +647 -0
- package/global/lib/command_tracker.py +147 -0
- package/global/lib/context_optimizer.py +323 -0
- package/global/lib/linear_provider.py +210 -16
- package/global/lib/log_rotation.py +287 -0
- package/global/lib/optimization_applier.py +582 -0
- package/global/lib/ralph_events.py +398 -0
- package/global/lib/ralph_notifier.py +366 -0
- package/global/lib/ralph_state.py +264 -24
- package/global/lib/ralph_webhooks.py +470 -0
- package/global/lib/state_manager.py +121 -0
- package/global/lib/token_analyzer.py +1383 -0
- package/global/lib/token_metrics.py +919 -0
- package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/test_command_tracker.py +172 -0
- package/global/tests/test_context_optimizer.py +321 -0
- package/global/tests/test_linear_filtering.py +319 -0
- package/global/tests/test_linear_provider.py +40 -1
- package/global/tests/test_optimization_applier.py +508 -0
- package/global/tests/test_token_analyzer.py +735 -0
- package/global/tests/test_token_analyzer_phase6.py +537 -0
- package/global/tests/test_token_metrics.py +829 -0
- package/global/tools/README.md +153 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +86 -1
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
- package/global/tools/anvil-memory/src/commands/context.ts +322 -0
- package/global/tools/anvil-memory/src/db.ts +108 -0
- package/global/tools/anvil-memory/src/index.ts +2 -8
- package/global/tools/orient_linear.py +159 -0
- package/global/tools/ralph-watch +423 -0
- package/package.json +2 -1
- package/project/.anvil-project.yaml.template +93 -0
- package/project/CLAUDE.md.template +343 -0
- package/project/agents/README.md +119 -0
- package/project/agents/cross-layer-debugger.md +217 -0
- package/project/agents/security-code-reviewer.md +162 -0
- package/project/constitution.md.template +235 -0
- package/project/coordination.md +103 -0
- package/project/docs/background-tasks.md +258 -0
- package/project/docs/skills-frontmatter.md +243 -0
- package/project/examples/README.md +106 -0
- package/project/examples/api-route-template.ts +171 -0
- package/project/examples/component-template.tsx +110 -0
- package/project/examples/hook-template.ts +152 -0
- package/project/examples/service-template.ts +207 -0
- package/project/examples/test-template.test.tsx +249 -0
- package/project/hooks/README.md +491 -0
- package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/project/hooks/notification.py +183 -0
- package/project/hooks/permission_request.py +438 -0
- package/project/hooks/post_tool_use.py +397 -0
- package/project/hooks/pre_compact.py +126 -0
- package/project/hooks/pre_tool_use.py +454 -0
- package/project/hooks/session_start.py +656 -0
- package/project/hooks/stop.py +356 -0
- package/project/hooks/subagent_start.py +223 -0
- package/project/hooks/subagent_stop.py +215 -0
- package/project/hooks/user_prompt_submit.py +110 -0
- package/project/hooks/utils/llm/anth.py +114 -0
- package/project/hooks/utils/llm/oai.py +114 -0
- package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
- package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
- package/project/hooks/utils/tts/openai_tts.py +92 -0
- package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
- package/project/linear.yaml.template +23 -0
- package/project/product.md.template +238 -0
- package/project/retros/README.md +126 -0
- package/project/rules/README.md +90 -0
- package/project/rules/debugging.md +139 -0
- package/project/rules/security-review.md +115 -0
- package/project/settings.yaml.template +185 -0
- package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
- package/project/templates/api-python/CLAUDE.md +547 -0
- package/project/templates/generic/CLAUDE.md +260 -0
- package/project/templates/saas/CLAUDE.md +478 -0
- package/project/tests/README.md +140 -0
- package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
- package/project/tests/fixtures/sample-transcript.jsonl +21 -0
- package/project/tests/test-hooks.sh +259 -0
- package/project/tests/test-lib.sh +248 -0
- package/project/tests/test-statusline.sh +165 -0
- 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
|
});
|