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.
Files changed (136) hide show
  1. package/bin/generate-manifest.mjs +253 -0
  2. package/bin/install.mjs +134 -4
  3. package/docs/HOOKS.md +243 -0
  4. package/docs/INBOX.md +233 -0
  5. package/ftm/SKILL.md +34 -0
  6. package/ftm-audit/SKILL.md +69 -0
  7. package/ftm-brainstorm/SKILL.md +51 -0
  8. package/ftm-browse/SKILL.md +39 -0
  9. package/ftm-capture/SKILL.md +370 -0
  10. package/ftm-capture.yml +4 -0
  11. package/ftm-codex-gate/SKILL.md +59 -0
  12. package/ftm-config/SKILL.md +35 -0
  13. package/ftm-council/SKILL.md +56 -0
  14. package/ftm-dashboard/SKILL.md +163 -0
  15. package/ftm-debug/SKILL.md +84 -0
  16. package/ftm-diagram/SKILL.md +44 -0
  17. package/ftm-executor/SKILL.md +97 -0
  18. package/ftm-git/SKILL.md +60 -0
  19. package/ftm-inbox/backend/__init__.py +0 -0
  20. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  21. package/ftm-inbox/backend/adapters/__init__.py +0 -0
  22. package/ftm-inbox/backend/adapters/_retry.py +64 -0
  23. package/ftm-inbox/backend/adapters/base.py +230 -0
  24. package/ftm-inbox/backend/adapters/freshservice.py +104 -0
  25. package/ftm-inbox/backend/adapters/gmail.py +125 -0
  26. package/ftm-inbox/backend/adapters/jira.py +136 -0
  27. package/ftm-inbox/backend/adapters/registry.py +192 -0
  28. package/ftm-inbox/backend/adapters/slack.py +110 -0
  29. package/ftm-inbox/backend/db/__init__.py +0 -0
  30. package/ftm-inbox/backend/db/connection.py +54 -0
  31. package/ftm-inbox/backend/db/schema.py +78 -0
  32. package/ftm-inbox/backend/executor/__init__.py +7 -0
  33. package/ftm-inbox/backend/executor/engine.py +149 -0
  34. package/ftm-inbox/backend/executor/step_runner.py +98 -0
  35. package/ftm-inbox/backend/main.py +103 -0
  36. package/ftm-inbox/backend/models/__init__.py +1 -0
  37. package/ftm-inbox/backend/models/unified_task.py +36 -0
  38. package/ftm-inbox/backend/planner/__init__.py +6 -0
  39. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  40. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  41. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  42. package/ftm-inbox/backend/planner/generator.py +127 -0
  43. package/ftm-inbox/backend/planner/schema.py +34 -0
  44. package/ftm-inbox/backend/requirements.txt +5 -0
  45. package/ftm-inbox/backend/routes/__init__.py +0 -0
  46. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  47. package/ftm-inbox/backend/routes/execute.py +186 -0
  48. package/ftm-inbox/backend/routes/health.py +52 -0
  49. package/ftm-inbox/backend/routes/inbox.py +68 -0
  50. package/ftm-inbox/backend/routes/plan.py +271 -0
  51. package/ftm-inbox/bin/launchagent.mjs +91 -0
  52. package/ftm-inbox/bin/setup.mjs +188 -0
  53. package/ftm-inbox/bin/start.sh +10 -0
  54. package/ftm-inbox/bin/status.sh +17 -0
  55. package/ftm-inbox/bin/stop.sh +8 -0
  56. package/ftm-inbox/config.example.yml +55 -0
  57. package/ftm-inbox/package-lock.json +2898 -0
  58. package/ftm-inbox/package.json +26 -0
  59. package/ftm-inbox/postcss.config.js +6 -0
  60. package/ftm-inbox/src/app.css +199 -0
  61. package/ftm-inbox/src/app.html +18 -0
  62. package/ftm-inbox/src/lib/api.ts +166 -0
  63. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
  64. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
  65. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
  66. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
  67. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
  68. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
  69. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
  70. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
  71. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
  72. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
  73. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
  74. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
  75. package/ftm-inbox/src/lib/theme.ts +47 -0
  76. package/ftm-inbox/src/routes/+layout.svelte +76 -0
  77. package/ftm-inbox/src/routes/+page.svelte +401 -0
  78. package/ftm-inbox/static/favicon.png +0 -0
  79. package/ftm-inbox/svelte.config.js +12 -0
  80. package/ftm-inbox/tailwind.config.ts +63 -0
  81. package/ftm-inbox/tsconfig.json +13 -0
  82. package/ftm-inbox/vite.config.ts +6 -0
  83. package/ftm-intent/SKILL.md +44 -0
  84. package/ftm-manifest.json +3794 -0
  85. package/ftm-map/SKILL.md +259 -0
  86. package/ftm-map/scripts/db.py +391 -0
  87. package/ftm-map/scripts/index.py +341 -0
  88. package/ftm-map/scripts/parser.py +455 -0
  89. package/ftm-map/scripts/queries/.gitkeep +0 -0
  90. package/ftm-map/scripts/queries/javascript-tags.scm +23 -0
  91. package/ftm-map/scripts/queries/python-tags.scm +17 -0
  92. package/ftm-map/scripts/queries/typescript-tags.scm +29 -0
  93. package/ftm-map/scripts/query.py +149 -0
  94. package/ftm-map/scripts/requirements.txt +2 -0
  95. package/ftm-map/scripts/setup-hooks.sh +27 -0
  96. package/ftm-map/scripts/setup.sh +45 -0
  97. package/ftm-map/scripts/test_db.py +124 -0
  98. package/ftm-map/scripts/test_parser.py +106 -0
  99. package/ftm-map/scripts/test_query.py +66 -0
  100. package/ftm-map/scripts/tests/fixtures/__init__.py +0 -0
  101. package/ftm-map/scripts/tests/fixtures/sample_project/api.ts +16 -0
  102. package/ftm-map/scripts/tests/fixtures/sample_project/auth.py +15 -0
  103. package/ftm-map/scripts/tests/fixtures/sample_project/utils.js +16 -0
  104. package/ftm-map/scripts/views.py +545 -0
  105. package/ftm-mind/SKILL.md +173 -66
  106. package/ftm-pause/SKILL.md +43 -0
  107. package/ftm-researcher/SKILL.md +275 -0
  108. package/ftm-researcher/evals/agent-diversity.yaml +17 -0
  109. package/ftm-researcher/evals/synthesis-quality.yaml +12 -0
  110. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -0
  111. package/ftm-researcher/references/adaptive-search.md +116 -0
  112. package/ftm-researcher/references/agent-prompts.md +193 -0
  113. package/ftm-researcher/references/council-integration.md +193 -0
  114. package/ftm-researcher/references/output-format.md +203 -0
  115. package/ftm-researcher/references/synthesis-pipeline.md +165 -0
  116. package/ftm-researcher/scripts/score_credibility.py +234 -0
  117. package/ftm-researcher/scripts/validate_research.py +92 -0
  118. package/ftm-resume/SKILL.md +47 -0
  119. package/ftm-retro/SKILL.md +54 -0
  120. package/ftm-routine/SKILL.md +170 -0
  121. package/ftm-state/blackboard/capabilities.json +5 -0
  122. package/ftm-state/blackboard/capabilities.schema.json +27 -0
  123. package/ftm-upgrade/SKILL.md +41 -0
  124. package/ftm-upgrade/scripts/check-version.sh +1 -1
  125. package/ftm-upgrade/scripts/upgrade.sh +1 -1
  126. package/hooks/ftm-blackboard-enforcer.sh +94 -0
  127. package/hooks/ftm-discovery-reminder.sh +90 -0
  128. package/hooks/ftm-drafts-gate.sh +61 -0
  129. package/hooks/ftm-event-logger.mjs +107 -0
  130. package/hooks/ftm-map-autodetect.sh +79 -0
  131. package/hooks/ftm-pending-sync-check.sh +22 -0
  132. package/hooks/ftm-plan-gate.sh +96 -0
  133. package/hooks/ftm-post-commit-trigger.sh +57 -0
  134. package/hooks/settings-template.json +81 -0
  135. package/install.sh +140 -11
  136. 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 ftm skills from: $REPO_DIR"
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
- # Set up blackboard state (copy templates, don't overwrite existing data)
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
- # Copy default config if none exists
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
- # Install hooks (copy to ~/.claude/hooks/, don't overwrite existing)
73
- HOOKS_DIR="$HOME/.claude/hooks"
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
- if [ "$HOOK_COUNT" -gt 0 ]; then
94
- echo ""
95
- echo " $HOOK_COUNT hooks installed to $HOOKS_DIR"
96
- echo " To activate, add them to ~/.claude/settings.json (see docs/HOOKS.md)"
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. $(ls "$REPO_DIR"/ftm*.yml 2>/dev/null | wc -l | tr -d ' ') skills linked."
230
+ echo "Done. $SKILL_COUNT skills, $HOOK_COUNT hooks."
102
231
  echo "Try: /ftm help"