feed-the-machine 1.0.0 → 1.2.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/generate-manifest.mjs +253 -0
- package/bin/install.mjs +134 -4
- package/docs/HOOKS.md +243 -0
- package/docs/INBOX.md +233 -0
- package/ftm/SKILL.md +34 -0
- package/ftm-audit/SKILL.md +69 -0
- package/ftm-brainstorm/SKILL.md +51 -0
- package/ftm-browse/SKILL.md +39 -0
- package/ftm-capture/SKILL.md +370 -0
- package/ftm-capture.yml +4 -0
- package/ftm-codex-gate/SKILL.md +59 -0
- package/ftm-config/SKILL.md +35 -0
- package/ftm-council/SKILL.md +56 -0
- package/ftm-dashboard/SKILL.md +163 -0
- package/ftm-debug/SKILL.md +84 -0
- package/ftm-diagram/SKILL.md +44 -0
- package/ftm-executor/SKILL.md +97 -0
- package/ftm-git/SKILL.md +60 -0
- package/ftm-inbox/backend/__init__.py +0 -0
- package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/adapters/__init__.py +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -0
- package/ftm-inbox/backend/adapters/base.py +230 -0
- package/ftm-inbox/backend/adapters/freshservice.py +104 -0
- package/ftm-inbox/backend/adapters/gmail.py +125 -0
- package/ftm-inbox/backend/adapters/jira.py +136 -0
- package/ftm-inbox/backend/adapters/registry.py +192 -0
- package/ftm-inbox/backend/adapters/slack.py +110 -0
- package/ftm-inbox/backend/db/__init__.py +0 -0
- package/ftm-inbox/backend/db/connection.py +54 -0
- package/ftm-inbox/backend/db/schema.py +78 -0
- package/ftm-inbox/backend/executor/__init__.py +7 -0
- package/ftm-inbox/backend/executor/engine.py +149 -0
- package/ftm-inbox/backend/executor/step_runner.py +98 -0
- package/ftm-inbox/backend/main.py +103 -0
- package/ftm-inbox/backend/models/__init__.py +1 -0
- package/ftm-inbox/backend/models/unified_task.py +36 -0
- package/ftm-inbox/backend/planner/__init__.py +6 -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/planner/generator.py +127 -0
- package/ftm-inbox/backend/planner/schema.py +34 -0
- package/ftm-inbox/backend/requirements.txt +5 -0
- package/ftm-inbox/backend/routes/__init__.py +0 -0
- package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -0
- package/ftm-inbox/backend/routes/health.py +52 -0
- package/ftm-inbox/backend/routes/inbox.py +68 -0
- package/ftm-inbox/backend/routes/plan.py +271 -0
- package/ftm-inbox/bin/launchagent.mjs +91 -0
- package/ftm-inbox/bin/setup.mjs +188 -0
- package/ftm-inbox/bin/start.sh +10 -0
- package/ftm-inbox/bin/status.sh +17 -0
- package/ftm-inbox/bin/stop.sh +8 -0
- package/ftm-inbox/config.example.yml +55 -0
- package/ftm-inbox/package-lock.json +2898 -0
- package/ftm-inbox/package.json +26 -0
- package/ftm-inbox/postcss.config.js +6 -0
- package/ftm-inbox/src/app.css +199 -0
- package/ftm-inbox/src/app.html +18 -0
- package/ftm-inbox/src/lib/api.ts +166 -0
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
- package/ftm-inbox/src/lib/theme.ts +47 -0
- package/ftm-inbox/src/routes/+layout.svelte +76 -0
- package/ftm-inbox/src/routes/+page.svelte +401 -0
- package/ftm-inbox/static/favicon.png +0 -0
- package/ftm-inbox/svelte.config.js +12 -0
- package/ftm-inbox/tailwind.config.ts +63 -0
- package/ftm-inbox/tsconfig.json +13 -0
- package/ftm-inbox/vite.config.ts +6 -0
- package/ftm-intent/SKILL.md +44 -0
- package/ftm-manifest.json +3794 -0
- package/ftm-map/SKILL.md +259 -0
- package/ftm-map/scripts/db.py +391 -0
- package/ftm-map/scripts/index.py +341 -0
- package/ftm-map/scripts/parser.py +455 -0
- package/ftm-map/scripts/queries/.gitkeep +0 -0
- package/ftm-map/scripts/queries/javascript-tags.scm +23 -0
- package/ftm-map/scripts/queries/python-tags.scm +17 -0
- package/ftm-map/scripts/queries/typescript-tags.scm +29 -0
- package/ftm-map/scripts/query.py +149 -0
- package/ftm-map/scripts/requirements.txt +2 -0
- package/ftm-map/scripts/setup-hooks.sh +27 -0
- package/ftm-map/scripts/setup.sh +45 -0
- package/ftm-map/scripts/test_db.py +124 -0
- package/ftm-map/scripts/test_parser.py +106 -0
- package/ftm-map/scripts/test_query.py +66 -0
- package/ftm-map/scripts/tests/fixtures/__init__.py +0 -0
- package/ftm-map/scripts/tests/fixtures/sample_project/api.ts +16 -0
- package/ftm-map/scripts/tests/fixtures/sample_project/auth.py +15 -0
- package/ftm-map/scripts/tests/fixtures/sample_project/utils.js +16 -0
- package/ftm-map/scripts/views.py +545 -0
- package/ftm-mind/SKILL.md +173 -66
- package/ftm-pause/SKILL.md +43 -0
- package/ftm-researcher/SKILL.md +275 -0
- package/ftm-researcher/evals/agent-diversity.yaml +17 -0
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -0
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -0
- package/ftm-researcher/references/adaptive-search.md +116 -0
- package/ftm-researcher/references/agent-prompts.md +193 -0
- package/ftm-researcher/references/council-integration.md +193 -0
- package/ftm-researcher/references/output-format.md +203 -0
- package/ftm-researcher/references/synthesis-pipeline.md +165 -0
- package/ftm-researcher/scripts/score_credibility.py +234 -0
- package/ftm-researcher/scripts/validate_research.py +92 -0
- package/ftm-resume/SKILL.md +47 -0
- package/ftm-retro/SKILL.md +54 -0
- package/ftm-routine/SKILL.md +170 -0
- package/ftm-state/blackboard/capabilities.json +5 -0
- package/ftm-state/blackboard/capabilities.schema.json +27 -0
- package/ftm-upgrade/SKILL.md +41 -0
- package/ftm-upgrade/scripts/check-version.sh +1 -1
- package/ftm-upgrade/scripts/upgrade.sh +1 -1
- package/hooks/ftm-blackboard-enforcer.sh +94 -0
- package/hooks/ftm-discovery-reminder.sh +90 -0
- package/hooks/ftm-drafts-gate.sh +61 -0
- package/hooks/ftm-event-logger.mjs +107 -0
- package/hooks/ftm-map-autodetect.sh +79 -0
- package/hooks/ftm-pending-sync-check.sh +22 -0
- package/hooks/ftm-plan-gate.sh +96 -0
- package/hooks/ftm-post-commit-trigger.sh +57 -0
- package/hooks/settings-template.json +81 -0
- package/install.sh +140 -11
- package/package.json +12 -2
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# UserPromptSubmit hook: auto-detect unmapped projects when ftm skills are invoked.
|
|
3
|
+
# Detects greenfield vs brownfield and injects instructions to bootstrap ftm-map.
|
|
4
|
+
# Only fires once per project (writes .ftm-map/.offered marker).
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null)
|
|
10
|
+
|
|
11
|
+
# Quick exit if no prompt or jq unavailable
|
|
12
|
+
[ -n "$PROMPT" ] || exit 0
|
|
13
|
+
|
|
14
|
+
# Only fire on ftm-related invocations
|
|
15
|
+
PROMPT_LOWER=$(echo "$PROMPT" | tr '[:upper:]' '[:lower:]')
|
|
16
|
+
IS_FTM=false
|
|
17
|
+
for trigger in "/ftm" "ftm-" "brainstorm" "research" "debug this" "audit" "deep dive" "investigate"; do
|
|
18
|
+
if [[ "$PROMPT_LOWER" == *"$trigger"* ]]; then
|
|
19
|
+
IS_FTM=true
|
|
20
|
+
break
|
|
21
|
+
fi
|
|
22
|
+
done
|
|
23
|
+
$IS_FTM || exit 0
|
|
24
|
+
|
|
25
|
+
# Already mapped — nothing to do
|
|
26
|
+
[ -f ".ftm-map/map.db" ] && exit 0
|
|
27
|
+
|
|
28
|
+
# Already offered for this project — don't nag
|
|
29
|
+
[ -f ".ftm-map/.offered" ] && exit 0
|
|
30
|
+
|
|
31
|
+
# --- Greenfield vs Brownfield detection ---
|
|
32
|
+
|
|
33
|
+
# Count source files (fast, capped at 500 to avoid slow ls on huge repos)
|
|
34
|
+
SRC_COUNT=0
|
|
35
|
+
if command -v find &>/dev/null; then
|
|
36
|
+
SRC_COUNT=$(find . -maxdepth 4 \
|
|
37
|
+
\( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" \
|
|
38
|
+
-o -name "*.jsx" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" \
|
|
39
|
+
-o -name "*.java" -o -name "*.rb" -o -name "*.sh" -o -name "*.mjs" \
|
|
40
|
+
-o -name "*.cjs" \) \
|
|
41
|
+
-not -path "*/node_modules/*" \
|
|
42
|
+
-not -path "*/.venv/*" \
|
|
43
|
+
-not -path "*/__pycache__/*" \
|
|
44
|
+
-not -path "*/.git/*" \
|
|
45
|
+
-not -path "*/.worktrees/*" \
|
|
46
|
+
2>/dev/null | head -500 | wc -l | tr -d ' ')
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Git history depth
|
|
50
|
+
COMMIT_COUNT=0
|
|
51
|
+
if git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
|
|
52
|
+
COMMIT_COUNT=$(git rev-list --count HEAD 2>/dev/null || echo "0")
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Classify
|
|
56
|
+
if [ "$SRC_COUNT" -le 5 ] && [ "$COMMIT_COUNT" -le 3 ]; then
|
|
57
|
+
PROJECT_TYPE="greenfield"
|
|
58
|
+
PROJECT_DESC="Greenfield project (~${SRC_COUNT} source files, ${COMMIT_COUNT} commits). Small enough to index instantly."
|
|
59
|
+
elif [ "$SRC_COUNT" -le 50 ]; then
|
|
60
|
+
PROJECT_TYPE="small-brownfield"
|
|
61
|
+
PROJECT_DESC="Small brownfield project (~${SRC_COUNT} source files, ${COMMIT_COUNT} commits). Quick to index."
|
|
62
|
+
elif [ "$SRC_COUNT" -le 200 ]; then
|
|
63
|
+
PROJECT_TYPE="medium-brownfield"
|
|
64
|
+
PROJECT_DESC="Medium brownfield project (~${SRC_COUNT} source files, ${COMMIT_COUNT} commits). Index takes ~30s."
|
|
65
|
+
else
|
|
66
|
+
PROJECT_TYPE="large-brownfield"
|
|
67
|
+
PROJECT_DESC="Large brownfield project (~${SRC_COUNT} source files, ${COMMIT_COUNT} commits). Index may take 1-2 min."
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Write marker so we don't ask again
|
|
71
|
+
mkdir -p .ftm-map
|
|
72
|
+
echo "$PROJECT_TYPE" > .ftm-map/.offered
|
|
73
|
+
|
|
74
|
+
# Inject context
|
|
75
|
+
cat <<EOJSON
|
|
76
|
+
{
|
|
77
|
+
"additionalContext": "[ftm-map-autodetect] This project has no code knowledge graph yet (.ftm-map/map.db missing). ${PROJECT_DESC} Before proceeding with the ftm skill, bootstrap the map: run \`ftm-map/scripts/setup.sh\` then \`ftm-map/scripts/.venv/bin/python3 ftm-map/scripts/index.py --bootstrap .\` in the background. This enables structural queries (blast radius, dependency chains) and keeps INTENT.md/ARCHITECTURE.mmd in sync via the post-commit hook. If the user's request is time-sensitive, proceed with the skill and index in parallel."
|
|
78
|
+
}
|
|
79
|
+
EOJSON
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# UserPromptSubmit hook: checks for pending commit syncs from out-of-session commits
|
|
3
|
+
|
|
4
|
+
PENDING_FILE="$HOME/.claude/ftm-state/.pending-commit-syncs"
|
|
5
|
+
|
|
6
|
+
# Quick exit if no pending syncs
|
|
7
|
+
[ -f "$PENDING_FILE" ] || { echo '{}'; exit 0; }
|
|
8
|
+
[ -s "$PENDING_FILE" ] || { echo '{}'; exit 0; }
|
|
9
|
+
|
|
10
|
+
# Count pending entries
|
|
11
|
+
COUNT=$(wc -l < "$PENDING_FILE" | tr -d ' ')
|
|
12
|
+
|
|
13
|
+
# Read all pending entries
|
|
14
|
+
ENTRIES=$(cat "$PENDING_FILE")
|
|
15
|
+
|
|
16
|
+
# Consume the file (mark as processed)
|
|
17
|
+
rm -f "$PENDING_FILE"
|
|
18
|
+
|
|
19
|
+
# Inject context
|
|
20
|
+
cat <<EOJSON
|
|
21
|
+
{"additionalContext": "There are $COUNT pending out-of-session commits that need ftm-map incremental sync. Run ftm-map incremental for each:\n$ENTRIES"}
|
|
22
|
+
EOJSON
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ftm-plan-gate.sh
|
|
3
|
+
# PreToolUse hook for Edit/Write tools.
|
|
4
|
+
#
|
|
5
|
+
# Checks if a plan has been presented and approved for this session before
|
|
6
|
+
# allowing code edits. If no plan marker exists and the session involves
|
|
7
|
+
# a medium+ task (detected by ftm-state), injects additionalContext
|
|
8
|
+
# telling Claude to stop and present a plan first.
|
|
9
|
+
#
|
|
10
|
+
# The marker file is created by Claude when it presents a plan — we check
|
|
11
|
+
# for it here. If the marker doesn't exist but edits are happening, it
|
|
12
|
+
# means Claude skipped the planning step.
|
|
13
|
+
#
|
|
14
|
+
# Hook: PreToolUse (matcher: Edit|Write)
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
20
|
+
|
|
21
|
+
# Only gate Edit and Write tools
|
|
22
|
+
if [[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]]; then
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
STATE_DIR="$HOME/.claude/ftm-state"
|
|
27
|
+
PLAN_MARKER="$STATE_DIR/.plan-presented"
|
|
28
|
+
SESSION_MARKER="$STATE_DIR/.session-id"
|
|
29
|
+
EDIT_COUNTER="$STATE_DIR/.edit-count"
|
|
30
|
+
SKILL_FILES_DIR="$HOME/.claude/skills"
|
|
31
|
+
|
|
32
|
+
# Get the file being edited
|
|
33
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
34
|
+
|
|
35
|
+
# Always allow edits to: skill files, ftm-state, drafts, .gitignore, markdown docs
|
|
36
|
+
# These are "meta" edits that happen during planning/setup, not code grinding
|
|
37
|
+
if [[ "$FILE_PATH" == *".claude/skills/"* ]] || \
|
|
38
|
+
[[ "$FILE_PATH" == *".claude/ftm-state/"* ]] || \
|
|
39
|
+
[[ "$FILE_PATH" == *".ftm-drafts/"* ]] || \
|
|
40
|
+
[[ "$FILE_PATH" == *".gitignore" ]] || \
|
|
41
|
+
[[ "$FILE_PATH" == *"INTENT.md"* ]] || \
|
|
42
|
+
[[ "$FILE_PATH" == *"ARCHITECTURE.mmd"* ]] || \
|
|
43
|
+
[[ "$FILE_PATH" == *"STYLE.md"* ]] || \
|
|
44
|
+
[[ "$FILE_PATH" == *"DEBUG.md"* ]] || \
|
|
45
|
+
[[ "$FILE_PATH" == *"PROGRESS.md"* ]] || \
|
|
46
|
+
[[ "$FILE_PATH" == *"CLAUDE.md"* ]]; then
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# If plan marker exists and matches current session, allow
|
|
51
|
+
CURRENT_SESSION="${CLAUDE_SESSION_ID:-unknown}"
|
|
52
|
+
if [[ -f "$PLAN_MARKER" ]]; then
|
|
53
|
+
MARKER_SESSION=$(cat "$PLAN_MARKER" 2>/dev/null || echo "")
|
|
54
|
+
if [[ "$MARKER_SESSION" == "$CURRENT_SESSION" ]]; then
|
|
55
|
+
exit 0 # Plan was presented this session, allow edits
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Count edits this session (without a plan marker)
|
|
60
|
+
EDIT_COUNT=0
|
|
61
|
+
if [[ -f "$EDIT_COUNTER" ]]; then
|
|
62
|
+
STORED=$(cat "$EDIT_COUNTER" 2>/dev/null || echo "0:unknown")
|
|
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
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
EDIT_COUNT=$((EDIT_COUNT + 1))
|
|
70
|
+
echo "${EDIT_COUNT}:${CURRENT_SESSION}" > "$EDIT_COUNTER"
|
|
71
|
+
|
|
72
|
+
# First 2 edits get a warning injected as context (don't block — could be micro tasks)
|
|
73
|
+
# After 3+ edits without a plan marker, escalate the warning
|
|
74
|
+
if [[ $EDIT_COUNT -le 2 ]]; then
|
|
75
|
+
# Soft reminder — inject context but allow
|
|
76
|
+
cat <<'JSON'
|
|
77
|
+
{
|
|
78
|
+
"hookSpecificOutput": {
|
|
79
|
+
"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 by writing the current session ID to ~/.claude/ftm-state/.plan-presented after confirming the task is genuinely small. To create the marker: Write tool → ~/.claude/ftm-state/.plan-presented with content being the session ID."
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
JSON
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# 3+ edits without a plan — stronger warning
|
|
88
|
+
cat <<'JSON'
|
|
89
|
+
{
|
|
90
|
+
"hookSpecificOutput": {
|
|
91
|
+
"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 the session ID 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
|
+
}
|
|
94
|
+
}
|
|
95
|
+
JSON
|
|
96
|
+
exit 0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PostToolUse hook: detect git commits and inject ftm-map update instructions
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# Read stdin into variable
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
|
|
9
|
+
# Exit gracefully if stdin is empty or jq is unavailable
|
|
10
|
+
if [ -z "$INPUT" ]; then
|
|
11
|
+
echo "{}"
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if ! command -v jq &>/dev/null; then
|
|
16
|
+
echo "{}"
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Extract tool_name
|
|
21
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
22
|
+
|
|
23
|
+
if [ -z "$TOOL_NAME" ]; then
|
|
24
|
+
echo "{}"
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Check if this is a commit operation
|
|
29
|
+
IS_COMMIT=0
|
|
30
|
+
|
|
31
|
+
if [ "$TOOL_NAME" = "mcp__git__git_commit" ]; then
|
|
32
|
+
IS_COMMIT=1
|
|
33
|
+
elif [ "$TOOL_NAME" = "Bash" ]; then
|
|
34
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
35
|
+
if [ -n "$COMMAND" ] && echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
|
|
36
|
+
IS_COMMIT=1
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Exit quickly for non-commit operations
|
|
41
|
+
if [ "$IS_COMMIT" -eq 0 ]; then
|
|
42
|
+
echo "{}"
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Only trigger for mapped projects
|
|
47
|
+
if [ ! -f ".ftm-map/map.db" ]; then
|
|
48
|
+
echo "{}"
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Output additionalContext to trigger ftm-map update workflow
|
|
53
|
+
cat <<'EOF'
|
|
54
|
+
{
|
|
55
|
+
"additionalContext": "A commit was just made. Run ftm-map incremental on changed files, then update INTENT.md and ARCHITECTURE.mmd via ftm-intent and ftm-diagram."
|
|
56
|
+
}
|
|
57
|
+
EOF
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"_comment": "FTM hooks configuration template. Merge these entries into your ~/.claude/settings.json under the 'hooks' key. install.sh can do this automatically with --setup-hooks.",
|
|
4
|
+
"hooks": {
|
|
5
|
+
"PreToolUse": [
|
|
6
|
+
{
|
|
7
|
+
"matcher": "Edit|Write",
|
|
8
|
+
"hooks": [
|
|
9
|
+
{
|
|
10
|
+
"type": "command",
|
|
11
|
+
"command": "~/.claude/hooks/ftm-plan-gate.sh",
|
|
12
|
+
"timeout": 5
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"matcher": "mcp__slack__slack_post_message|mcp__slack__slack_reply_to_thread|mcp__gmail__send_email",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "~/.claude/hooks/ftm-drafts-gate.sh",
|
|
22
|
+
"timeout": 5
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"UserPromptSubmit": [
|
|
28
|
+
{
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "~/.claude/hooks/ftm-discovery-reminder.sh",
|
|
33
|
+
"timeout": 5
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"type": "command",
|
|
37
|
+
"command": "~/.claude/hooks/ftm-pending-sync-check.sh",
|
|
38
|
+
"timeout": 5
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "~/.claude/hooks/ftm-map-autodetect.sh",
|
|
43
|
+
"timeout": 5
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"PostToolUse": [
|
|
49
|
+
{
|
|
50
|
+
"matcher": "",
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "node ~/.claude/hooks/ftm-event-logger.mjs"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"matcher": "Bash|mcp__git__git_commit",
|
|
60
|
+
"hooks": [
|
|
61
|
+
{
|
|
62
|
+
"type": "command",
|
|
63
|
+
"command": "~/.claude/hooks/ftm-post-commit-trigger.sh",
|
|
64
|
+
"timeout": 5
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"Stop": [
|
|
70
|
+
{
|
|
71
|
+
"hooks": [
|
|
72
|
+
{
|
|
73
|
+
"type": "command",
|
|
74
|
+
"command": "~/.claude/hooks/ftm-blackboard-enforcer.sh",
|
|
75
|
+
"timeout": 5
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
package/install.sh
CHANGED
|
@@ -4,21 +4,39 @@ set -euo pipefail
|
|
|
4
4
|
# FTM Skills Installer
|
|
5
5
|
# Creates symlinks from this repo into ~/.claude/skills/ so slash commands work.
|
|
6
6
|
# Safe to re-run — idempotent. Run after cloning or adding new skills.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ./install.sh # Install skills, hooks, and state templates
|
|
10
|
+
# ./install.sh --setup-hooks # Also merge hook config into settings.json
|
|
7
11
|
|
|
8
12
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
13
|
SKILLS_DIR="$HOME/.claude/skills"
|
|
10
14
|
STATE_DIR="$HOME/.claude/ftm-state"
|
|
11
15
|
CONFIG_DIR="$HOME/.claude"
|
|
16
|
+
HOOKS_DIR="$HOME/.claude/hooks"
|
|
17
|
+
SETTINGS_FILE="$CONFIG_DIR/settings.json"
|
|
18
|
+
|
|
19
|
+
SETUP_HOOKS=false
|
|
20
|
+
for arg in "$@"; do
|
|
21
|
+
case "$arg" in
|
|
22
|
+
--setup-hooks) SETUP_HOOKS=true ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
12
25
|
|
|
13
|
-
echo "Installing
|
|
26
|
+
echo "Installing FTM skills from: $REPO_DIR"
|
|
14
27
|
echo "Linking into: $SKILLS_DIR"
|
|
15
28
|
echo ""
|
|
16
29
|
|
|
17
30
|
mkdir -p "$SKILLS_DIR"
|
|
18
31
|
|
|
32
|
+
# --- Skills ---
|
|
33
|
+
|
|
19
34
|
# Link all ftm*.yml files
|
|
20
35
|
for yml in "$REPO_DIR"/ftm*.yml; do
|
|
36
|
+
[ -f "$yml" ] || continue
|
|
21
37
|
name=$(basename "$yml")
|
|
38
|
+
# Skip ftm-config.default.yml — it's a template, not a skill
|
|
39
|
+
[[ "$name" == *".default."* ]] && continue
|
|
22
40
|
target="$SKILLS_DIR/$name"
|
|
23
41
|
if [ -L "$target" ]; then
|
|
24
42
|
rm "$target"
|
|
@@ -32,6 +50,7 @@ done
|
|
|
32
50
|
|
|
33
51
|
# Link all ftm* directories (skills with SKILL.md)
|
|
34
52
|
for dir in "$REPO_DIR"/ftm*/; do
|
|
53
|
+
[ -d "$dir" ] || continue
|
|
35
54
|
name=$(basename "$dir")
|
|
36
55
|
[ "$name" = "ftm-state" ] && continue # state is handled separately
|
|
37
56
|
target="$SKILLS_DIR/$name"
|
|
@@ -45,10 +64,17 @@ for dir in "$REPO_DIR"/ftm*/; do
|
|
|
45
64
|
echo " LINK $name/"
|
|
46
65
|
done
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
SKILL_COUNT=$(ls "$REPO_DIR"/ftm*.yml 2>/dev/null | grep -v '.default.' | wc -l | tr -d ' ')
|
|
68
|
+
echo ""
|
|
69
|
+
echo " $SKILL_COUNT skills linked."
|
|
70
|
+
|
|
71
|
+
# --- Blackboard State ---
|
|
72
|
+
|
|
49
73
|
if [ -d "$REPO_DIR/ftm-state" ]; then
|
|
74
|
+
echo ""
|
|
50
75
|
mkdir -p "$STATE_DIR/blackboard/experiences"
|
|
51
76
|
for f in "$REPO_DIR/ftm-state/blackboard"/*.json; do
|
|
77
|
+
[ -f "$f" ] || continue
|
|
52
78
|
name=$(basename "$f")
|
|
53
79
|
target="$STATE_DIR/blackboard/$name"
|
|
54
80
|
if [ ! -f "$target" ]; then
|
|
@@ -63,23 +89,28 @@ if [ -d "$REPO_DIR/ftm-state" ]; then
|
|
|
63
89
|
fi
|
|
64
90
|
fi
|
|
65
91
|
|
|
66
|
-
#
|
|
92
|
+
# --- Config ---
|
|
93
|
+
|
|
67
94
|
if [ ! -f "$CONFIG_DIR/ftm-config.yml" ] && [ -f "$REPO_DIR/ftm-config.default.yml" ]; then
|
|
68
95
|
cp "$REPO_DIR/ftm-config.default.yml" "$CONFIG_DIR/ftm-config.yml"
|
|
69
96
|
echo " INIT ftm-config.yml (from default template)"
|
|
70
97
|
fi
|
|
71
98
|
|
|
72
|
-
#
|
|
73
|
-
|
|
99
|
+
# --- Hooks ---
|
|
100
|
+
|
|
101
|
+
echo ""
|
|
102
|
+
echo "Installing hooks..."
|
|
103
|
+
|
|
74
104
|
if [ -d "$REPO_DIR/hooks" ]; then
|
|
75
105
|
mkdir -p "$HOOKS_DIR"
|
|
76
106
|
HOOK_COUNT=0
|
|
107
|
+
|
|
108
|
+
# Install shell hooks
|
|
77
109
|
for hook in "$REPO_DIR/hooks"/ftm-*.sh; do
|
|
78
110
|
[ -f "$hook" ] || continue
|
|
79
111
|
name=$(basename "$hook")
|
|
80
112
|
target="$HOOKS_DIR/$name"
|
|
81
113
|
if [ -f "$target" ]; then
|
|
82
|
-
# Overwrite — hooks should always be the latest version
|
|
83
114
|
cp "$hook" "$target"
|
|
84
115
|
chmod +x "$target"
|
|
85
116
|
echo " UPDATE $name"
|
|
@@ -90,13 +121,111 @@ if [ -d "$REPO_DIR/hooks" ]; then
|
|
|
90
121
|
fi
|
|
91
122
|
HOOK_COUNT=$((HOOK_COUNT + 1))
|
|
92
123
|
done
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
139
|
+
|
|
140
|
+
echo ""
|
|
141
|
+
echo " $HOOK_COUNT hooks installed to $HOOKS_DIR"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# --- Hook Config Merge (--setup-hooks) ---
|
|
145
|
+
|
|
146
|
+
if [ "$SETUP_HOOKS" = true ]; then
|
|
147
|
+
echo ""
|
|
148
|
+
echo "Setting up hook configuration in settings.json..."
|
|
149
|
+
|
|
150
|
+
TEMPLATE="$REPO_DIR/hooks/settings-template.json"
|
|
151
|
+
if [ ! -f "$TEMPLATE" ]; then
|
|
152
|
+
echo " ERROR: hooks/settings-template.json not found"
|
|
153
|
+
exit 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if ! command -v jq &>/dev/null; then
|
|
157
|
+
echo " ERROR: jq is required for --setup-hooks. Install with: brew install jq"
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# Expand ~ to $HOME in the template (jq doesn't expand shell paths)
|
|
162
|
+
EXPANDED_TEMPLATE=$(sed "s|~/.claude|$HOME/.claude|g" "$TEMPLATE")
|
|
163
|
+
|
|
164
|
+
if [ ! -f "$SETTINGS_FILE" ]; then
|
|
165
|
+
# No settings.json — create one from the template hooks section
|
|
166
|
+
echo "$EXPANDED_TEMPLATE" | jq '{hooks: .hooks}' > "$SETTINGS_FILE"
|
|
167
|
+
echo " CREATED $SETTINGS_FILE with FTM hooks"
|
|
168
|
+
else
|
|
169
|
+
# Merge FTM hooks into existing settings.json
|
|
170
|
+
# Strategy: for each hook event type, append FTM entries that don't already exist
|
|
171
|
+
BACKUP="$SETTINGS_FILE.ftm-backup-$(date +%Y%m%d%H%M%S)"
|
|
172
|
+
cp "$SETTINGS_FILE" "$BACKUP"
|
|
173
|
+
echo " BACKUP $BACKUP"
|
|
174
|
+
|
|
175
|
+
# Extract the hooks section from the template
|
|
176
|
+
TEMPLATE_HOOKS=$(echo "$EXPANDED_TEMPLATE" | jq '.hooks')
|
|
177
|
+
|
|
178
|
+
# Read existing settings
|
|
179
|
+
EXISTING=$(cat "$SETTINGS_FILE")
|
|
180
|
+
|
|
181
|
+
# Ensure hooks key exists
|
|
182
|
+
if echo "$EXISTING" | jq -e '.hooks' >/dev/null 2>&1; then
|
|
183
|
+
: # hooks key exists
|
|
184
|
+
else
|
|
185
|
+
EXISTING=$(echo "$EXISTING" | jq '. + {hooks: {}}')
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# Merge each hook event type
|
|
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] // []')
|
|
192
|
+
|
|
193
|
+
# Check if any FTM hooks are already present (by checking command paths)
|
|
194
|
+
FTM_COMMANDS=$(echo "$TEMPLATE_ENTRIES" | jq -r '.[].hooks[]?.command // empty' 2>/dev/null)
|
|
195
|
+
ALREADY_PRESENT=false
|
|
196
|
+
|
|
197
|
+
for cmd in $FTM_COMMANDS; do
|
|
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
|
|
204
|
+
|
|
205
|
+
if [ "$ALREADY_PRESENT" = true ]; then
|
|
206
|
+
echo " SKIP $EVENT hooks (already configured)"
|
|
207
|
+
continue
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# Append template entries to existing
|
|
211
|
+
MERGED=$(jq -n --argjson existing "$EXISTING_ENTRIES" --argjson template "$TEMPLATE_ENTRIES" '$existing + $template')
|
|
212
|
+
EXISTING=$(echo "$EXISTING" | jq --arg e "$EVENT" --argjson m "$MERGED" '.hooks[$e] = $m')
|
|
213
|
+
echo " MERGE $EVENT hooks"
|
|
214
|
+
done
|
|
215
|
+
|
|
216
|
+
echo "$EXISTING" | jq '.' > "$SETTINGS_FILE"
|
|
217
|
+
echo " UPDATED $SETTINGS_FILE"
|
|
97
218
|
fi
|
|
219
|
+
|
|
220
|
+
echo ""
|
|
221
|
+
echo " Hooks are now active. See docs/HOOKS.md for details."
|
|
222
|
+
else
|
|
223
|
+
echo ""
|
|
224
|
+
echo " To activate hooks, run: ./install.sh --setup-hooks"
|
|
225
|
+
echo " Or manually add entries from hooks/settings-template.json to ~/.claude/settings.json"
|
|
226
|
+
echo " See docs/HOOKS.md for details."
|
|
98
227
|
fi
|
|
99
228
|
|
|
100
229
|
echo ""
|
|
101
|
-
echo "Done. $
|
|
230
|
+
echo "Done. $SKILL_COUNT skills, $HOOK_COUNT hooks."
|
|
102
231
|
echo "Try: /ftm help"
|