anvil-dev-framework 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,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();