feed-the-machine 1.6.0 → 1.7.0
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/LICENSE +21 -21
- package/README.md +170 -170
- package/bin/brain.py +1340 -0
- package/bin/convert_claude_skills_to_codex.py +490 -0
- package/bin/generate-manifest.mjs +463 -463
- package/bin/harden_codex_skills.py +141 -0
- package/bin/install.mjs +491 -491
- package/bin/migrate-eng-buddy-data.py +875 -0
- package/bin/playbook_engine/__init__.py +1 -0
- package/bin/playbook_engine/conftest.py +8 -0
- package/bin/playbook_engine/extractor.py +33 -0
- package/bin/playbook_engine/manager.py +102 -0
- package/bin/playbook_engine/models.py +84 -0
- package/bin/playbook_engine/registry.py +35 -0
- package/bin/playbook_engine/test_extractor.py +72 -0
- package/bin/playbook_engine/test_integration.py +129 -0
- package/bin/playbook_engine/test_manager.py +85 -0
- package/bin/playbook_engine/test_models.py +166 -0
- package/bin/playbook_engine/test_registry.py +67 -0
- package/bin/playbook_engine/test_tracer.py +86 -0
- package/bin/playbook_engine/tracer.py +93 -0
- package/bin/tasks_db.py +456 -0
- package/docs/HOOKS.md +243 -243
- package/docs/INBOX.md +233 -233
- package/ftm/SKILL.md +125 -122
- package/ftm-audit/SKILL.md +623 -623
- package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
- package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
- package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
- package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
- package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
- package/ftm-audit/scripts/run-knip.sh +23 -23
- package/ftm-audit.yml +2 -2
- package/ftm-brainstorm/SKILL.md +1003 -498
- package/ftm-brainstorm/evals/evals.json +180 -100
- package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
- package/ftm-brainstorm/references/agent-prompts.md +552 -224
- package/ftm-brainstorm/references/plan-template.md +209 -121
- package/ftm-brainstorm.yml +2 -2
- package/ftm-browse/SKILL.md +454 -454
- package/ftm-browse/daemon/browser-manager.ts +206 -206
- package/ftm-browse/daemon/bun.lock +30 -30
- package/ftm-browse/daemon/cli.ts +347 -347
- package/ftm-browse/daemon/commands.ts +410 -410
- package/ftm-browse/daemon/main.ts +357 -357
- package/ftm-browse/daemon/package.json +17 -17
- package/ftm-browse/daemon/server.ts +189 -189
- package/ftm-browse/daemon/snapshot.ts +519 -519
- package/ftm-browse/daemon/tsconfig.json +22 -22
- package/ftm-browse.yml +4 -4
- package/ftm-capture/SKILL.md +370 -370
- package/ftm-capture.yml +4 -4
- package/ftm-codex-gate/SKILL.md +361 -361
- package/ftm-codex-gate.yml +2 -2
- package/ftm-config/SKILL.md +422 -345
- package/ftm-config.default.yml +125 -82
- package/ftm-config.yml +44 -2
- package/ftm-council/SKILL.md +416 -416
- package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
- package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
- package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
- package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
- package/ftm-council.yml +2 -2
- package/ftm-dashboard/SKILL.md +163 -163
- package/ftm-dashboard.yml +4 -4
- package/ftm-debug/SKILL.md +1037 -1037
- package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
- package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
- package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
- package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
- package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
- package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
- package/ftm-debug.yml +2 -2
- package/ftm-diagram/SKILL.md +277 -277
- package/ftm-diagram.yml +2 -2
- package/ftm-executor/SKILL.md +777 -777
- package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
- package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
- package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
- package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
- package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
- package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
- package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
- package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
- package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
- package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
- package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
- package/ftm-executor/runtime/package.json +8 -8
- package/ftm-executor.yml +2 -2
- package/ftm-git/SKILL.md +441 -441
- package/ftm-git/evals/evals.json +26 -26
- package/ftm-git/evals/promptfoo.yaml +75 -75
- package/ftm-git/hooks/post-commit-experience.sh +92 -92
- package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
- package/ftm-git/references/protocols/REMEDIATION.md +139 -139
- package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
- package/ftm-git.yml +2 -2
- package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -64
- package/ftm-inbox/backend/adapters/base.py +230 -230
- package/ftm-inbox/backend/adapters/freshservice.py +104 -104
- package/ftm-inbox/backend/adapters/gmail.py +125 -125
- package/ftm-inbox/backend/adapters/jira.py +136 -136
- package/ftm-inbox/backend/adapters/registry.py +192 -192
- package/ftm-inbox/backend/adapters/slack.py +110 -110
- package/ftm-inbox/backend/db/connection.py +54 -54
- package/ftm-inbox/backend/db/schema.py +78 -78
- package/ftm-inbox/backend/executor/__init__.py +7 -7
- package/ftm-inbox/backend/executor/engine.py +149 -149
- package/ftm-inbox/backend/executor/step_runner.py +98 -98
- package/ftm-inbox/backend/main.py +103 -103
- package/ftm-inbox/backend/models/__init__.py +1 -1
- package/ftm-inbox/backend/models/unified_task.py +36 -36
- package/ftm-inbox/backend/planner/__init__.py +6 -6
- package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/generator.py +127 -127
- package/ftm-inbox/backend/planner/schema.py +34 -34
- package/ftm-inbox/backend/requirements.txt +5 -5
- package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -186
- package/ftm-inbox/backend/routes/health.py +52 -52
- package/ftm-inbox/backend/routes/inbox.py +68 -68
- package/ftm-inbox/backend/routes/plan.py +271 -271
- package/ftm-inbox/bin/launchagent.mjs +91 -91
- package/ftm-inbox/bin/setup.mjs +188 -188
- package/ftm-inbox/bin/start.sh +10 -10
- package/ftm-inbox/bin/status.sh +17 -17
- package/ftm-inbox/bin/stop.sh +8 -8
- package/ftm-inbox/config.example.yml +55 -55
- package/ftm-inbox/package-lock.json +2898 -2898
- package/ftm-inbox/package.json +26 -26
- package/ftm-inbox/postcss.config.js +6 -6
- package/ftm-inbox/src/app.css +199 -199
- package/ftm-inbox/src/app.html +18 -18
- package/ftm-inbox/src/lib/api.ts +166 -166
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
- package/ftm-inbox/src/lib/theme.ts +47 -47
- package/ftm-inbox/src/routes/+layout.svelte +76 -76
- package/ftm-inbox/src/routes/+page.svelte +401 -401
- package/ftm-inbox/svelte.config.js +12 -12
- package/ftm-inbox/tailwind.config.ts +63 -63
- package/ftm-inbox/tsconfig.json +13 -13
- package/ftm-inbox/vite.config.ts +6 -6
- package/ftm-intent/SKILL.md +241 -241
- package/ftm-intent.yml +2 -2
- package/ftm-manifest.json +3794 -3794
- package/ftm-map/SKILL.md +291 -291
- package/ftm-map/scripts/db.py +712 -712
- package/ftm-map/scripts/index.py +415 -415
- package/ftm-map/scripts/parser.py +224 -224
- package/ftm-map/scripts/queries/go-tags.scm +20 -20
- package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
- package/ftm-map/scripts/queries/python-tags.scm +31 -31
- package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
- package/ftm-map/scripts/queries/rust-tags.scm +37 -37
- package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
- package/ftm-map/scripts/query.py +301 -301
- package/ftm-map/scripts/ranker.py +377 -377
- package/ftm-map/scripts/requirements.txt +5 -5
- package/ftm-map/scripts/setup-hooks.sh +27 -27
- package/ftm-map/scripts/setup.sh +56 -56
- package/ftm-map/scripts/test_db.py +364 -364
- package/ftm-map/scripts/test_parser.py +174 -174
- package/ftm-map/scripts/test_query.py +183 -183
- package/ftm-map/scripts/test_ranker.py +199 -199
- package/ftm-map/scripts/views.py +591 -591
- package/ftm-map.yml +2 -2
- package/ftm-mind/SKILL.md +201 -1943
- package/ftm-mind/evals/promptfoo.yaml +142 -142
- package/ftm-mind/references/blackboard-protocol.md +110 -0
- package/ftm-mind/references/blackboard-schema.md +328 -328
- package/ftm-mind/references/complexity-guide.md +110 -110
- package/ftm-mind/references/complexity-sizing.md +138 -0
- package/ftm-mind/references/decide-act-protocol.md +172 -0
- package/ftm-mind/references/direct-execution.md +51 -0
- package/ftm-mind/references/environment-discovery.md +77 -0
- package/ftm-mind/references/event-registry.md +319 -319
- package/ftm-mind/references/mcp-inventory.md +300 -296
- package/ftm-mind/references/ops-routing.md +47 -0
- package/ftm-mind/references/orient-protocol.md +234 -0
- package/ftm-mind/references/personality.md +40 -0
- package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
- package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
- package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
- package/ftm-mind/references/reflexion-protocol.md +249 -249
- package/ftm-mind/references/routing/SCENARIOS.md +22 -22
- package/ftm-mind/references/routing-scenarios.md +35 -35
- package/ftm-mind.yml +2 -2
- package/ftm-ops.yml +4 -0
- package/ftm-pause/SKILL.md +395 -395
- package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
- package/ftm-pause/references/protocols/VALIDATION.md +80 -80
- package/ftm-pause.yml +2 -2
- package/ftm-researcher/SKILL.md +275 -275
- package/ftm-researcher/evals/agent-diversity.yaml +17 -17
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
- package/ftm-researcher/references/adaptive-search.md +116 -116
- package/ftm-researcher/references/agent-prompts.md +193 -193
- package/ftm-researcher/references/council-integration.md +193 -193
- package/ftm-researcher/references/output-format.md +203 -203
- package/ftm-researcher/references/synthesis-pipeline.md +165 -165
- package/ftm-researcher/scripts/score_credibility.py +234 -234
- package/ftm-researcher/scripts/validate_research.py +92 -92
- package/ftm-researcher.yml +2 -2
- package/ftm-resume/SKILL.md +518 -518
- package/ftm-resume/references/protocols/VALIDATION.md +172 -172
- package/ftm-resume.yml +2 -2
- package/ftm-retro/SKILL.md +380 -380
- package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
- package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
- package/ftm-retro.yml +2 -2
- package/ftm-routine/SKILL.md +170 -170
- package/ftm-routine.yml +4 -4
- package/ftm-state/blackboard/capabilities.json +5 -5
- package/ftm-state/blackboard/capabilities.schema.json +27 -27
- package/ftm-state/blackboard/context.json +37 -23
- package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
- package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
- package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
- package/ftm-state/blackboard/experiences/index.json +58 -9
- package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
- package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
- package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
- package/ftm-state/blackboard/patterns.json +6 -6
- package/ftm-state/schemas/context.schema.json +130 -130
- package/ftm-state/schemas/experience-index.schema.json +77 -77
- package/ftm-state/schemas/experience.schema.json +78 -78
- package/ftm-state/schemas/patterns.schema.json +44 -44
- package/ftm-upgrade/SKILL.md +194 -194
- package/ftm-upgrade/scripts/check-version.sh +76 -76
- package/ftm-upgrade/scripts/upgrade.sh +143 -143
- package/ftm-upgrade.yml +2 -2
- package/ftm-verify.yml +2 -2
- package/ftm.yml +2 -2
- package/hooks/ftm-auto-log.sh +137 -0
- package/hooks/ftm-blackboard-enforcer.sh +93 -93
- package/hooks/ftm-discovery-reminder.sh +90 -90
- package/hooks/ftm-drafts-gate.sh +61 -61
- package/hooks/ftm-event-logger.mjs +107 -107
- package/hooks/ftm-install-hooks.sh +240 -0
- package/hooks/ftm-learning-capture.sh +117 -0
- package/hooks/ftm-map-autodetect.sh +79 -79
- package/hooks/ftm-pending-sync-check.sh +22 -22
- package/hooks/ftm-plan-gate.sh +92 -92
- package/hooks/ftm-post-commit-trigger.sh +57 -57
- package/hooks/ftm-post-compaction.sh +138 -0
- package/hooks/ftm-pre-compaction.sh +147 -0
- package/hooks/ftm-session-end.sh +52 -0
- package/hooks/ftm-session-snapshot.sh +213 -0
- package/hooks/settings-template.json +81 -81
- package/install.sh +363 -363
- package/package.json +84 -84
- 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
|