feed-the-machine 1.6.1 → 1.7.1

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 (272) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +262 -170
  3. package/bin/__pycache__/tasks_db.cpython-314.pyc +0 -0
  4. package/bin/brain.py +1340 -0
  5. package/bin/convert_claude_skills_to_codex.py +490 -0
  6. package/bin/generate-manifest.mjs +463 -463
  7. package/bin/harden_codex_skills.py +141 -0
  8. package/bin/install.mjs +491 -491
  9. package/bin/migrate-eng-buddy-data.py +875 -0
  10. package/bin/playbook_engine/__init__.py +1 -0
  11. package/bin/playbook_engine/conftest.py +8 -0
  12. package/bin/playbook_engine/extractor.py +33 -0
  13. package/bin/playbook_engine/manager.py +102 -0
  14. package/bin/playbook_engine/models.py +84 -0
  15. package/bin/playbook_engine/registry.py +35 -0
  16. package/bin/playbook_engine/test_extractor.py +72 -0
  17. package/bin/playbook_engine/test_integration.py +129 -0
  18. package/bin/playbook_engine/test_manager.py +85 -0
  19. package/bin/playbook_engine/test_models.py +166 -0
  20. package/bin/playbook_engine/test_registry.py +67 -0
  21. package/bin/playbook_engine/test_tracer.py +86 -0
  22. package/bin/playbook_engine/tracer.py +93 -0
  23. package/bin/tasks_db.py +456 -0
  24. package/docs/HOOKS.md +243 -243
  25. package/docs/INBOX.md +233 -233
  26. package/ftm/SKILL.md +125 -122
  27. package/ftm-audit/SKILL.md +673 -623
  28. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
  29. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
  30. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
  31. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
  32. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
  33. package/ftm-audit/scripts/run-knip.sh +23 -23
  34. package/ftm-audit.yml +2 -2
  35. package/ftm-brainstorm/SKILL.md +1003 -498
  36. package/ftm-brainstorm/evals/evals.json +180 -100
  37. package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
  38. package/ftm-brainstorm/references/agent-prompts.md +552 -224
  39. package/ftm-brainstorm/references/plan-template.md +209 -121
  40. package/ftm-brainstorm.yml +2 -2
  41. package/ftm-browse/SKILL.md +454 -454
  42. package/ftm-browse/daemon/browser-manager.ts +206 -206
  43. package/ftm-browse/daemon/bun.lock +30 -30
  44. package/ftm-browse/daemon/cli.ts +347 -347
  45. package/ftm-browse/daemon/commands.ts +410 -410
  46. package/ftm-browse/daemon/main.ts +357 -357
  47. package/ftm-browse/daemon/package.json +17 -17
  48. package/ftm-browse/daemon/server.ts +189 -189
  49. package/ftm-browse/daemon/snapshot.ts +519 -519
  50. package/ftm-browse/daemon/tsconfig.json +22 -22
  51. package/ftm-browse.yml +4 -4
  52. package/ftm-capture/SKILL.md +370 -370
  53. package/ftm-capture.yml +4 -4
  54. package/ftm-codex-gate/SKILL.md +361 -361
  55. package/ftm-codex-gate.yml +2 -2
  56. package/ftm-config/SKILL.md +422 -345
  57. package/ftm-config.default.yml +125 -82
  58. package/ftm-config.yml +44 -2
  59. package/ftm-council/SKILL.md +416 -416
  60. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
  61. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
  62. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
  63. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
  64. package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
  65. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
  66. package/ftm-council-chat.yml +2 -0
  67. package/ftm-council.yml +2 -2
  68. package/ftm-dashboard/SKILL.md +163 -163
  69. package/ftm-dashboard.yml +4 -4
  70. package/ftm-debug/SKILL.md +1037 -1037
  71. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
  72. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
  73. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
  74. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
  75. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
  76. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
  77. package/ftm-debug.yml +2 -2
  78. package/ftm-diagram/SKILL.md +277 -277
  79. package/ftm-diagram.yml +2 -2
  80. package/ftm-executor/SKILL.md +777 -777
  81. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
  82. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
  83. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
  84. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
  85. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +81 -72
  86. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
  87. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
  88. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
  89. package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
  90. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
  91. package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
  92. package/ftm-executor/runtime/package.json +8 -8
  93. package/ftm-executor.yml +2 -2
  94. package/ftm-git/SKILL.md +441 -441
  95. package/ftm-git/evals/evals.json +26 -26
  96. package/ftm-git/evals/promptfoo.yaml +75 -75
  97. package/ftm-git/hooks/post-commit-experience.sh +92 -92
  98. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
  99. package/ftm-git/references/protocols/REMEDIATION.md +139 -139
  100. package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
  101. package/ftm-git.yml +2 -2
  102. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  103. package/ftm-inbox/backend/adapters/_retry.py +64 -64
  104. package/ftm-inbox/backend/adapters/base.py +230 -230
  105. package/ftm-inbox/backend/adapters/freshservice.py +104 -104
  106. package/ftm-inbox/backend/adapters/gmail.py +125 -125
  107. package/ftm-inbox/backend/adapters/jira.py +136 -136
  108. package/ftm-inbox/backend/adapters/registry.py +192 -192
  109. package/ftm-inbox/backend/adapters/slack.py +110 -110
  110. package/ftm-inbox/backend/db/connection.py +54 -54
  111. package/ftm-inbox/backend/db/schema.py +78 -78
  112. package/ftm-inbox/backend/executor/__init__.py +7 -7
  113. package/ftm-inbox/backend/executor/engine.py +149 -149
  114. package/ftm-inbox/backend/executor/step_runner.py +98 -98
  115. package/ftm-inbox/backend/main.py +103 -103
  116. package/ftm-inbox/backend/models/__init__.py +1 -1
  117. package/ftm-inbox/backend/models/unified_task.py +36 -36
  118. package/ftm-inbox/backend/planner/__init__.py +6 -6
  119. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  120. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  121. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  122. package/ftm-inbox/backend/planner/generator.py +127 -127
  123. package/ftm-inbox/backend/planner/schema.py +34 -34
  124. package/ftm-inbox/backend/requirements.txt +5 -5
  125. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  126. package/ftm-inbox/backend/routes/execute.py +186 -186
  127. package/ftm-inbox/backend/routes/health.py +52 -52
  128. package/ftm-inbox/backend/routes/inbox.py +68 -68
  129. package/ftm-inbox/backend/routes/plan.py +271 -271
  130. package/ftm-inbox/bin/launchagent.mjs +91 -91
  131. package/ftm-inbox/bin/setup.mjs +188 -188
  132. package/ftm-inbox/bin/start.sh +10 -10
  133. package/ftm-inbox/bin/status.sh +17 -17
  134. package/ftm-inbox/bin/stop.sh +8 -8
  135. package/ftm-inbox/config.example.yml +55 -55
  136. package/ftm-inbox/package-lock.json +2898 -2898
  137. package/ftm-inbox/package.json +26 -26
  138. package/ftm-inbox/postcss.config.js +6 -6
  139. package/ftm-inbox/src/app.css +199 -199
  140. package/ftm-inbox/src/app.html +18 -18
  141. package/ftm-inbox/src/lib/api.ts +166 -166
  142. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
  143. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
  144. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
  145. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
  146. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
  147. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
  148. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
  149. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
  150. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
  151. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
  152. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
  153. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
  154. package/ftm-inbox/src/lib/theme.ts +47 -47
  155. package/ftm-inbox/src/routes/+layout.svelte +76 -76
  156. package/ftm-inbox/src/routes/+page.svelte +401 -401
  157. package/ftm-inbox/svelte.config.js +12 -12
  158. package/ftm-inbox/tailwind.config.ts +63 -63
  159. package/ftm-inbox/tsconfig.json +13 -13
  160. package/ftm-inbox/vite.config.ts +6 -6
  161. package/ftm-intent/SKILL.md +241 -241
  162. package/ftm-intent.yml +2 -2
  163. package/ftm-manifest.json +3794 -3794
  164. package/ftm-map/SKILL.md +291 -291
  165. package/ftm-map/scripts/db.py +712 -712
  166. package/ftm-map/scripts/index.py +415 -415
  167. package/ftm-map/scripts/parser.py +224 -224
  168. package/ftm-map/scripts/queries/go-tags.scm +20 -20
  169. package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
  170. package/ftm-map/scripts/queries/python-tags.scm +31 -31
  171. package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
  172. package/ftm-map/scripts/queries/rust-tags.scm +37 -37
  173. package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
  174. package/ftm-map/scripts/query.py +301 -301
  175. package/ftm-map/scripts/ranker.py +377 -377
  176. package/ftm-map/scripts/requirements.txt +5 -5
  177. package/ftm-map/scripts/setup-hooks.sh +27 -27
  178. package/ftm-map/scripts/setup.sh +56 -56
  179. package/ftm-map/scripts/test_db.py +364 -364
  180. package/ftm-map/scripts/test_parser.py +174 -174
  181. package/ftm-map/scripts/test_query.py +183 -183
  182. package/ftm-map/scripts/test_ranker.py +199 -199
  183. package/ftm-map/scripts/views.py +591 -591
  184. package/ftm-map.yml +2 -2
  185. package/ftm-mind/SKILL.md +201 -1943
  186. package/ftm-mind/evals/promptfoo.yaml +142 -142
  187. package/ftm-mind/references/blackboard-protocol.md +110 -0
  188. package/ftm-mind/references/blackboard-schema.md +328 -328
  189. package/ftm-mind/references/complexity-guide.md +110 -110
  190. package/ftm-mind/references/complexity-sizing.md +138 -0
  191. package/ftm-mind/references/decide-act-protocol.md +172 -0
  192. package/ftm-mind/references/direct-execution.md +51 -0
  193. package/ftm-mind/references/environment-discovery.md +77 -0
  194. package/ftm-mind/references/event-registry.md +319 -319
  195. package/ftm-mind/references/mcp-inventory.md +300 -296
  196. package/ftm-mind/references/ops-routing.md +47 -0
  197. package/ftm-mind/references/orient-protocol.md +234 -0
  198. package/ftm-mind/references/personality.md +40 -0
  199. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
  200. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
  201. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
  202. package/ftm-mind/references/reflexion-protocol.md +249 -249
  203. package/ftm-mind/references/routing/SCENARIOS.md +22 -22
  204. package/ftm-mind/references/routing-scenarios.md +35 -35
  205. package/ftm-mind.yml +2 -2
  206. package/ftm-ops.yml +4 -0
  207. package/ftm-pause/SKILL.md +395 -395
  208. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
  209. package/ftm-pause/references/protocols/VALIDATION.md +80 -80
  210. package/ftm-pause.yml +2 -2
  211. package/ftm-researcher/SKILL.md +275 -275
  212. package/ftm-researcher/evals/agent-diversity.yaml +17 -17
  213. package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
  214. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
  215. package/ftm-researcher/references/adaptive-search.md +116 -116
  216. package/ftm-researcher/references/agent-prompts.md +193 -193
  217. package/ftm-researcher/references/council-integration.md +193 -193
  218. package/ftm-researcher/references/output-format.md +203 -203
  219. package/ftm-researcher/references/synthesis-pipeline.md +165 -165
  220. package/ftm-researcher/scripts/score_credibility.py +234 -234
  221. package/ftm-researcher/scripts/validate_research.py +92 -92
  222. package/ftm-researcher.yml +2 -2
  223. package/ftm-resume/SKILL.md +518 -518
  224. package/ftm-resume/references/protocols/VALIDATION.md +172 -172
  225. package/ftm-resume.yml +2 -2
  226. package/ftm-retro/SKILL.md +380 -380
  227. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
  228. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
  229. package/ftm-retro.yml +2 -2
  230. package/ftm-routine/SKILL.md +170 -170
  231. package/ftm-routine.yml +4 -4
  232. package/ftm-state/blackboard/capabilities.json +5 -5
  233. package/ftm-state/blackboard/capabilities.schema.json +27 -27
  234. package/ftm-state/blackboard/context.json +37 -23
  235. package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
  236. package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
  237. package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
  238. package/ftm-state/blackboard/experiences/index.json +58 -9
  239. package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
  240. package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
  241. package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
  242. package/ftm-state/blackboard/patterns.json +6 -6
  243. package/ftm-state/schemas/context.schema.json +130 -130
  244. package/ftm-state/schemas/experience-index.schema.json +77 -77
  245. package/ftm-state/schemas/experience.schema.json +78 -78
  246. package/ftm-state/schemas/patterns.schema.json +44 -44
  247. package/ftm-upgrade/SKILL.md +194 -194
  248. package/ftm-upgrade/scripts/check-version.sh +76 -76
  249. package/ftm-upgrade/scripts/upgrade.sh +143 -143
  250. package/ftm-upgrade.yml +2 -2
  251. package/ftm-verify.yml +2 -2
  252. package/ftm.yml +2 -2
  253. package/hooks/ftm-auto-log.sh +137 -0
  254. package/hooks/ftm-blackboard-enforcer.sh +93 -93
  255. package/hooks/ftm-discovery-reminder.sh +90 -90
  256. package/hooks/ftm-drafts-gate.sh +61 -61
  257. package/hooks/ftm-event-logger.mjs +107 -107
  258. package/hooks/ftm-install-hooks.sh +240 -0
  259. package/hooks/ftm-learning-capture.sh +117 -0
  260. package/hooks/ftm-map-autodetect.sh +79 -79
  261. package/hooks/ftm-pending-sync-check.sh +22 -22
  262. package/hooks/ftm-plan-gate.sh +92 -92
  263. package/hooks/ftm-post-commit-trigger.sh +57 -57
  264. package/hooks/ftm-post-compaction.sh +138 -0
  265. package/hooks/ftm-pre-compaction.sh +147 -0
  266. package/hooks/ftm-session-end.sh +52 -0
  267. package/hooks/ftm-session-snapshot.sh +213 -0
  268. package/hooks/ftm-task-loader.sh +100 -0
  269. package/hooks/settings-template.json +91 -81
  270. package/install.sh +363 -363
  271. package/package.json +84 -84
  272. package/uninstall.sh +25 -25
@@ -1,107 +1,107 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * FTM Event Logger — PostToolUse hook
5
- * Appends structured JSONL entries to ~/.claude/ftm-state/events.log
6
- * Debounced: fires every 3rd tool use to avoid overhead
7
- */
8
-
9
- import { readFileSync, appendFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
- import { join } from 'path';
11
- import { homedir } from 'os';
12
-
13
- const HOME = homedir();
14
- const STATE_DIR = join(HOME, '.claude', 'ftm-state');
15
- const LOG_PATH = join(STATE_DIR, 'events.log');
16
- const COUNTER_PATH = join(STATE_DIR, '.event-counter');
17
- const ARCHIVE_DIR = join(STATE_DIR, 'event-archives');
18
- const MAX_AGE_DAYS = 30;
19
-
20
- // Ensure directories exist
21
- if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
22
-
23
- // Read stdin for hook input
24
- let input = '';
25
- process.stdin.setEncoding('utf-8');
26
- process.stdin.on('data', (chunk) => { input += chunk; });
27
- process.stdin.on('end', () => {
28
- try {
29
- const hookData = JSON.parse(input);
30
-
31
- // Debounce: only fire every 3rd tool use
32
- let counter = 0;
33
- try {
34
- counter = parseInt(readFileSync(COUNTER_PATH, 'utf-8').trim(), 10) || 0;
35
- } catch { /* first run */ }
36
-
37
- counter++;
38
- writeFileSync(COUNTER_PATH, String(counter));
39
-
40
- if (counter % 3 !== 0) {
41
- process.exit(0); // Skip this invocation
42
- }
43
-
44
- // Build log entry
45
- const entry = {
46
- timestamp: new Date().toISOString(),
47
- event_type: 'tool_use',
48
- tool_name: hookData.tool_name || 'unknown',
49
- tool_input_keys: hookData.tool_input ? Object.keys(hookData.tool_input) : [],
50
- session_id: process.env.CLAUDE_SESSION_ID || 'unknown',
51
- skill_context: detectSkillContext(hookData),
52
- };
53
-
54
- // Append JSONL
55
- appendFileSync(LOG_PATH, JSON.stringify(entry) + '\n');
56
-
57
- // Log rotation: check once per 100 writes
58
- if (counter % 100 === 0) {
59
- rotateOldEntries();
60
- }
61
-
62
- } catch (e) {
63
- // Never crash — logging failure should not block execution
64
- process.exit(0);
65
- }
66
- });
67
-
68
- function detectSkillContext(hookData) {
69
- const toolName = hookData.tool_name || '';
70
- if (toolName === 'Skill') return hookData.tool_input?.skill || 'unknown-skill';
71
- if (toolName === 'Agent') return 'agent-dispatch';
72
- return null;
73
- }
74
-
75
- function rotateOldEntries() {
76
- try {
77
- if (!existsSync(LOG_PATH)) return;
78
-
79
- const lines = readFileSync(LOG_PATH, 'utf-8').split('\n').filter(Boolean);
80
- const cutoff = Date.now() - (MAX_AGE_DAYS * 24 * 60 * 60 * 1000);
81
-
82
- const recent = [];
83
- const archived = [];
84
-
85
- for (const line of lines) {
86
- try {
87
- const entry = JSON.parse(line);
88
- if (new Date(entry.timestamp).getTime() > cutoff) {
89
- recent.push(line);
90
- } else {
91
- archived.push(line);
92
- }
93
- } catch {
94
- recent.push(line); // Keep unparseable lines
95
- }
96
- }
97
-
98
- if (archived.length > 0) {
99
- if (!existsSync(ARCHIVE_DIR)) mkdirSync(ARCHIVE_DIR, { recursive: true });
100
- const archivePath = join(ARCHIVE_DIR, `events-${new Date().toISOString().slice(0, 10)}.log`);
101
- appendFileSync(archivePath, archived.join('\n') + '\n');
102
- writeFileSync(LOG_PATH, recent.join('\n') + (recent.length > 0 ? '\n' : ''));
103
- }
104
- } catch {
105
- // Rotation failure is non-critical
106
- }
107
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * FTM Event Logger — PostToolUse hook
5
+ * Appends structured JSONL entries to ~/.claude/ftm-state/events.log
6
+ * Debounced: fires every 3rd tool use to avoid overhead
7
+ */
8
+
9
+ import { readFileSync, appendFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+
13
+ const HOME = homedir();
14
+ const STATE_DIR = join(HOME, '.claude', 'ftm-state');
15
+ const LOG_PATH = join(STATE_DIR, 'events.log');
16
+ const COUNTER_PATH = join(STATE_DIR, '.event-counter');
17
+ const ARCHIVE_DIR = join(STATE_DIR, 'event-archives');
18
+ const MAX_AGE_DAYS = 30;
19
+
20
+ // Ensure directories exist
21
+ if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
22
+
23
+ // Read stdin for hook input
24
+ let input = '';
25
+ process.stdin.setEncoding('utf-8');
26
+ process.stdin.on('data', (chunk) => { input += chunk; });
27
+ process.stdin.on('end', () => {
28
+ try {
29
+ const hookData = JSON.parse(input);
30
+
31
+ // Debounce: only fire every 3rd tool use
32
+ let counter = 0;
33
+ try {
34
+ counter = parseInt(readFileSync(COUNTER_PATH, 'utf-8').trim(), 10) || 0;
35
+ } catch { /* first run */ }
36
+
37
+ counter++;
38
+ writeFileSync(COUNTER_PATH, String(counter));
39
+
40
+ if (counter % 3 !== 0) {
41
+ process.exit(0); // Skip this invocation
42
+ }
43
+
44
+ // Build log entry
45
+ const entry = {
46
+ timestamp: new Date().toISOString(),
47
+ event_type: 'tool_use',
48
+ tool_name: hookData.tool_name || 'unknown',
49
+ tool_input_keys: hookData.tool_input ? Object.keys(hookData.tool_input) : [],
50
+ session_id: process.env.CLAUDE_SESSION_ID || 'unknown',
51
+ skill_context: detectSkillContext(hookData),
52
+ };
53
+
54
+ // Append JSONL
55
+ appendFileSync(LOG_PATH, JSON.stringify(entry) + '\n');
56
+
57
+ // Log rotation: check once per 100 writes
58
+ if (counter % 100 === 0) {
59
+ rotateOldEntries();
60
+ }
61
+
62
+ } catch (e) {
63
+ // Never crash — logging failure should not block execution
64
+ process.exit(0);
65
+ }
66
+ });
67
+
68
+ function detectSkillContext(hookData) {
69
+ const toolName = hookData.tool_name || '';
70
+ if (toolName === 'Skill') return hookData.tool_input?.skill || 'unknown-skill';
71
+ if (toolName === 'Agent') return 'agent-dispatch';
72
+ return null;
73
+ }
74
+
75
+ function rotateOldEntries() {
76
+ try {
77
+ if (!existsSync(LOG_PATH)) return;
78
+
79
+ const lines = readFileSync(LOG_PATH, 'utf-8').split('\n').filter(Boolean);
80
+ const cutoff = Date.now() - (MAX_AGE_DAYS * 24 * 60 * 60 * 1000);
81
+
82
+ const recent = [];
83
+ const archived = [];
84
+
85
+ for (const line of lines) {
86
+ try {
87
+ const entry = JSON.parse(line);
88
+ if (new Date(entry.timestamp).getTime() > cutoff) {
89
+ recent.push(line);
90
+ } else {
91
+ archived.push(line);
92
+ }
93
+ } catch {
94
+ recent.push(line); // Keep unparseable lines
95
+ }
96
+ }
97
+
98
+ if (archived.length > 0) {
99
+ if (!existsSync(ARCHIVE_DIR)) mkdirSync(ARCHIVE_DIR, { recursive: true });
100
+ const archivePath = join(ARCHIVE_DIR, `events-${new Date().toISOString().slice(0, 10)}.log`);
101
+ appendFileSync(archivePath, archived.join('\n') + '\n');
102
+ writeFileSync(LOG_PATH, recent.join('\n') + (recent.length > 0 ? '\n' : ''));
103
+ }
104
+ } catch {
105
+ // Rotation failure is non-critical
106
+ }
107
+ }
@@ -0,0 +1,240 @@
1
+ #!/bin/bash
2
+ # ftm-install-hooks.sh
3
+ # Installs all ftm-*.sh hooks into ~/.claude/hooks/ and updates settings.json
4
+ #
5
+ # Usage: bash hooks/ftm-install-hooks.sh [--dry-run]
6
+ #
7
+ # What it does:
8
+ # 1. Copies all ftm-*.sh hooks to ~/.claude/hooks/
9
+ # 2. Makes all hooks executable
10
+ # 3. Updates ~/.claude/settings.json with correct hook entries
11
+ # - Removes old eng-buddy hook entries
12
+ # - Adds ftm- prefixed hook entries
13
+ # - Ensures SessionEnd hooks are in SEPARATE matcher entries
14
+ # (ftm-session-snapshot.sh first, ftm-session-end.sh second)
15
+
16
+ set -euo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ HOOKS_DEST="$HOME/.claude/hooks"
20
+ SETTINGS_FILE="$HOME/.claude/settings.json"
21
+ DRY_RUN=false
22
+
23
+ for arg in "$@"; do
24
+ case "$arg" in
25
+ --dry-run) DRY_RUN=true ;;
26
+ esac
27
+ done
28
+
29
+ log() { echo "[ftm-install-hooks] $*"; }
30
+ dry() { echo "[DRY RUN] $*"; }
31
+
32
+ # --- Step 1: Copy hooks ---
33
+ log "Copying ftm-*.sh hooks to $HOOKS_DEST"
34
+ mkdir -p "$HOOKS_DEST"
35
+
36
+ for hook in "$SCRIPT_DIR"/ftm-*.sh; do
37
+ name="$(basename "$hook")"
38
+ dest="$HOOKS_DEST/$name"
39
+ if [ "$DRY_RUN" = true ]; then
40
+ dry "cp $hook -> $dest"
41
+ else
42
+ cp "$hook" "$dest"
43
+ chmod +x "$dest"
44
+ log " installed $name"
45
+ fi
46
+ done
47
+
48
+ # Also install non-sh ftm hooks if present (e.g., .mjs)
49
+ for hook in "$SCRIPT_DIR"/ftm-*.mjs; do
50
+ [ -f "$hook" ] || continue
51
+ name="$(basename "$hook")"
52
+ dest="$HOOKS_DEST/$name"
53
+ if [ "$DRY_RUN" = true ]; then
54
+ dry "cp $hook -> $dest"
55
+ else
56
+ cp "$hook" "$dest"
57
+ log " installed $name"
58
+ fi
59
+ done
60
+
61
+ # --- Step 2: Update settings.json ---
62
+ if [ ! -f "$SETTINGS_FILE" ]; then
63
+ log "ERROR: $SETTINGS_FILE not found — cannot update hook entries"
64
+ exit 1
65
+ fi
66
+
67
+ log "Updating $SETTINGS_FILE hook entries"
68
+
69
+ if [ "$DRY_RUN" = true ]; then
70
+ dry "Would update settings.json:"
71
+ dry " - Remove eng-buddy hook entries"
72
+ dry " - Add ftm-auto-log.sh to UserPromptSubmit"
73
+ dry " - Add ftm-pre-compaction.sh to UserPromptSubmit"
74
+ dry " - Add ftm-post-compaction.sh to UserPromptSubmit"
75
+ dry " - Add ftm-learning-capture.sh to PostToolUse"
76
+ dry " - Add ftm-session-snapshot.sh to SessionEnd (matcher entry 1)"
77
+ dry " - Add ftm-session-end.sh to SessionEnd (matcher entry 2, separate)"
78
+ exit 0
79
+ fi
80
+
81
+ python3 - "$SETTINGS_FILE" << 'PYEOF'
82
+ import json, sys, copy
83
+ from pathlib import Path
84
+
85
+ settings_path = sys.argv[1]
86
+ hooks_dir = str(Path.home() / ".claude/hooks")
87
+
88
+ with open(settings_path) as f:
89
+ settings = json.load(f)
90
+
91
+ hooks = settings.setdefault("hooks", {})
92
+
93
+ # Helper: remove hooks by command substring
94
+ def remove_hook_commands(hook_list, substrings):
95
+ """Remove hook entries whose command contains any of the given substrings."""
96
+ result = []
97
+ for entry in hook_list:
98
+ hooks_in_entry = entry.get("hooks", [])
99
+ filtered = [
100
+ h for h in hooks_in_entry
101
+ if not any(s in h.get("command", "") for s in substrings)
102
+ ]
103
+ if filtered:
104
+ entry = dict(entry)
105
+ entry["hooks"] = filtered
106
+ result.append(entry)
107
+ return result
108
+
109
+ ENG_BUDDY_HOOKS = [
110
+ "eng-buddy-auto-log",
111
+ "eng-buddy-draft-enforcer",
112
+ "eng-buddy-learning-capture",
113
+ "eng-buddy-session-snapshot",
114
+ "eng-buddy-session-end",
115
+ "eng-buddy-pre-compaction",
116
+ "eng-buddy-post-compaction",
117
+ "eng-buddy-task-sync",
118
+ "eng-buddy-session-manager",
119
+ ]
120
+
121
+ # --- Clean eng-buddy entries from all hook events ---
122
+ for event in list(hooks.keys()):
123
+ hooks[event] = remove_hook_commands(hooks[event], ENG_BUDDY_HOOKS)
124
+
125
+ # --- UserPromptSubmit: add ftm hooks to existing matcher-less entry (or create one) ---
126
+ ups_hooks = hooks.setdefault("UserPromptSubmit", [])
127
+
128
+ FTM_UPS_COMMANDS = [
129
+ f"{hooks_dir}/ftm-auto-log.sh",
130
+ f"{hooks_dir}/ftm-pre-compaction.sh",
131
+ f"{hooks_dir}/ftm-post-compaction.sh",
132
+ ]
133
+
134
+ # Find the first matcher-less (global) entry
135
+ global_entry = None
136
+ for entry in ups_hooks:
137
+ if not entry.get("matcher"):
138
+ global_entry = entry
139
+ break
140
+
141
+ if global_entry is None:
142
+ global_entry = {"hooks": []}
143
+ ups_hooks.insert(0, global_entry)
144
+
145
+ existing_cmds = {h["command"] for h in global_entry["hooks"]}
146
+ for cmd in FTM_UPS_COMMANDS:
147
+ if cmd not in existing_cmds:
148
+ global_entry["hooks"].append({"type": "command", "command": cmd})
149
+
150
+ # --- PostToolUse: add ftm-learning-capture to existing global entry ---
151
+ ptu_hooks = hooks.setdefault("PostToolUse", [])
152
+
153
+ FTM_PTU_COMMANDS = [
154
+ f"{hooks_dir}/ftm-learning-capture.sh",
155
+ ]
156
+
157
+ # Find global (no matcher or empty matcher) PostToolUse entry
158
+ ptu_global = None
159
+ for entry in ptu_hooks:
160
+ matcher = entry.get("matcher", "")
161
+ if not matcher:
162
+ ptu_global = entry
163
+ break
164
+
165
+ if ptu_global is None:
166
+ ptu_global = {"hooks": []}
167
+ ptu_hooks.insert(0, ptu_global)
168
+
169
+ existing_ptu_cmds = {h["command"] for h in ptu_global["hooks"]}
170
+ for cmd in FTM_PTU_COMMANDS:
171
+ if cmd not in existing_ptu_cmds:
172
+ ptu_global["hooks"].insert(0, {"type": "command", "command": cmd})
173
+
174
+ # --- SessionEnd: ensure snapshot and end are in SEPARATE matcher entries ---
175
+ # Snapshot must complete before end (snapshot reads context.json status; end modifies it)
176
+ se_hooks = hooks.setdefault("SessionEnd", [])
177
+
178
+ SNAPSHOT_CMD = f"{hooks_dir}/ftm-session-snapshot.sh"
179
+ END_CMD = f"{hooks_dir}/ftm-session-end.sh"
180
+
181
+ # Check if already present
182
+ has_snapshot = any(
183
+ any(h.get("command") == SNAPSHOT_CMD for h in e.get("hooks", []))
184
+ for e in se_hooks
185
+ )
186
+ has_end = any(
187
+ any(h.get("command") == END_CMD for h in e.get("hooks", []))
188
+ for e in se_hooks
189
+ )
190
+
191
+ # Remove any combined entry that has both (shouldn't happen but clean up if so)
192
+ cleaned_se = []
193
+ for entry in se_hooks:
194
+ cmds = [h.get("command", "") for h in entry.get("hooks", [])]
195
+ if SNAPSHOT_CMD in cmds and END_CMD in cmds:
196
+ # Split into two separate entries
197
+ snap_hooks = [h for h in entry["hooks"] if h.get("command") != END_CMD]
198
+ end_hooks = [h for h in entry["hooks"] if h.get("command") == END_CMD]
199
+ other_hooks = [h for h in entry["hooks"] if h.get("command") not in (SNAPSHOT_CMD, END_CMD)]
200
+ if snap_hooks or other_hooks:
201
+ cleaned_se.append({"hooks": snap_hooks + other_hooks})
202
+ if end_hooks:
203
+ cleaned_se.append({"hooks": end_hooks})
204
+ has_snapshot = True
205
+ has_end = True
206
+ else:
207
+ cleaned_se.append(entry)
208
+ se_hooks[:] = cleaned_se
209
+
210
+ # Add snapshot entry (first)
211
+ if not has_snapshot:
212
+ se_hooks.insert(0, {"hooks": [{"type": "command", "command": SNAPSHOT_CMD}]})
213
+
214
+ # Add end entry (separate, after snapshot)
215
+ if not has_end:
216
+ # Find position of snapshot entry and insert end after it
217
+ snap_idx = next(
218
+ (i for i, e in enumerate(se_hooks)
219
+ if any(h.get("command") == SNAPSHOT_CMD for h in e.get("hooks", []))),
220
+ -1
221
+ )
222
+ insert_pos = snap_idx + 1 if snap_idx >= 0 else len(se_hooks)
223
+ se_hooks.insert(insert_pos, {"hooks": [{"type": "command", "command": END_CMD}]})
224
+
225
+ with open(settings_path, "w") as f:
226
+ json.dump(settings, f, indent=2)
227
+ f.write("\n")
228
+
229
+ print("settings.json updated successfully")
230
+ PYEOF
231
+
232
+ log "Hook installation complete."
233
+ log ""
234
+ log "Installed hooks:"
235
+ for hook in "$HOOKS_DEST"/ftm-*.sh; do
236
+ [ -f "$hook" ] || continue
237
+ echo " $(basename "$hook")"
238
+ done
239
+ log ""
240
+ log "Verify with: ls $HOOKS_DEST/ftm-*.sh | wc -l"
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+ # ftm-learning-capture.sh
3
+ # Hook: Capture write/task completions into learning engine DB
4
+ # Trigger: PostToolUse
5
+ #
6
+ # Behavior:
7
+ # - Runs only for active ftm sessions (context.json has non-completed task)
8
+ # - Captures Write/Edit/Bash/task-style MCP completions into learning events
9
+ # - Routes known categories into knowledge files via brain.py if available
10
+ # - If category mapping is unknown, asks Claude to confirm category expansion
11
+
12
+ set -euo pipefail
13
+
14
+ FTM_STATE="$HOME/.claude/ftm-state"
15
+ CONTEXT_JSON="$FTM_STATE/blackboard/context.json"
16
+
17
+ PAYLOAD=$(cat)
18
+ if [ -z "$PAYLOAD" ]; then
19
+ exit 0
20
+ fi
21
+
22
+ HOOK_EVENT=$(printf '%s' "$PAYLOAD" | python3 -c '
23
+ import json, sys
24
+ try:
25
+ print(json.load(sys.stdin).get("hook_event_name", ""))
26
+ except Exception:
27
+ print("")
28
+ ' 2>/dev/null)
29
+
30
+ if [ "$HOOK_EVENT" != "PostToolUse" ]; then
31
+ exit 0
32
+ fi
33
+
34
+ TOOL_NAME=$(printf '%s' "$PAYLOAD" | python3 -c '
35
+ import json, sys
36
+ try:
37
+ print(json.load(sys.stdin).get("tool_name", ""))
38
+ except Exception:
39
+ print("")
40
+ ' 2>/dev/null)
41
+
42
+ case "$TOOL_NAME" in
43
+ Write|Edit|MultiEdit|NotebookEdit|Bash|Task|mcp__*) ;;
44
+ *) exit 0 ;;
45
+ esac
46
+
47
+ # Session gating: check if context.json has an active (non-completed) session
48
+ IS_FTM_ACTIVE=$(python3 -c "
49
+ import json, sys
50
+ try:
51
+ with open('$CONTEXT_JSON') as f:
52
+ d = json.load(f)
53
+ task = d.get('current_task', {})
54
+ status = task.get('status', '')
55
+ print('1' if status not in ('', 'completed', 'none') else '0')
56
+ except Exception:
57
+ print('0')
58
+ " 2>/dev/null)
59
+
60
+ if [ "$IS_FTM_ACTIVE" != "1" ]; then
61
+ exit 0
62
+ fi
63
+
64
+ # Attempt brain.py capture if available (ftm skill may ship its own brain.py)
65
+ BRAIN_PY="$HOME/.claude/skills/ftm/bin/brain.py"
66
+ if [ ! -f "$BRAIN_PY" ]; then
67
+ BRAIN_PY="$HOME/.claude/ftm-state/brain.py"
68
+ fi
69
+
70
+ if [ -f "$BRAIN_PY" ]; then
71
+ RESULT=$(printf '%s' "$PAYLOAD" | python3 "$BRAIN_PY" --capture-post-tool 2>/dev/null || echo '{"recorded": false}')
72
+
73
+ NEEDS_EXPANSION=$(printf '%s' "$RESULT" | python3 -c '
74
+ import json, sys
75
+ try:
76
+ print("1" if json.load(sys.stdin).get("needs_category_expansion") else "0")
77
+ except Exception:
78
+ print("0")
79
+ ' 2>/dev/null)
80
+
81
+ if [ "$NEEDS_EXPANSION" = "1" ]; then
82
+ PROPOSED_CATEGORY=$(printf '%s' "$RESULT" | python3 -c '
83
+ import json, sys
84
+ try:
85
+ print(json.load(sys.stdin).get("proposed_category", "new-category"))
86
+ except Exception:
87
+ print("new-category")
88
+ ' 2>/dev/null)
89
+
90
+ SAFE_PROPOSED=$(printf '%s' "$PROPOSED_CATEGORY" | tr -cd '[:alnum:]-_')
91
+ if [ -z "$SAFE_PROPOSED" ]; then
92
+ SAFE_PROPOSED="new-category"
93
+ fi
94
+
95
+ echo ""
96
+ echo "[Learning Engine] Captured a completion event that does not match an existing learning category."
97
+ echo "Before wrapping up, ask the user:"
98
+ echo "\"Should we add learning category '$SAFE_PROPOSED' so future ftm completions route cleanly?\""
99
+ echo ""
100
+ fi
101
+ fi
102
+
103
+ # --- Feed Playbook Tracer if active trace exists ---
104
+ ACTIVE_TRACE_FILE="$FTM_STATE/.active-trace-id"
105
+ if [ -f "$ACTIVE_TRACE_FILE" ] && [ -f "$BRAIN_PY" ]; then
106
+ TRACE_ID=$(cat "$ACTIVE_TRACE_FILE")
107
+ printf '%s' "$PAYLOAD" | python3 -c "
108
+ import sys, json
109
+ d = json.load(sys.stdin)
110
+ tool_name = d.get('tool_name', '')
111
+ if tool_name:
112
+ event = {'trace_id': '$TRACE_ID', 'event': {'type': 'tool_call', 'tool': tool_name, 'params': d.get('tool_input', {})}}
113
+ json.dump(event, sys.stdout)
114
+ " 2>/dev/null | python3 "$BRAIN_PY" --playbook-trace-event 2>/dev/null
115
+ fi
116
+
117
+ exit 0