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.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook - Auto-capture observations from tool output
|
|
4
|
+
*
|
|
5
|
+
* Claude Code Hook Type: PostToolUse
|
|
6
|
+
* Purpose: Analyze tool output and create observations for significant events
|
|
7
|
+
*
|
|
8
|
+
* Input: JSON from stdin with tool_name, tool_input, tool_result
|
|
9
|
+
* Output: Empty JSON (hook doesn't modify response)
|
|
10
|
+
*
|
|
11
|
+
* Performance target: <100ms (non-blocking)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { AnvilMemoryDb, dbExists, getDefaultDbPath } from '../tools/anvil-memory/src/db.ts';
|
|
15
|
+
import type { ObservationType } from '../tools/anvil-memory/src/types.ts';
|
|
16
|
+
import { basename } from 'path';
|
|
17
|
+
|
|
18
|
+
// Tools to skip (low-value for observation capture)
|
|
19
|
+
const SKIP_TOOLS = new Set([
|
|
20
|
+
'TodoWrite',
|
|
21
|
+
'AskUserQuestion',
|
|
22
|
+
'ListMcpResourcesTool',
|
|
23
|
+
'SlashCommand',
|
|
24
|
+
'Skill',
|
|
25
|
+
'ExitPlanMode',
|
|
26
|
+
'EnterPlanMode',
|
|
27
|
+
'Glob',
|
|
28
|
+
'Grep',
|
|
29
|
+
'LS',
|
|
30
|
+
'TaskOutput',
|
|
31
|
+
'KillShell',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// Keywords for classifying observation types
|
|
35
|
+
const TYPE_KEYWORDS: Record<ObservationType, string[]> = {
|
|
36
|
+
bugfix: ['fix', 'bug', 'error', 'issue', 'broken', 'crash', 'fail', 'wrong'],
|
|
37
|
+
feature: ['add', 'implement', 'create', 'new feature', 'feat'],
|
|
38
|
+
refactor: ['refactor', 'restructure', 'reorganize', 'clean up', 'simplify'],
|
|
39
|
+
discovery: ['found', 'discover', 'notice', 'observe', 'see that', 'realize'],
|
|
40
|
+
decision: ['decide', 'choose', 'pick', 'select', 'option', 'approach'],
|
|
41
|
+
change: ['update', 'modify', 'change', 'edit', 'alter'],
|
|
42
|
+
checkpoint: ['checkpoint', 'save point', 'snapshot'],
|
|
43
|
+
ralph_iteration: ['ralph', 'iteration', 'loop'],
|
|
44
|
+
handoff: ['handoff', 'handover', 'transfer'],
|
|
45
|
+
shard: ['shard', 'split', 'break down'],
|
|
46
|
+
linear_sync: ['linear', 'issue', 'ticket'],
|
|
47
|
+
session_request: ['session', 'request', 'user asked'],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
interface PostToolUseInput {
|
|
51
|
+
tool_name: string;
|
|
52
|
+
tool_input?: Record<string, unknown>;
|
|
53
|
+
tool_result?: string;
|
|
54
|
+
session_id?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Determines if a tool output is significant enough to capture as an observation.
|
|
59
|
+
* Filters out low-value tools, empty results, and most Read operations.
|
|
60
|
+
* Write/Edit operations, modifying Bash commands, Task results, and web operations
|
|
61
|
+
* are considered significant.
|
|
62
|
+
*
|
|
63
|
+
* @param input - The PostToolUseInput containing tool_name, tool_input, and tool_result
|
|
64
|
+
* @returns true if the tool output should be captured as an observation, false otherwise
|
|
65
|
+
*/
|
|
66
|
+
function isSignificant(input: PostToolUseInput): boolean {
|
|
67
|
+
const { tool_name, tool_result } = input;
|
|
68
|
+
|
|
69
|
+
// Skip certain tools entirely
|
|
70
|
+
if (SKIP_TOOLS.has(tool_name)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Skip if no result
|
|
75
|
+
if (!tool_result || tool_result.length < 20) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Skip Read tool for most files (too noisy)
|
|
80
|
+
if (tool_name === 'Read') {
|
|
81
|
+
// Only capture if reading a significant file type
|
|
82
|
+
const filePath = input.tool_input?.file_path as string;
|
|
83
|
+
if (filePath) {
|
|
84
|
+
const significantExtensions = ['.md', '.json', '.toml', '.yaml', '.yml'];
|
|
85
|
+
const lastDot = filePath.lastIndexOf('.');
|
|
86
|
+
const ext = lastDot >= 0 ? filePath.substring(lastDot) : '';
|
|
87
|
+
if (!significantExtensions.includes(ext)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Write/Edit operations are always significant
|
|
94
|
+
if (['Write', 'Edit', 'MultiEdit', 'NotebookEdit'].includes(tool_name)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Bash commands that modify things are significant
|
|
99
|
+
if (tool_name === 'Bash') {
|
|
100
|
+
const command = input.tool_input?.command as string;
|
|
101
|
+
if (command) {
|
|
102
|
+
const modifyingCommands = ['git commit', 'git push', 'npm install', 'bun install', 'mkdir', 'rm', 'mv', 'cp'];
|
|
103
|
+
return modifyingCommands.some(cmd => command.includes(cmd));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Task tool results are significant
|
|
108
|
+
if (tool_name === 'Task') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// WebFetch/WebSearch results are significant
|
|
113
|
+
if (['WebFetch', 'WebSearch'].includes(tool_name)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Classifies the observation type based on tool name, input, and result content.
|
|
122
|
+
* Checks for type-specific keywords in the combined text and falls back to
|
|
123
|
+
* tool-based classification if no keywords match.
|
|
124
|
+
*
|
|
125
|
+
* @param input - The PostToolUseInput containing tool context
|
|
126
|
+
* @returns The classified ObservationType (e.g., 'bugfix', 'feature', 'change', 'discovery')
|
|
127
|
+
*/
|
|
128
|
+
function classifyType(input: PostToolUseInput): ObservationType {
|
|
129
|
+
const { tool_name, tool_input, tool_result } = input;
|
|
130
|
+
const combinedText = `${tool_name} ${JSON.stringify(tool_input || {})} ${tool_result || ''}`.toLowerCase();
|
|
131
|
+
|
|
132
|
+
// Check for type keywords
|
|
133
|
+
for (const [type, keywords] of Object.entries(TYPE_KEYWORDS)) {
|
|
134
|
+
for (const keyword of keywords) {
|
|
135
|
+
if (combinedText.includes(keyword)) {
|
|
136
|
+
return type as ObservationType;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Default based on tool type
|
|
142
|
+
if (['Write', 'Edit', 'MultiEdit'].includes(tool_name)) {
|
|
143
|
+
return 'change';
|
|
144
|
+
}
|
|
145
|
+
if (tool_name === 'Bash') {
|
|
146
|
+
const command = input.tool_input?.command as string;
|
|
147
|
+
if (command?.includes('git commit')) return 'change';
|
|
148
|
+
if (command?.includes('git push')) return 'change';
|
|
149
|
+
if (command?.includes('test')) return 'change';
|
|
150
|
+
}
|
|
151
|
+
if (['WebFetch', 'WebSearch'].includes(tool_name)) {
|
|
152
|
+
return 'discovery';
|
|
153
|
+
}
|
|
154
|
+
if (tool_name === 'Task') {
|
|
155
|
+
return 'discovery';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return 'change';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generates a human-readable title for an observation based on the tool operation.
|
|
163
|
+
* Extracts meaningful information from tool input to create descriptive titles
|
|
164
|
+
* like "Modified config.ts" or "Git commit operation".
|
|
165
|
+
*
|
|
166
|
+
* @param input - The PostToolUseInput containing tool_name and tool_input
|
|
167
|
+
* @returns A descriptive title string for the observation
|
|
168
|
+
*/
|
|
169
|
+
function generateTitle(input: PostToolUseInput): string {
|
|
170
|
+
const { tool_name, tool_input } = input;
|
|
171
|
+
|
|
172
|
+
switch (tool_name) {
|
|
173
|
+
case 'Write':
|
|
174
|
+
case 'Edit':
|
|
175
|
+
case 'MultiEdit': {
|
|
176
|
+
const filePath = tool_input?.file_path as string;
|
|
177
|
+
if (filePath) {
|
|
178
|
+
return `Modified ${basename(filePath)}`;
|
|
179
|
+
}
|
|
180
|
+
return `File ${tool_name.toLowerCase()} operation`;
|
|
181
|
+
}
|
|
182
|
+
case 'Bash': {
|
|
183
|
+
const command = tool_input?.command as string;
|
|
184
|
+
if (command) {
|
|
185
|
+
// Extract first meaningful part of command
|
|
186
|
+
const parts = command.split(/[;&|]/)[0].trim().split(' ');
|
|
187
|
+
const cmd = parts[0];
|
|
188
|
+
if (cmd === 'git' && parts.length > 1) {
|
|
189
|
+
return `Git ${parts[1]} operation`;
|
|
190
|
+
}
|
|
191
|
+
return `Executed ${cmd}`;
|
|
192
|
+
}
|
|
193
|
+
return 'Bash command executed';
|
|
194
|
+
}
|
|
195
|
+
case 'Task': {
|
|
196
|
+
const subagentType = tool_input?.subagent_type as string;
|
|
197
|
+
const description = tool_input?.description as string;
|
|
198
|
+
return description || `${subagentType} agent task`;
|
|
199
|
+
}
|
|
200
|
+
case 'WebFetch':
|
|
201
|
+
case 'WebSearch': {
|
|
202
|
+
const url = tool_input?.url as string;
|
|
203
|
+
const query = tool_input?.query as string;
|
|
204
|
+
if (url) {
|
|
205
|
+
try {
|
|
206
|
+
return `Fetched ${new URL(url).hostname}`;
|
|
207
|
+
} catch {
|
|
208
|
+
return `Fetched ${url.substring(0, 40)}`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (query) return `Searched: ${query.substring(0, 40)}`;
|
|
212
|
+
return `Web ${tool_name.toLowerCase()}`;
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
return `${tool_name} operation`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Extracts file paths from the tool input for associating files with the observation.
|
|
221
|
+
* Handles both single file_path and array files properties.
|
|
222
|
+
*
|
|
223
|
+
* @param input - The PostToolUseInput containing tool_input with potential file paths
|
|
224
|
+
* @returns Array of file path strings, or undefined if no files found
|
|
225
|
+
*/
|
|
226
|
+
function extractFiles(input: PostToolUseInput): string[] | undefined {
|
|
227
|
+
const { tool_input } = input;
|
|
228
|
+
const files: string[] = [];
|
|
229
|
+
|
|
230
|
+
if (tool_input?.file_path) {
|
|
231
|
+
files.push(tool_input.file_path as string);
|
|
232
|
+
}
|
|
233
|
+
if (tool_input?.files && Array.isArray(tool_input.files)) {
|
|
234
|
+
files.push(...(tool_input.files as string[]));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return files.length > 0 ? files : undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Gets the current project name from the working directory.
|
|
242
|
+
* Uses the basename of process.cwd() as the project identifier.
|
|
243
|
+
*
|
|
244
|
+
* @returns The project directory name, or undefined on error
|
|
245
|
+
*/
|
|
246
|
+
function getProject(): string | undefined {
|
|
247
|
+
try {
|
|
248
|
+
const cwd = process.cwd();
|
|
249
|
+
return basename(cwd);
|
|
250
|
+
} catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Main hook execution
|
|
257
|
+
*/
|
|
258
|
+
async function main(): Promise<void> {
|
|
259
|
+
// Read input from stdin
|
|
260
|
+
let input: PostToolUseInput | null = null;
|
|
261
|
+
try {
|
|
262
|
+
const stdin = await Bun.stdin.text();
|
|
263
|
+
if (stdin.trim()) {
|
|
264
|
+
input = JSON.parse(stdin);
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// Ignore stdin parse errors
|
|
268
|
+
console.log('{}');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!input || !input.tool_name) {
|
|
273
|
+
console.log('{}');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check if significant
|
|
278
|
+
if (!isSignificant(input)) {
|
|
279
|
+
console.log('{}');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if database exists
|
|
284
|
+
const dbPath = getDefaultDbPath();
|
|
285
|
+
if (!dbExists(dbPath)) {
|
|
286
|
+
console.log('{}');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Create observation
|
|
292
|
+
const db = new AnvilMemoryDb(dbPath);
|
|
293
|
+
|
|
294
|
+
const type = classifyType(input);
|
|
295
|
+
const title = generateTitle(input);
|
|
296
|
+
const files = extractFiles(input);
|
|
297
|
+
const project = getProject();
|
|
298
|
+
|
|
299
|
+
// Truncate tool_result for content (keep it reasonable)
|
|
300
|
+
const content = input.tool_result
|
|
301
|
+
? input.tool_result.substring(0, 1000)
|
|
302
|
+
: `${input.tool_name} operation`;
|
|
303
|
+
|
|
304
|
+
db.createObservation({
|
|
305
|
+
type,
|
|
306
|
+
title,
|
|
307
|
+
content,
|
|
308
|
+
project,
|
|
309
|
+
files,
|
|
310
|
+
timestamp: new Date().toISOString(),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
db.close();
|
|
314
|
+
} catch {
|
|
315
|
+
// Silently fail - don't disrupt Claude Code flow
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Always output empty JSON (hook doesn't modify response)
|
|
319
|
+
console.log('{}');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
main();
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart Hook - Inject recent context from Anvil Memory
|
|
4
|
+
*
|
|
5
|
+
* Claude Code Hook Type: SessionStart
|
|
6
|
+
* Purpose: Query recent observations and inject into session context
|
|
7
|
+
*
|
|
8
|
+
* Input: JSON from stdin with session info
|
|
9
|
+
* Output: JSON with { additionalContext: "markdown content" }
|
|
10
|
+
*
|
|
11
|
+
* Performance target: <100ms
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { AnvilMemoryDb, dbExists, getDefaultDbPath } from '../tools/anvil-memory/src/db.ts';
|
|
15
|
+
import type { Observation } from '../tools/anvil-memory/src/types.ts';
|
|
16
|
+
|
|
17
|
+
// Configuration
|
|
18
|
+
const MAX_OBSERVATIONS = 30;
|
|
19
|
+
// TODO: Implement token counting/truncation in generateContext to enforce this limit
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
21
|
+
const MAX_CONTEXT_TOKENS = 4000; // Approximate token limit for context injection
|
|
22
|
+
|
|
23
|
+
// Type emoji mapping
|
|
24
|
+
const TYPE_EMOJI: Record<string, string> = {
|
|
25
|
+
bugfix: '🔴',
|
|
26
|
+
feature: '🟣',
|
|
27
|
+
refactor: '🔄',
|
|
28
|
+
discovery: '🔵',
|
|
29
|
+
decision: '⚖️',
|
|
30
|
+
change: '✅',
|
|
31
|
+
checkpoint: '📍',
|
|
32
|
+
ralph_iteration: '🤖',
|
|
33
|
+
handoff: '📤',
|
|
34
|
+
shard: '🧩',
|
|
35
|
+
linear_sync: '🔗',
|
|
36
|
+
session_request: '🎯',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface HookInput {
|
|
40
|
+
cwd?: string;
|
|
41
|
+
claudeMdFiles?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface HookOutput {
|
|
45
|
+
additionalContext?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Formats a single observation as a markdown table row.
|
|
50
|
+
* Converts the timestamp to a localized time string and maps the type to an emoji.
|
|
51
|
+
*
|
|
52
|
+
* @param obs - The Observation object with id, timestamp, type, and title fields
|
|
53
|
+
* @returns A markdown table row string in format "| #id | time | emoji | title |"
|
|
54
|
+
*/
|
|
55
|
+
function formatObservation(obs: Observation): string {
|
|
56
|
+
const emoji = TYPE_EMOJI[obs.type] || '📝';
|
|
57
|
+
const time = new Date(obs.timestamp).toLocaleTimeString('en-US', {
|
|
58
|
+
hour: 'numeric',
|
|
59
|
+
minute: '2-digit',
|
|
60
|
+
});
|
|
61
|
+
return `| #${obs.id} | ${time} | ${emoji} | ${obs.title} |`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Groups observations by their project field.
|
|
66
|
+
* Observations without a project are grouped under 'General'.
|
|
67
|
+
*
|
|
68
|
+
* @param observations - Array of Observation objects to group
|
|
69
|
+
* @returns A Map where keys are project names (or 'General') and values are arrays of observations for that project
|
|
70
|
+
*/
|
|
71
|
+
function groupByProject(observations: Observation[]): Map<string, Observation[]> {
|
|
72
|
+
const groups = new Map<string, Observation[]>();
|
|
73
|
+
for (const obs of observations) {
|
|
74
|
+
const project = obs.project || 'General';
|
|
75
|
+
const existing = groups.get(project) || [];
|
|
76
|
+
existing.push(obs);
|
|
77
|
+
groups.set(project, existing);
|
|
78
|
+
}
|
|
79
|
+
return groups;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generates a markdown summary of recent observations for context injection.
|
|
84
|
+
* Groups observations by project and formats them as tables with headers.
|
|
85
|
+
* Includes a legend explaining the type emojis.
|
|
86
|
+
*
|
|
87
|
+
* @param observations - Array of Observation objects to summarize
|
|
88
|
+
* @returns Markdown string suitable for context injection, or empty string if no observations
|
|
89
|
+
*/
|
|
90
|
+
function generateContext(observations: Observation[]): string {
|
|
91
|
+
if (observations.length === 0) {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lines: string[] = [];
|
|
96
|
+
lines.push('# Recent Context from Anvil Memory');
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push(`> ${observations.length} recent observations loaded`);
|
|
99
|
+
lines.push('');
|
|
100
|
+
|
|
101
|
+
// Group by project
|
|
102
|
+
const grouped = groupByProject(observations);
|
|
103
|
+
|
|
104
|
+
for (const [project, obs] of grouped) {
|
|
105
|
+
lines.push(`## ${project}`);
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('| ID | Time | Type | Title |');
|
|
108
|
+
lines.push('|----|------|------|-------|');
|
|
109
|
+
for (const o of obs) {
|
|
110
|
+
lines.push(formatObservation(o));
|
|
111
|
+
}
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Add legend
|
|
116
|
+
lines.push('**Legend:** 🔴 bugfix | 🟣 feature | 🔄 refactor | 🔵 discovery | ⚖️ decision | ✅ change');
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push('*Use `anvil-memory get <id>` to fetch full observation details.*');
|
|
119
|
+
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Main hook execution
|
|
125
|
+
*/
|
|
126
|
+
async function main(): Promise<void> {
|
|
127
|
+
// Read input from stdin
|
|
128
|
+
let input: HookInput = {};
|
|
129
|
+
try {
|
|
130
|
+
const stdin = await Bun.stdin.text();
|
|
131
|
+
if (stdin.trim()) {
|
|
132
|
+
input = JSON.parse(stdin);
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Ignore stdin parse errors
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if database exists
|
|
139
|
+
const dbPath = getDefaultDbPath();
|
|
140
|
+
if (!dbExists(dbPath)) {
|
|
141
|
+
// No database - output empty response
|
|
142
|
+
const output: HookOutput = {};
|
|
143
|
+
console.log(JSON.stringify(output));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Query recent observations
|
|
149
|
+
const db = new AnvilMemoryDb(dbPath);
|
|
150
|
+
const observations = db.getRecentObservations(MAX_OBSERVATIONS);
|
|
151
|
+
db.close();
|
|
152
|
+
|
|
153
|
+
// Generate context
|
|
154
|
+
const context = generateContext(observations);
|
|
155
|
+
|
|
156
|
+
// Output result
|
|
157
|
+
const output: HookOutput = context ? { additionalContext: context } : {};
|
|
158
|
+
console.log(JSON.stringify(output));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// On error, output empty response (graceful degradation)
|
|
161
|
+
const output: HookOutput = {};
|
|
162
|
+
console.log(JSON.stringify(output));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
main();
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Stop Hook - Save session summary to Anvil Memory
|
|
4
|
+
*
|
|
5
|
+
* Claude Code Hook Type: Stop
|
|
6
|
+
* Purpose: Generate session summary and save to database
|
|
7
|
+
*
|
|
8
|
+
* Input: JSON from stdin with session stats
|
|
9
|
+
* Output: Empty JSON (hook doesn't block)
|
|
10
|
+
*
|
|
11
|
+
* Performance target: <500ms
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { AnvilMemoryDb, dbExists, getDefaultDbPath } from '../tools/anvil-memory/src/db.ts';
|
|
15
|
+
import { basename } from 'path';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
|
|
18
|
+
interface StopInput {
|
|
19
|
+
session_id?: string;
|
|
20
|
+
transcript?: string;
|
|
21
|
+
context_used_percent?: number;
|
|
22
|
+
tool_calls?: number;
|
|
23
|
+
duration_seconds?: number;
|
|
24
|
+
started_at?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets the current git branch name.
|
|
29
|
+
* Uses a 100ms timeout to prevent hangs when git is unresponsive.
|
|
30
|
+
*
|
|
31
|
+
* @returns The current branch name, or undefined if not in a git repo or on error/timeout
|
|
32
|
+
*/
|
|
33
|
+
function getGitBranch(): string | undefined {
|
|
34
|
+
try {
|
|
35
|
+
return execSync('git branch --show-current', { encoding: 'utf-8', timeout: 100 }).trim();
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets the current git commit hash (short form).
|
|
43
|
+
* Uses a 100ms timeout to prevent hangs when git is unresponsive.
|
|
44
|
+
*
|
|
45
|
+
* @returns The short commit hash, or undefined if not in a git repo or on error/timeout
|
|
46
|
+
*/
|
|
47
|
+
function getGitHash(): string | undefined {
|
|
48
|
+
try {
|
|
49
|
+
return execSync('git rev-parse --short HEAD', { encoding: 'utf-8', timeout: 100 }).trim();
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gets the current project name from the working directory.
|
|
57
|
+
* Uses the basename of process.cwd() as the project identifier.
|
|
58
|
+
*
|
|
59
|
+
* @returns The project directory name, or 'unknown' if unable to determine
|
|
60
|
+
*/
|
|
61
|
+
function getProject(): string {
|
|
62
|
+
try {
|
|
63
|
+
return basename(process.cwd());
|
|
64
|
+
} catch {
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generates a session summary from recent observations in the database.
|
|
71
|
+
* Queries the 20 most recent observations and creates a summary including:
|
|
72
|
+
* - Total observation count
|
|
73
|
+
* - Breakdown by observation type
|
|
74
|
+
* - Key work titles (first 3 observations)
|
|
75
|
+
*
|
|
76
|
+
* @param db - The AnvilMemoryDb instance to query
|
|
77
|
+
* @returns A human-readable summary string describing the session's observations
|
|
78
|
+
*/
|
|
79
|
+
function generateSummary(db: AnvilMemoryDb): string {
|
|
80
|
+
// Get recent observations (from last hour as proxy for "this session")
|
|
81
|
+
const observations = db.getRecentObservations(20);
|
|
82
|
+
|
|
83
|
+
if (observations.length === 0) {
|
|
84
|
+
return 'No observations recorded during this session.';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Count by type
|
|
88
|
+
const typeCounts: Record<string, number> = {};
|
|
89
|
+
for (const obs of observations) {
|
|
90
|
+
typeCounts[obs.type] = (typeCounts[obs.type] || 0) + 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build summary
|
|
94
|
+
const parts: string[] = [];
|
|
95
|
+
parts.push(`Session recorded ${observations.length} observation(s).`);
|
|
96
|
+
|
|
97
|
+
// Add type breakdown
|
|
98
|
+
const typeBreakdown = Object.entries(typeCounts)
|
|
99
|
+
.map(([type, count]) => `${count} ${type}`)
|
|
100
|
+
.join(', ');
|
|
101
|
+
if (typeBreakdown) {
|
|
102
|
+
parts.push(`Types: ${typeBreakdown}.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add key observations (first 3 titles)
|
|
106
|
+
const keyTitles = observations.slice(0, 3).map(o => o.title);
|
|
107
|
+
if (keyTitles.length > 0) {
|
|
108
|
+
parts.push(`Key work: ${keyTitles.join('; ')}.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return parts.join(' ');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Calculates the session start time from available input data.
|
|
116
|
+
* Uses a three-tier precedence:
|
|
117
|
+
* 1. Explicit started_at from input (if provided)
|
|
118
|
+
* 2. Calculated from duration_seconds (current time - duration)
|
|
119
|
+
* 3. Current time as fallback
|
|
120
|
+
*
|
|
121
|
+
* @param input - The StopInput containing optional started_at or duration_seconds
|
|
122
|
+
* @returns ISO 8601 formatted timestamp string
|
|
123
|
+
*/
|
|
124
|
+
function calculateStartedAt(input: StopInput): string {
|
|
125
|
+
// Use explicit started_at if provided
|
|
126
|
+
if (input.started_at) {
|
|
127
|
+
return input.started_at;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Calculate from duration if available
|
|
131
|
+
if (input.duration_seconds && input.duration_seconds > 0) {
|
|
132
|
+
const startTime = Date.now() - input.duration_seconds * 1000;
|
|
133
|
+
return new Date(startTime).toISOString();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Fallback to current time
|
|
137
|
+
return new Date().toISOString();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Main hook execution
|
|
142
|
+
*/
|
|
143
|
+
async function main(): Promise<void> {
|
|
144
|
+
// Read input from stdin
|
|
145
|
+
let input: StopInput = {};
|
|
146
|
+
try {
|
|
147
|
+
const stdin = await Bun.stdin.text();
|
|
148
|
+
if (stdin.trim()) {
|
|
149
|
+
input = JSON.parse(stdin);
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// Ignore stdin parse errors
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if database exists
|
|
156
|
+
const dbPath = getDefaultDbPath();
|
|
157
|
+
if (!dbExists(dbPath)) {
|
|
158
|
+
console.log('{}');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const db = new AnvilMemoryDb(dbPath);
|
|
164
|
+
|
|
165
|
+
// Generate summary
|
|
166
|
+
const summary = generateSummary(db);
|
|
167
|
+
|
|
168
|
+
// Create session record
|
|
169
|
+
db.createSession({
|
|
170
|
+
project: getProject(),
|
|
171
|
+
branch: getGitBranch(),
|
|
172
|
+
git_hash: getGitHash(),
|
|
173
|
+
context_peak_percent: input.context_used_percent,
|
|
174
|
+
summary,
|
|
175
|
+
started_at: calculateStartedAt(input),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
db.close();
|
|
179
|
+
} catch {
|
|
180
|
+
// Silently fail - don't disrupt Claude Code flow
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Always output empty JSON
|
|
184
|
+
console.log('{}');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main();
|