feed-the-machine 1.2.0 → 1.3.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/bin/install.mjs +272 -25
- package/hooks/ftm-blackboard-enforcer.sh +28 -27
- package/hooks/ftm-plan-gate.sh +21 -25
- package/install.sh +238 -111
- package/package.json +1 -1
- package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
- 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/routes/__pycache__/plan.cpython-314.pyc +0 -0
package/bin/install.mjs
CHANGED
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* npx feed-the-machine — installs ftm skills into ~/.claude/skills/
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Full install: skills, hooks, settings.json merge, and verification.
|
|
7
|
+
* Safe to re-run — idempotent.
|
|
8
|
+
*
|
|
9
|
+
* Flags:
|
|
10
|
+
* --with-inbox Also install the inbox service
|
|
11
|
+
* --no-hooks Skip hooks entirely
|
|
12
|
+
* --skip-merge Install hook files but don't touch settings.json
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { existsSync, mkdirSync, readdirSync, lstatSync, readFileSync, writeFileSync, copyFileSync, symlinkSync, unlinkSync, chmodSync, cpSync } from "fs";
|
|
@@ -21,15 +26,25 @@ const SKILLS_DIR = join(HOME, ".claude", "skills");
|
|
|
21
26
|
const STATE_DIR = join(HOME, ".claude", "ftm-state");
|
|
22
27
|
const CONFIG_DIR = join(HOME, ".claude");
|
|
23
28
|
const HOOKS_DIR = join(HOME, ".claude", "hooks");
|
|
29
|
+
const SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
|
|
24
30
|
const INBOX_INSTALL_DIR = join(HOME, ".claude", "ftm-inbox");
|
|
25
31
|
|
|
26
32
|
const ARGS = process.argv.slice(2);
|
|
27
33
|
const WITH_INBOX = ARGS.includes("--with-inbox");
|
|
34
|
+
const NO_HOOKS = ARGS.includes("--no-hooks");
|
|
35
|
+
const SKIP_MERGE = ARGS.includes("--skip-merge");
|
|
36
|
+
|
|
37
|
+
let warnCount = 0;
|
|
28
38
|
|
|
29
39
|
function log(msg) {
|
|
30
40
|
console.log(` ${msg}`);
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
function warn(msg) {
|
|
44
|
+
console.log(` WARN: ${msg}`);
|
|
45
|
+
warnCount++;
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
function ensureDir(dir) {
|
|
34
49
|
if (!existsSync(dir)) {
|
|
35
50
|
mkdirSync(dir, { recursive: true });
|
|
@@ -52,7 +67,212 @@ function safeSymlink(src, dest) {
|
|
|
52
67
|
log(`LINK ${name}`);
|
|
53
68
|
}
|
|
54
69
|
|
|
70
|
+
function commandExists(cmd) {
|
|
71
|
+
try {
|
|
72
|
+
execSync(`command -v ${cmd}`, { stdio: "ignore" });
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function commandVersion(cmd, flag = "--version") {
|
|
80
|
+
try {
|
|
81
|
+
return execSync(`${cmd} ${flag}`, { encoding: "utf8" }).trim().split("\n")[0];
|
|
82
|
+
} catch {
|
|
83
|
+
return "unknown";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Preflight ---
|
|
88
|
+
|
|
89
|
+
function preflight() {
|
|
90
|
+
console.log("Preflight checks...");
|
|
91
|
+
|
|
92
|
+
if (!NO_HOOKS) {
|
|
93
|
+
// jq is required for all shell hooks (they parse JSON stdin via jq)
|
|
94
|
+
if (!commandExists("jq")) {
|
|
95
|
+
console.log("");
|
|
96
|
+
console.log(" ERROR: jq is required for FTM hooks.");
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log(" Install it:");
|
|
99
|
+
console.log(" macOS: brew install jq");
|
|
100
|
+
console.log(" Ubuntu: sudo apt-get install jq");
|
|
101
|
+
console.log(" Alpine: apk add jq");
|
|
102
|
+
console.log("");
|
|
103
|
+
console.log(" Or skip hooks: npx feed-the-machine --no-hooks");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
log(`jq: ${commandVersion("jq")}`);
|
|
107
|
+
log(`node: ${process.version}`);
|
|
108
|
+
} else {
|
|
109
|
+
log("hooks skipped (--no-hooks)");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Settings Merge ---
|
|
116
|
+
|
|
117
|
+
function mergeHooksIntoSettings() {
|
|
118
|
+
const templatePath = join(REPO_DIR, "hooks", "settings-template.json");
|
|
119
|
+
if (!existsSync(templatePath)) {
|
|
120
|
+
warn("hooks/settings-template.json not found — hooks installed but not registered");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log("Registering hooks in settings.json...");
|
|
126
|
+
|
|
127
|
+
// Read and expand ~ to actual home directory
|
|
128
|
+
const rawTemplate = readFileSync(templatePath, "utf8");
|
|
129
|
+
const expandedTemplate = rawTemplate.replace(/~\/.claude/g, join(HOME, ".claude"));
|
|
130
|
+
const template = JSON.parse(expandedTemplate);
|
|
131
|
+
const templateHooks = template.hooks || {};
|
|
132
|
+
|
|
133
|
+
if (!existsSync(SETTINGS_FILE)) {
|
|
134
|
+
// No settings.json — create one with just the hooks
|
|
135
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify({ hooks: templateHooks }, null, 2) + "\n");
|
|
136
|
+
log("CREATED settings.json with FTM hooks");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Read existing settings
|
|
141
|
+
const existing = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
|
|
142
|
+
|
|
143
|
+
// Backup
|
|
144
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
145
|
+
const backupPath = `${SETTINGS_FILE}.ftm-backup-${ts}`;
|
|
146
|
+
copyFileSync(SETTINGS_FILE, backupPath);
|
|
147
|
+
log(`BACKUP ${backupPath}`);
|
|
148
|
+
|
|
149
|
+
// Ensure hooks key exists
|
|
150
|
+
if (!existing.hooks) {
|
|
151
|
+
existing.hooks = {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Merge each event type
|
|
155
|
+
const events = ["PreToolUse", "UserPromptSubmit", "PostToolUse", "Stop"];
|
|
156
|
+
for (const event of events) {
|
|
157
|
+
const templateEntries = templateHooks[event] || [];
|
|
158
|
+
const existingEntries = existing.hooks[event] || [];
|
|
159
|
+
|
|
160
|
+
if (templateEntries.length === 0) continue;
|
|
161
|
+
|
|
162
|
+
// Check if FTM hooks are already present by looking for ftm- in command paths
|
|
163
|
+
const existingCommands = JSON.stringify(existingEntries);
|
|
164
|
+
const alreadyPresent = templateEntries.some((entry) => {
|
|
165
|
+
const hooks = entry.hooks || [];
|
|
166
|
+
return hooks.some((h) => {
|
|
167
|
+
const cmd = h.command || "";
|
|
168
|
+
const cmdBase = basename(cmd.split(" ").pop()); // handle "node foo.mjs"
|
|
169
|
+
return existingCommands.includes(cmdBase);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (alreadyPresent) {
|
|
174
|
+
log(`SKIP ${event} hooks (already configured)`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
existing.hooks[event] = [...existingEntries, ...templateEntries];
|
|
179
|
+
log(`MERGE ${event} hooks`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(existing, null, 2) + "\n");
|
|
183
|
+
log("UPDATED settings.json");
|
|
184
|
+
console.log("");
|
|
185
|
+
log("Hooks are active.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Verification ---
|
|
189
|
+
|
|
190
|
+
function verify(skillCount, hookCount) {
|
|
191
|
+
console.log("");
|
|
192
|
+
console.log("Verifying installation...");
|
|
193
|
+
|
|
194
|
+
let errors = 0;
|
|
195
|
+
|
|
196
|
+
// Check skill symlinks resolve
|
|
197
|
+
let brokenLinks = 0;
|
|
198
|
+
const skillEntries = readdirSync(SKILLS_DIR).filter((f) => f.startsWith("ftm"));
|
|
199
|
+
for (const entry of skillEntries) {
|
|
200
|
+
const fullPath = join(SKILLS_DIR, entry);
|
|
201
|
+
try {
|
|
202
|
+
if (lstatSync(fullPath).isSymbolicLink() && !existsSync(fullPath)) {
|
|
203
|
+
warn(`broken symlink: ${entry}`);
|
|
204
|
+
brokenLinks++;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// ignore
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (brokenLinks === 0) {
|
|
211
|
+
log(`Skills: ${skillCount} linked, all symlinks valid`);
|
|
212
|
+
} else {
|
|
213
|
+
errors++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check blackboard state
|
|
217
|
+
const contextFile = join(STATE_DIR, "blackboard", "context.json");
|
|
218
|
+
const patternsFile = join(STATE_DIR, "blackboard", "patterns.json");
|
|
219
|
+
if (existsSync(contextFile) && existsSync(patternsFile)) {
|
|
220
|
+
log("Blackboard: initialized");
|
|
221
|
+
} else {
|
|
222
|
+
warn("blackboard state incomplete");
|
|
223
|
+
errors++;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check config
|
|
227
|
+
if (existsSync(join(CONFIG_DIR, "ftm-config.yml"))) {
|
|
228
|
+
log("Config: present");
|
|
229
|
+
} else {
|
|
230
|
+
warn("ftm-config.yml missing");
|
|
231
|
+
errors++;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check hooks
|
|
235
|
+
if (!NO_HOOKS && hookCount > 0) {
|
|
236
|
+
const hookFiles = readdirSync(HOOKS_DIR).filter((f) => f.startsWith("ftm-"));
|
|
237
|
+
const allExecutable = hookFiles
|
|
238
|
+
.filter((f) => f.endsWith(".sh"))
|
|
239
|
+
.every((f) => {
|
|
240
|
+
try {
|
|
241
|
+
const stat = lstatSync(join(HOOKS_DIR, f));
|
|
242
|
+
return (stat.mode & 0o111) !== 0;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (allExecutable) {
|
|
249
|
+
log(`Hooks: ${hookCount} installed, all executable`);
|
|
250
|
+
} else {
|
|
251
|
+
warn("some hook files not executable");
|
|
252
|
+
errors++;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Verify settings.json has FTM hooks
|
|
256
|
+
if (!SKIP_MERGE && existsSync(SETTINGS_FILE)) {
|
|
257
|
+
const settingsContent = readFileSync(SETTINGS_FILE, "utf8");
|
|
258
|
+
const ftmMatches = (settingsContent.match(/ftm-/g) || []).length;
|
|
259
|
+
if (ftmMatches > 0) {
|
|
260
|
+
log(`Settings: ${ftmMatches} FTM entries in settings.json`);
|
|
261
|
+
} else {
|
|
262
|
+
warn("no FTM hooks found in settings.json");
|
|
263
|
+
errors++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { errors };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Main ---
|
|
272
|
+
|
|
55
273
|
function main() {
|
|
274
|
+
preflight();
|
|
275
|
+
|
|
56
276
|
console.log(`Installing ftm skills from: ${REPO_DIR}`);
|
|
57
277
|
console.log(`Linking into: ${SKILLS_DIR}`);
|
|
58
278
|
console.log("");
|
|
@@ -82,9 +302,13 @@ function main() {
|
|
|
82
302
|
safeSymlink(join(REPO_DIR, dir), join(SKILLS_DIR, dir));
|
|
83
303
|
}
|
|
84
304
|
|
|
305
|
+
console.log("");
|
|
306
|
+
log(`${ymlFiles.length} skills linked.`);
|
|
307
|
+
|
|
85
308
|
// Set up blackboard state (copy templates, don't overwrite existing data)
|
|
86
309
|
const bbDir = join(REPO_DIR, "ftm-state", "blackboard");
|
|
87
310
|
if (existsSync(bbDir)) {
|
|
311
|
+
console.log("");
|
|
88
312
|
ensureDir(join(STATE_DIR, "blackboard", "experiences"));
|
|
89
313
|
|
|
90
314
|
const jsonFiles = readdirSync(bbDir).filter((f) => f.endsWith(".json"));
|
|
@@ -113,39 +337,62 @@ function main() {
|
|
|
113
337
|
}
|
|
114
338
|
|
|
115
339
|
// Install hooks
|
|
116
|
-
const hooksDir = join(REPO_DIR, "hooks");
|
|
117
340
|
let hookCount = 0;
|
|
118
|
-
|
|
119
|
-
|
|
341
|
+
|
|
342
|
+
if (NO_HOOKS) {
|
|
120
343
|
console.log("");
|
|
121
|
-
console.log("
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
344
|
+
console.log("Skipping hooks (--no-hooks).");
|
|
345
|
+
} else {
|
|
346
|
+
const hooksDir = join(REPO_DIR, "hooks");
|
|
347
|
+
if (existsSync(hooksDir)) {
|
|
348
|
+
ensureDir(HOOKS_DIR);
|
|
349
|
+
console.log("");
|
|
350
|
+
console.log("Installing hooks...");
|
|
351
|
+
|
|
352
|
+
const hookFiles = readdirSync(hooksDir).filter(
|
|
353
|
+
(f) => f.startsWith("ftm-") && (f.endsWith(".sh") || f.endsWith(".mjs"))
|
|
354
|
+
);
|
|
355
|
+
for (const hook of hookFiles) {
|
|
356
|
+
const src = join(hooksDir, hook);
|
|
357
|
+
const dest = join(HOOKS_DIR, hook);
|
|
358
|
+
const action = existsSync(dest) ? "UPDATE" : "INSTALL";
|
|
359
|
+
copyFileSync(src, dest);
|
|
360
|
+
if (hook.endsWith(".sh")) {
|
|
361
|
+
chmodSync(dest, 0o755);
|
|
362
|
+
}
|
|
363
|
+
log(`${action} ${hook}`);
|
|
364
|
+
hookCount++;
|
|
133
365
|
}
|
|
134
|
-
|
|
135
|
-
|
|
366
|
+
|
|
367
|
+
console.log("");
|
|
368
|
+
log(`${hookCount} hooks installed to ${HOOKS_DIR}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Merge hooks into settings.json
|
|
372
|
+
if (SKIP_MERGE) {
|
|
373
|
+
console.log("");
|
|
374
|
+
log("Skipping settings.json merge (--skip-merge).");
|
|
375
|
+
log("Add entries from hooks/settings-template.json to ~/.claude/settings.json manually.");
|
|
376
|
+
} else {
|
|
377
|
+
mergeHooksIntoSettings();
|
|
136
378
|
}
|
|
137
379
|
}
|
|
138
380
|
|
|
381
|
+
// Verification
|
|
382
|
+
const { errors } = verify(ymlFiles.length, hookCount);
|
|
383
|
+
|
|
384
|
+
// Summary
|
|
139
385
|
console.log("");
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(" See docs/HOOKS.md for details.");
|
|
386
|
+
if (errors === 0 && warnCount === 0) {
|
|
387
|
+
console.log(`Done. ${ymlFiles.length} skills, ${hookCount} hooks. Everything checks out.`);
|
|
388
|
+
} else {
|
|
389
|
+
console.log(`Done. ${ymlFiles.length} skills, ${hookCount} hooks. ${warnCount} warning(s).`);
|
|
390
|
+
}
|
|
146
391
|
console.log("");
|
|
392
|
+
console.log("Restart Claude Code (or start a new session) to pick up the skills.");
|
|
147
393
|
|
|
148
394
|
if (WITH_INBOX) {
|
|
395
|
+
console.log("");
|
|
149
396
|
installInbox();
|
|
150
397
|
} else {
|
|
151
398
|
console.log("Try: /ftm help");
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ftm-blackboard-enforcer.sh
|
|
3
|
-
# Stop hook that
|
|
4
|
-
#
|
|
5
|
-
# to write the experience first.
|
|
3
|
+
# Stop hook that nudges Claude to record an experience if meaningful work
|
|
4
|
+
# was done but no blackboard entry was written.
|
|
6
5
|
#
|
|
7
|
-
#
|
|
6
|
+
# Uses additionalContext (not "decision: block") so Claude can still act on
|
|
7
|
+
# the reminder. A blocking stop creates a deadlock — Claude can't write files
|
|
8
|
+
# after the user ends the conversation.
|
|
9
|
+
#
|
|
10
|
+
# "Meaningful work" = 3+ edits tracked by the edit counter,
|
|
8
11
|
# or ftm skills were invoked (checked via context.json).
|
|
9
12
|
#
|
|
10
13
|
# Hook: Stop
|
|
@@ -13,29 +16,23 @@ set -euo pipefail
|
|
|
13
16
|
|
|
14
17
|
INPUT=$(cat)
|
|
15
18
|
|
|
16
|
-
# Prevent infinite loop — if this hook already fired, let Claude stop
|
|
17
|
-
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
18
|
-
if [[ "$STOP_HOOK_ACTIVE" == "true" ]]; then
|
|
19
|
-
exit 0
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
19
|
STATE_DIR="$HOME/.claude/ftm-state"
|
|
23
20
|
BB_DIR="$STATE_DIR/blackboard"
|
|
24
21
|
EDIT_COUNTER="$STATE_DIR/.edit-count"
|
|
25
22
|
CONTEXT_FILE="$BB_DIR/context.json"
|
|
26
23
|
EXPERIENCES_DIR="$BB_DIR/experiences"
|
|
27
|
-
EXPERIENCE_INDEX="$EXPERIENCES_DIR/index.json"
|
|
28
|
-
|
|
29
|
-
CURRENT_SESSION="${CLAUDE_SESSION_ID:-unknown}"
|
|
30
24
|
|
|
31
25
|
# Check 1: Were there meaningful edits this session?
|
|
26
|
+
# Edit counter contains just a number now (no session ID).
|
|
27
|
+
# If the counter file is recent (< 4 hours) and >= 3, count as meaningful.
|
|
32
28
|
HAD_EDITS=false
|
|
33
29
|
if [[ -f "$EDIT_COUNTER" ]]; then
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
COUNTER_AGE=$(( $(date +%s) - $(stat -c %Y "$EDIT_COUNTER" 2>/dev/null || stat -f %m "$EDIT_COUNTER" 2>/dev/null || echo "0") ))
|
|
31
|
+
if [[ "$COUNTER_AGE" -lt 14400 ]]; then
|
|
32
|
+
STORED_COUNT=$(cat "$EDIT_COUNTER" 2>/dev/null || echo "0")
|
|
33
|
+
if [[ "$STORED_COUNT" -ge 3 ]]; then
|
|
34
|
+
HAD_EDITS=true
|
|
35
|
+
fi
|
|
39
36
|
fi
|
|
40
37
|
fi
|
|
41
38
|
|
|
@@ -48,8 +45,10 @@ if [[ -f "$CONTEXT_FILE" ]]; then
|
|
|
48
45
|
fi
|
|
49
46
|
fi
|
|
50
47
|
|
|
51
|
-
# If no meaningful work detected, allow stop
|
|
48
|
+
# If no meaningful work detected, allow stop quietly
|
|
52
49
|
if [[ "$HAD_EDITS" == "false" && "$HAD_SKILLS" == "false" ]]; then
|
|
50
|
+
# Clean up session markers
|
|
51
|
+
rm -f "$EDIT_COUNTER" "$STATE_DIR/.plan-presented" 2>/dev/null
|
|
53
52
|
exit 0
|
|
54
53
|
fi
|
|
55
54
|
|
|
@@ -58,19 +57,17 @@ TODAY=$(date +%Y-%m-%d)
|
|
|
58
57
|
HAS_EXPERIENCE=false
|
|
59
58
|
|
|
60
59
|
if [[ -d "$EXPERIENCES_DIR" ]]; then
|
|
61
|
-
# Check for experience files created today
|
|
62
60
|
TODAY_EXPERIENCE=$(find "$EXPERIENCES_DIR" -name "${TODAY}*" -type f 2>/dev/null | head -1)
|
|
63
61
|
if [[ -n "$TODAY_EXPERIENCE" ]]; then
|
|
64
62
|
HAS_EXPERIENCE=true
|
|
65
63
|
fi
|
|
66
64
|
fi
|
|
67
65
|
|
|
68
|
-
# Also check if context.json was updated
|
|
66
|
+
# Also check if context.json was updated today (recent_decisions not empty)
|
|
69
67
|
if [[ -f "$CONTEXT_FILE" ]]; then
|
|
70
68
|
DECISIONS_COUNT=$(jq -r '.recent_decisions | length' "$CONTEXT_FILE" 2>/dev/null || echo "0")
|
|
71
69
|
LAST_UPDATED=$(jq -r '.session_metadata.last_updated // ""' "$CONTEXT_FILE" 2>/dev/null || echo "")
|
|
72
70
|
if [[ "$DECISIONS_COUNT" -gt 0 && -n "$LAST_UPDATED" ]]; then
|
|
73
|
-
# Check if last_updated is from today
|
|
74
71
|
if [[ "$LAST_UPDATED" == *"$TODAY"* ]]; then
|
|
75
72
|
HAS_EXPERIENCE=true
|
|
76
73
|
fi
|
|
@@ -78,17 +75,21 @@ if [[ -f "$CONTEXT_FILE" ]]; then
|
|
|
78
75
|
fi
|
|
79
76
|
|
|
80
77
|
if [[ "$HAS_EXPERIENCE" == "true" ]]; then
|
|
81
|
-
# Blackboard was written, allow stop
|
|
82
|
-
# Clean up session markers
|
|
78
|
+
# Blackboard was written, clean up and allow stop
|
|
83
79
|
rm -f "$EDIT_COUNTER" "$STATE_DIR/.plan-presented" 2>/dev/null
|
|
84
80
|
exit 0
|
|
85
81
|
fi
|
|
86
82
|
|
|
87
|
-
# Work was done but no blackboard write —
|
|
83
|
+
# Work was done but no blackboard write — nudge (don't block)
|
|
88
84
|
cat <<'JSON'
|
|
89
85
|
{
|
|
90
|
-
"
|
|
91
|
-
|
|
86
|
+
"hookSpecificOutput": {
|
|
87
|
+
"hookEventName": "Stop",
|
|
88
|
+
"additionalContext": "[ftm-blackboard-enforcer] You did meaningful work this session but did not record an experience to the blackboard. Before finishing, please: (1) Update ~/.claude/ftm-state/blackboard/context.json with current_task status and recent_decisions. (2) Write an experience file to ~/.claude/ftm-state/blackboard/experiences/ with task_type, tags, outcome, and lessons. (3) Update ~/.claude/ftm-state/blackboard/experiences/index.json with the new entry. This is how ftm learns — skipping it means the next session starts from zero."
|
|
89
|
+
}
|
|
92
90
|
}
|
|
93
91
|
JSON
|
|
92
|
+
|
|
93
|
+
# Clean up session markers regardless — don't let stale state carry over
|
|
94
|
+
rm -f "$EDIT_COUNTER" "$STATE_DIR/.plan-presented" 2>/dev/null
|
|
94
95
|
exit 0
|
package/hooks/ftm-plan-gate.sh
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
# ftm-plan-gate.sh
|
|
3
3
|
# PreToolUse hook for Edit/Write tools.
|
|
4
4
|
#
|
|
5
|
-
# Checks if a plan has been presented
|
|
6
|
-
#
|
|
7
|
-
# a medium+ task (detected by ftm-state), injects additionalContext
|
|
5
|
+
# Checks if a plan has been presented this session before allowing code edits.
|
|
6
|
+
# If no plan marker exists and the edit count is climbing, injects warnings
|
|
8
7
|
# telling Claude to stop and present a plan first.
|
|
9
8
|
#
|
|
10
|
-
# The marker file is created by Claude
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# The marker file (~/.claude/ftm-state/.plan-presented) is created by Claude
|
|
10
|
+
# when it presents a plan. Any non-empty content counts as "plan presented".
|
|
11
|
+
# The file is cleaned up by the blackboard enforcer at session end.
|
|
13
12
|
#
|
|
14
13
|
# Hook: PreToolUse (matcher: Edit|Write)
|
|
15
14
|
|
|
@@ -25,9 +24,7 @@ fi
|
|
|
25
24
|
|
|
26
25
|
STATE_DIR="$HOME/.claude/ftm-state"
|
|
27
26
|
PLAN_MARKER="$STATE_DIR/.plan-presented"
|
|
28
|
-
SESSION_MARKER="$STATE_DIR/.session-id"
|
|
29
27
|
EDIT_COUNTER="$STATE_DIR/.edit-count"
|
|
30
|
-
SKILL_FILES_DIR="$HOME/.claude/skills"
|
|
31
28
|
|
|
32
29
|
# Get the file being edited
|
|
33
30
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
@@ -47,37 +44,36 @@ if [[ "$FILE_PATH" == *".claude/skills/"* ]] || \
|
|
|
47
44
|
exit 0
|
|
48
45
|
fi
|
|
49
46
|
|
|
50
|
-
# If plan marker exists
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
# If plan marker exists (any content), allow edits
|
|
48
|
+
if [[ -f "$PLAN_MARKER" ]] && [[ -s "$PLAN_MARKER" ]]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Reset edit counter if it's stale (older than 4 hours = likely a new session)
|
|
53
|
+
if [[ -f "$EDIT_COUNTER" ]]; then
|
|
54
|
+
COUNTER_AGE=$(( $(date +%s) - $(stat -c %Y "$EDIT_COUNTER" 2>/dev/null || echo "0") ))
|
|
55
|
+
if [[ "$COUNTER_AGE" -gt 14400 ]]; then
|
|
56
|
+
rm -f "$EDIT_COUNTER"
|
|
56
57
|
fi
|
|
57
58
|
fi
|
|
58
59
|
|
|
59
|
-
# Count edits
|
|
60
|
+
# Count edits without a plan marker
|
|
60
61
|
EDIT_COUNT=0
|
|
61
62
|
if [[ -f "$EDIT_COUNTER" ]]; then
|
|
62
|
-
|
|
63
|
-
STORED_SESSION=$(echo "$STORED" | cut -d: -f2)
|
|
64
|
-
if [[ "$STORED_SESSION" == "$CURRENT_SESSION" ]]; then
|
|
65
|
-
EDIT_COUNT=$(echo "$STORED" | cut -d: -f1)
|
|
66
|
-
fi
|
|
63
|
+
EDIT_COUNT=$(cat "$EDIT_COUNTER" 2>/dev/null || echo "0")
|
|
67
64
|
fi
|
|
68
65
|
|
|
69
66
|
EDIT_COUNT=$((EDIT_COUNT + 1))
|
|
70
|
-
echo "$
|
|
67
|
+
echo "$EDIT_COUNT" > "$EDIT_COUNTER"
|
|
71
68
|
|
|
72
|
-
# First 2 edits get a
|
|
69
|
+
# First 2 edits get a soft reminder (don't block — could be micro tasks)
|
|
73
70
|
# After 3+ edits without a plan marker, escalate the warning
|
|
74
71
|
if [[ $EDIT_COUNT -le 2 ]]; then
|
|
75
|
-
# Soft reminder — inject context but allow
|
|
76
72
|
cat <<'JSON'
|
|
77
73
|
{
|
|
78
74
|
"hookSpecificOutput": {
|
|
79
75
|
"hookEventName": "PreToolUse",
|
|
80
|
-
"additionalContext": "[ftm-plan-gate] You are editing files without having presented a plan this session. If this task is medium+ (touches 3+ files, involves external systems, or has stakeholder coordination), you MUST present a numbered plan and get user approval BEFORE editing code. If this is a micro/small task, you can proceed — but create the plan marker
|
|
76
|
+
"additionalContext": "[ftm-plan-gate] You are editing files without having presented a plan this session. If this task is medium+ (touches 3+ files, involves external systems, or has stakeholder coordination), you MUST present a numbered plan and get user approval BEFORE editing code. If this is a micro/small task, you can proceed — but create the plan marker: write any content to ~/.claude/ftm-state/.plan-presented to acknowledge you've considered it."
|
|
81
77
|
}
|
|
82
78
|
}
|
|
83
79
|
JSON
|
|
@@ -89,7 +85,7 @@ cat <<'JSON'
|
|
|
89
85
|
{
|
|
90
86
|
"hookSpecificOutput": {
|
|
91
87
|
"hookEventName": "PreToolUse",
|
|
92
|
-
"additionalContext": "[ftm-plan-gate WARNING] You have made 3+ file edits this session without presenting a plan. This is exactly the 'grinding without a plan' pattern that ftm-mind is supposed to prevent. STOP editing and do one of: (1) Present a numbered plan to the user and wait for approval, then write
|
|
88
|
+
"additionalContext": "[ftm-plan-gate WARNING] You have made 3+ file edits this session without presenting a plan. This is exactly the 'grinding without a plan' pattern that ftm-mind is supposed to prevent. STOP editing and do one of: (1) Present a numbered plan to the user and wait for approval, then write any content to ~/.claude/ftm-state/.plan-presented. (2) If the user explicitly said 'just do it' or this is genuinely a micro task, write the plan marker to acknowledge you've considered it. Do NOT continue editing without addressing this."
|
|
93
89
|
}
|
|
94
90
|
}
|
|
95
91
|
JSON
|
package/install.sh
CHANGED
|
@@ -3,11 +3,13 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
# FTM Skills Installer
|
|
5
5
|
# Creates symlinks from this repo into ~/.claude/skills/ so slash commands work.
|
|
6
|
-
#
|
|
6
|
+
# Installs hooks, merges them into settings.json, and verifies the result.
|
|
7
|
+
# Safe to re-run — idempotent.
|
|
7
8
|
#
|
|
8
9
|
# Usage:
|
|
9
|
-
# ./install.sh #
|
|
10
|
-
# ./install.sh --
|
|
10
|
+
# ./install.sh # Full install (skills + hooks + settings merge)
|
|
11
|
+
# ./install.sh --no-hooks # Skills and state only, skip hooks entirely
|
|
12
|
+
# ./install.sh --skip-merge # Install hook files but don't touch settings.json
|
|
11
13
|
|
|
12
14
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
15
|
SKILLS_DIR="$HOME/.claude/skills"
|
|
@@ -16,13 +18,64 @@ CONFIG_DIR="$HOME/.claude"
|
|
|
16
18
|
HOOKS_DIR="$HOME/.claude/hooks"
|
|
17
19
|
SETTINGS_FILE="$CONFIG_DIR/settings.json"
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
NO_HOOKS=false
|
|
22
|
+
SKIP_MERGE=false
|
|
20
23
|
for arg in "$@"; do
|
|
21
24
|
case "$arg" in
|
|
22
|
-
--
|
|
25
|
+
--no-hooks) NO_HOOKS=true ;;
|
|
26
|
+
--skip-merge) SKIP_MERGE=true ;;
|
|
27
|
+
# Keep --setup-hooks for backwards compat (now a no-op since merge is default)
|
|
28
|
+
--setup-hooks) ;;
|
|
23
29
|
esac
|
|
24
30
|
done
|
|
25
31
|
|
|
32
|
+
WARN_COUNT=0
|
|
33
|
+
warn() {
|
|
34
|
+
echo " WARN: $1"
|
|
35
|
+
WARN_COUNT=$((WARN_COUNT + 1))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# --- Preflight Checks ---
|
|
39
|
+
|
|
40
|
+
echo "Preflight checks..."
|
|
41
|
+
|
|
42
|
+
# Check jq (required for hooks and settings merge)
|
|
43
|
+
if ! command -v jq &>/dev/null; then
|
|
44
|
+
if [ "$NO_HOOKS" = true ]; then
|
|
45
|
+
echo " jq not found (ok — hooks skipped)"
|
|
46
|
+
else
|
|
47
|
+
echo ""
|
|
48
|
+
echo " ERROR: jq is required for FTM hooks."
|
|
49
|
+
echo ""
|
|
50
|
+
echo " Install it:"
|
|
51
|
+
echo " macOS: brew install jq"
|
|
52
|
+
echo " Ubuntu: sudo apt-get install jq"
|
|
53
|
+
echo " Alpine: apk add jq"
|
|
54
|
+
echo ""
|
|
55
|
+
echo " Or skip hooks: ./install.sh --no-hooks"
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
else
|
|
59
|
+
echo " jq: $(jq --version)"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Check node (required for event logger hook)
|
|
63
|
+
if ! command -v node &>/dev/null; then
|
|
64
|
+
if [ "$NO_HOOKS" = true ]; then
|
|
65
|
+
echo " node not found (ok — hooks skipped)"
|
|
66
|
+
else
|
|
67
|
+
echo ""
|
|
68
|
+
echo " ERROR: Node.js is required for the FTM event logger hook."
|
|
69
|
+
echo ""
|
|
70
|
+
echo " Install it: https://nodejs.org/"
|
|
71
|
+
echo " Or skip hooks: ./install.sh --no-hooks"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
else
|
|
75
|
+
echo " node: $(node --version)"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo ""
|
|
26
79
|
echo "Installing FTM skills from: $REPO_DIR"
|
|
27
80
|
echo "Linking into: $SKILLS_DIR"
|
|
28
81
|
echo ""
|
|
@@ -98,134 +151,208 @@ fi
|
|
|
98
151
|
|
|
99
152
|
# --- Hooks ---
|
|
100
153
|
|
|
101
|
-
|
|
102
|
-
echo "Installing hooks..."
|
|
103
|
-
|
|
104
|
-
if [ -d "$REPO_DIR/hooks" ]; then
|
|
105
|
-
mkdir -p "$HOOKS_DIR"
|
|
106
|
-
HOOK_COUNT=0
|
|
107
|
-
|
|
108
|
-
# Install shell hooks
|
|
109
|
-
for hook in "$REPO_DIR/hooks"/ftm-*.sh; do
|
|
110
|
-
[ -f "$hook" ] || continue
|
|
111
|
-
name=$(basename "$hook")
|
|
112
|
-
target="$HOOKS_DIR/$name"
|
|
113
|
-
if [ -f "$target" ]; then
|
|
114
|
-
cp "$hook" "$target"
|
|
115
|
-
chmod +x "$target"
|
|
116
|
-
echo " UPDATE $name"
|
|
117
|
-
else
|
|
118
|
-
cp "$hook" "$target"
|
|
119
|
-
chmod +x "$target"
|
|
120
|
-
echo " INSTALL $name"
|
|
121
|
-
fi
|
|
122
|
-
HOOK_COUNT=$((HOOK_COUNT + 1))
|
|
123
|
-
done
|
|
124
|
-
|
|
125
|
-
# Install Node.js hooks
|
|
126
|
-
for hook in "$REPO_DIR/hooks"/ftm-*.mjs; do
|
|
127
|
-
[ -f "$hook" ] || continue
|
|
128
|
-
name=$(basename "$hook")
|
|
129
|
-
target="$HOOKS_DIR/$name"
|
|
130
|
-
if [ -f "$target" ]; then
|
|
131
|
-
cp "$hook" "$target"
|
|
132
|
-
echo " UPDATE $name"
|
|
133
|
-
else
|
|
134
|
-
cp "$hook" "$target"
|
|
135
|
-
echo " INSTALL $name"
|
|
136
|
-
fi
|
|
137
|
-
HOOK_COUNT=$((HOOK_COUNT + 1))
|
|
138
|
-
done
|
|
154
|
+
HOOK_COUNT=0
|
|
139
155
|
|
|
156
|
+
if [ "$NO_HOOKS" = true ]; then
|
|
140
157
|
echo ""
|
|
141
|
-
echo "
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# --- Hook Config Merge (--setup-hooks) ---
|
|
145
|
-
|
|
146
|
-
if [ "$SETUP_HOOKS" = true ]; then
|
|
158
|
+
echo "Skipping hooks (--no-hooks)."
|
|
159
|
+
else
|
|
147
160
|
echo ""
|
|
148
|
-
echo "
|
|
161
|
+
echo "Installing hooks..."
|
|
162
|
+
|
|
163
|
+
if [ -d "$REPO_DIR/hooks" ]; then
|
|
164
|
+
mkdir -p "$HOOKS_DIR"
|
|
165
|
+
|
|
166
|
+
# Install shell hooks
|
|
167
|
+
for hook in "$REPO_DIR/hooks"/ftm-*.sh; do
|
|
168
|
+
[ -f "$hook" ] || continue
|
|
169
|
+
name=$(basename "$hook")
|
|
170
|
+
target="$HOOKS_DIR/$name"
|
|
171
|
+
if [ -f "$target" ]; then
|
|
172
|
+
cp "$hook" "$target"
|
|
173
|
+
chmod +x "$target"
|
|
174
|
+
echo " UPDATE $name"
|
|
175
|
+
else
|
|
176
|
+
cp "$hook" "$target"
|
|
177
|
+
chmod +x "$target"
|
|
178
|
+
echo " INSTALL $name"
|
|
179
|
+
fi
|
|
180
|
+
HOOK_COUNT=$((HOOK_COUNT + 1))
|
|
181
|
+
done
|
|
149
182
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
183
|
+
# Install Node.js hooks
|
|
184
|
+
for hook in "$REPO_DIR/hooks"/ftm-*.mjs; do
|
|
185
|
+
[ -f "$hook" ] || continue
|
|
186
|
+
name=$(basename "$hook")
|
|
187
|
+
target="$HOOKS_DIR/$name"
|
|
188
|
+
if [ -f "$target" ]; then
|
|
189
|
+
cp "$hook" "$target"
|
|
190
|
+
echo " UPDATE $name"
|
|
191
|
+
else
|
|
192
|
+
cp "$hook" "$target"
|
|
193
|
+
echo " INSTALL $name"
|
|
194
|
+
fi
|
|
195
|
+
HOOK_COUNT=$((HOOK_COUNT + 1))
|
|
196
|
+
done
|
|
155
197
|
|
|
156
|
-
|
|
157
|
-
echo "
|
|
158
|
-
exit 1
|
|
198
|
+
echo ""
|
|
199
|
+
echo " $HOOK_COUNT hooks installed to $HOOKS_DIR"
|
|
159
200
|
fi
|
|
160
201
|
|
|
161
|
-
#
|
|
162
|
-
EXPANDED_TEMPLATE=$(sed "s|~/.claude|$HOME/.claude|g" "$TEMPLATE")
|
|
202
|
+
# --- Hook Config Merge (default behavior now) ---
|
|
163
203
|
|
|
164
|
-
if [
|
|
165
|
-
|
|
166
|
-
echo "
|
|
167
|
-
echo "
|
|
204
|
+
if [ "$SKIP_MERGE" = true ]; then
|
|
205
|
+
echo ""
|
|
206
|
+
echo " Skipping settings.json merge (--skip-merge)."
|
|
207
|
+
echo " Add entries from hooks/settings-template.json to ~/.claude/settings.json manually."
|
|
168
208
|
else
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
BACKUP="$SETTINGS_FILE.ftm-backup-$(date +%Y%m%d%H%M%S)"
|
|
172
|
-
cp "$SETTINGS_FILE" "$BACKUP"
|
|
173
|
-
echo " BACKUP $BACKUP"
|
|
209
|
+
echo ""
|
|
210
|
+
echo "Registering hooks in settings.json..."
|
|
174
211
|
|
|
175
|
-
|
|
176
|
-
|
|
212
|
+
TEMPLATE="$REPO_DIR/hooks/settings-template.json"
|
|
213
|
+
if [ ! -f "$TEMPLATE" ]; then
|
|
214
|
+
warn "hooks/settings-template.json not found — hooks installed but not registered"
|
|
215
|
+
else
|
|
216
|
+
# Expand ~ to $HOME in the template (jq doesn't expand shell paths)
|
|
217
|
+
EXPANDED_TEMPLATE=$(sed "s|~/.claude|$HOME/.claude|g" "$TEMPLATE")
|
|
218
|
+
|
|
219
|
+
if [ ! -f "$SETTINGS_FILE" ]; then
|
|
220
|
+
# No settings.json — create one from the template hooks section
|
|
221
|
+
echo "$EXPANDED_TEMPLATE" | jq '{hooks: .hooks}' > "$SETTINGS_FILE"
|
|
222
|
+
echo " CREATED $SETTINGS_FILE with FTM hooks"
|
|
223
|
+
else
|
|
224
|
+
# Merge FTM hooks into existing settings.json
|
|
225
|
+
BACKUP="$SETTINGS_FILE.ftm-backup-$(date +%Y%m%d%H%M%S)"
|
|
226
|
+
cp "$SETTINGS_FILE" "$BACKUP"
|
|
227
|
+
echo " BACKUP $BACKUP"
|
|
228
|
+
|
|
229
|
+
# Extract the hooks section from the template
|
|
230
|
+
TEMPLATE_HOOKS=$(echo "$EXPANDED_TEMPLATE" | jq '.hooks')
|
|
231
|
+
|
|
232
|
+
# Read existing settings
|
|
233
|
+
EXISTING=$(cat "$SETTINGS_FILE")
|
|
234
|
+
|
|
235
|
+
# Ensure hooks key exists
|
|
236
|
+
if echo "$EXISTING" | jq -e '.hooks' >/dev/null 2>&1; then
|
|
237
|
+
: # hooks key exists
|
|
238
|
+
else
|
|
239
|
+
EXISTING=$(echo "$EXISTING" | jq '. + {hooks: {}}')
|
|
240
|
+
fi
|
|
177
241
|
|
|
178
|
-
|
|
179
|
-
|
|
242
|
+
# Merge each hook event type
|
|
243
|
+
for EVENT in PreToolUse UserPromptSubmit PostToolUse Stop; do
|
|
244
|
+
TEMPLATE_ENTRIES=$(echo "$TEMPLATE_HOOKS" | jq --arg e "$EVENT" '.[$e] // []')
|
|
245
|
+
EXISTING_ENTRIES=$(echo "$EXISTING" | jq --arg e "$EVENT" '.hooks[$e] // []')
|
|
246
|
+
|
|
247
|
+
# Check if any FTM hooks are already present (by checking command paths)
|
|
248
|
+
FTM_COMMANDS=$(echo "$TEMPLATE_ENTRIES" | jq -r '.[].hooks[]?.command // empty' 2>/dev/null)
|
|
249
|
+
ALREADY_PRESENT=false
|
|
250
|
+
|
|
251
|
+
for cmd in $FTM_COMMANDS; do
|
|
252
|
+
cmd_basename=$(basename "$cmd")
|
|
253
|
+
if echo "$EXISTING_ENTRIES" | jq -r '.[].hooks[]?.command // empty' 2>/dev/null | grep -q "$cmd_basename"; then
|
|
254
|
+
ALREADY_PRESENT=true
|
|
255
|
+
break
|
|
256
|
+
fi
|
|
257
|
+
done
|
|
258
|
+
|
|
259
|
+
if [ "$ALREADY_PRESENT" = true ]; then
|
|
260
|
+
echo " SKIP $EVENT hooks (already configured)"
|
|
261
|
+
continue
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
# Append template entries to existing
|
|
265
|
+
MERGED=$(jq -n --argjson existing "$EXISTING_ENTRIES" --argjson template "$TEMPLATE_ENTRIES" '$existing + $template')
|
|
266
|
+
EXISTING=$(echo "$EXISTING" | jq --arg e "$EVENT" --argjson m "$MERGED" '.hooks[$e] = $m')
|
|
267
|
+
echo " MERGE $EVENT hooks"
|
|
268
|
+
done
|
|
269
|
+
|
|
270
|
+
echo "$EXISTING" | jq '.' > "$SETTINGS_FILE"
|
|
271
|
+
echo " UPDATED $SETTINGS_FILE"
|
|
272
|
+
fi
|
|
180
273
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
: # hooks key exists
|
|
184
|
-
else
|
|
185
|
-
EXISTING=$(echo "$EXISTING" | jq '. + {hooks: {}}')
|
|
274
|
+
echo ""
|
|
275
|
+
echo " Hooks are active."
|
|
186
276
|
fi
|
|
277
|
+
fi
|
|
278
|
+
fi
|
|
187
279
|
|
|
188
|
-
|
|
189
|
-
for EVENT in PreToolUse UserPromptSubmit PostToolUse Stop; do
|
|
190
|
-
TEMPLATE_ENTRIES=$(echo "$TEMPLATE_HOOKS" | jq --arg e "$EVENT" '.[$e] // []')
|
|
191
|
-
EXISTING_ENTRIES=$(echo "$EXISTING" | jq --arg e "$EVENT" '.hooks[$e] // []')
|
|
280
|
+
# --- Verification ---
|
|
192
281
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
ALREADY_PRESENT=false
|
|
282
|
+
echo ""
|
|
283
|
+
echo "Verifying installation..."
|
|
196
284
|
|
|
197
|
-
|
|
198
|
-
cmd_basename=$(basename "$cmd")
|
|
199
|
-
if echo "$EXISTING_ENTRIES" | jq -r '.[].hooks[]?.command // empty' 2>/dev/null | grep -q "$cmd_basename"; then
|
|
200
|
-
ALREADY_PRESENT=true
|
|
201
|
-
break
|
|
202
|
-
fi
|
|
203
|
-
done
|
|
285
|
+
ERRORS=0
|
|
204
286
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
287
|
+
# Check skill symlinks resolve
|
|
288
|
+
BROKEN_LINKS=0
|
|
289
|
+
for link in "$SKILLS_DIR"/ftm*; do
|
|
290
|
+
[ -L "$link" ] || continue
|
|
291
|
+
if [ ! -e "$link" ]; then
|
|
292
|
+
warn "broken symlink: $link"
|
|
293
|
+
BROKEN_LINKS=$((BROKEN_LINKS + 1))
|
|
294
|
+
fi
|
|
295
|
+
done
|
|
296
|
+
if [ "$BROKEN_LINKS" -eq 0 ]; then
|
|
297
|
+
echo " Skills: $SKILL_COUNT linked, all symlinks valid"
|
|
298
|
+
else
|
|
299
|
+
ERRORS=$((ERRORS + 1))
|
|
300
|
+
fi
|
|
209
301
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
302
|
+
# Check blackboard state
|
|
303
|
+
if [ -f "$STATE_DIR/blackboard/context.json" ] && [ -f "$STATE_DIR/blackboard/patterns.json" ]; then
|
|
304
|
+
echo " Blackboard: initialized"
|
|
305
|
+
else
|
|
306
|
+
warn "blackboard state incomplete"
|
|
307
|
+
ERRORS=$((ERRORS + 1))
|
|
308
|
+
fi
|
|
215
309
|
|
|
216
|
-
|
|
217
|
-
|
|
310
|
+
# Check config
|
|
311
|
+
if [ -f "$CONFIG_DIR/ftm-config.yml" ]; then
|
|
312
|
+
echo " Config: present"
|
|
313
|
+
else
|
|
314
|
+
warn "ftm-config.yml missing"
|
|
315
|
+
ERRORS=$((ERRORS + 1))
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Check hooks (if installed)
|
|
319
|
+
if [ "$NO_HOOKS" = false ] && [ "$HOOK_COUNT" -gt 0 ]; then
|
|
320
|
+
# Verify hook files exist and are executable
|
|
321
|
+
HOOK_OK=true
|
|
322
|
+
for hook in "$HOOKS_DIR"/ftm-*.sh; do
|
|
323
|
+
[ -f "$hook" ] || continue
|
|
324
|
+
if [ ! -x "$hook" ]; then
|
|
325
|
+
warn "$(basename "$hook") not executable"
|
|
326
|
+
HOOK_OK=false
|
|
327
|
+
fi
|
|
328
|
+
done
|
|
329
|
+
|
|
330
|
+
if [ "$HOOK_OK" = true ]; then
|
|
331
|
+
echo " Hooks: $HOOK_COUNT installed, all executable"
|
|
332
|
+
else
|
|
333
|
+
ERRORS=$((ERRORS + 1))
|
|
218
334
|
fi
|
|
219
335
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
336
|
+
# Verify settings.json has FTM hooks registered
|
|
337
|
+
if [ "$SKIP_MERGE" = false ] && [ -f "$SETTINGS_FILE" ]; then
|
|
338
|
+
FTM_REGISTERED=$(grep -c 'ftm-' "$SETTINGS_FILE" 2>/dev/null || echo "0")
|
|
339
|
+
if [ "$FTM_REGISTERED" -gt 0 ]; then
|
|
340
|
+
echo " Settings: $FTM_REGISTERED FTM entries in settings.json"
|
|
341
|
+
else
|
|
342
|
+
warn "no FTM hooks found in settings.json"
|
|
343
|
+
ERRORS=$((ERRORS + 1))
|
|
344
|
+
fi
|
|
345
|
+
fi
|
|
227
346
|
fi
|
|
228
347
|
|
|
348
|
+
# --- Summary ---
|
|
349
|
+
|
|
350
|
+
echo ""
|
|
351
|
+
if [ "$ERRORS" -eq 0 ] && [ "$WARN_COUNT" -eq 0 ]; then
|
|
352
|
+
echo "Done. $SKILL_COUNT skills, $HOOK_COUNT hooks. Everything checks out."
|
|
353
|
+
else
|
|
354
|
+
echo "Done. $SKILL_COUNT skills, $HOOK_COUNT hooks. $WARN_COUNT warning(s)."
|
|
355
|
+
fi
|
|
229
356
|
echo ""
|
|
230
|
-
echo "
|
|
357
|
+
echo "Restart Claude Code (or start a new session) to pick up the skills."
|
|
231
358
|
echo "Try: /ftm help"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feed-the-machine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A unified intelligence layer for Claude Code — 22 skills with OODA-based reasoning, persistent memory, multi-model deliberation, and optional operator cockpit inbox",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "kkudumu",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|