feed-the-machine 1.0.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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/bin/generate-manifest.mjs +210 -0
  4. package/bin/install.mjs +114 -0
  5. package/ftm/SKILL.md +88 -0
  6. package/ftm-audit/SKILL.md +146 -0
  7. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -0
  8. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -0
  9. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -0
  10. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -0
  11. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -0
  12. package/ftm-audit/scripts/run-knip.sh +23 -0
  13. package/ftm-audit.yml +2 -0
  14. package/ftm-brainstorm/SKILL.md +379 -0
  15. package/ftm-brainstorm/evals/evals.json +100 -0
  16. package/ftm-brainstorm/evals/promptfoo.yaml +109 -0
  17. package/ftm-brainstorm/references/agent-prompts.md +224 -0
  18. package/ftm-brainstorm/references/plan-template.md +121 -0
  19. package/ftm-brainstorm.yml +2 -0
  20. package/ftm-browse/SKILL.md +415 -0
  21. package/ftm-browse/daemon/browser-manager.ts +206 -0
  22. package/ftm-browse/daemon/bun.lock +30 -0
  23. package/ftm-browse/daemon/cli.ts +347 -0
  24. package/ftm-browse/daemon/commands.ts +410 -0
  25. package/ftm-browse/daemon/main.ts +357 -0
  26. package/ftm-browse/daemon/package.json +17 -0
  27. package/ftm-browse/daemon/server.ts +189 -0
  28. package/ftm-browse/daemon/snapshot.ts +519 -0
  29. package/ftm-browse/daemon/tsconfig.json +22 -0
  30. package/ftm-browse.yml +4 -0
  31. package/ftm-codex-gate/SKILL.md +302 -0
  32. package/ftm-codex-gate.yml +2 -0
  33. package/ftm-config/SKILL.md +310 -0
  34. package/ftm-config.default.yml +80 -0
  35. package/ftm-config.yml +2 -0
  36. package/ftm-council/SKILL.md +132 -0
  37. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -0
  38. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -0
  39. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -0
  40. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -0
  41. package/ftm-council/references/protocols/PREREQUISITES.md +47 -0
  42. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -0
  43. package/ftm-council.yml +2 -0
  44. package/ftm-dashboard.yml +4 -0
  45. package/ftm-debug/SKILL.md +146 -0
  46. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -0
  47. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -0
  48. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -0
  49. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -0
  50. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -0
  51. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -0
  52. package/ftm-debug.yml +2 -0
  53. package/ftm-diagram/SKILL.md +233 -0
  54. package/ftm-diagram.yml +2 -0
  55. package/ftm-executor/SKILL.md +657 -0
  56. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -0
  57. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -0
  58. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -0
  59. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -0
  60. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -0
  61. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -0
  62. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -0
  63. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -0
  64. package/ftm-executor/references/protocols/MODEL-PROFILE.md +44 -0
  65. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -0
  66. package/ftm-executor/runtime/ftm-runtime.mjs +252 -0
  67. package/ftm-executor/runtime/package.json +8 -0
  68. package/ftm-executor.yml +2 -0
  69. package/ftm-git/SKILL.md +195 -0
  70. package/ftm-git/evals/evals.json +26 -0
  71. package/ftm-git/evals/promptfoo.yaml +75 -0
  72. package/ftm-git/hooks/post-commit-experience.sh +92 -0
  73. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -0
  74. package/ftm-git/references/protocols/REMEDIATION.md +139 -0
  75. package/ftm-git/scripts/pre-commit-secrets.sh +110 -0
  76. package/ftm-git.yml +2 -0
  77. package/ftm-intent/SKILL.md +198 -0
  78. package/ftm-intent.yml +2 -0
  79. package/ftm-map.yml +2 -0
  80. package/ftm-mind/SKILL.md +986 -0
  81. package/ftm-mind/evals/promptfoo.yaml +142 -0
  82. package/ftm-mind/references/blackboard-schema.md +328 -0
  83. package/ftm-mind/references/complexity-guide.md +110 -0
  84. package/ftm-mind/references/event-registry.md +299 -0
  85. package/ftm-mind/references/mcp-inventory.md +296 -0
  86. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -0
  87. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -0
  88. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -0
  89. package/ftm-mind/references/reflexion-protocol.md +249 -0
  90. package/ftm-mind/references/routing/SCENARIOS.md +22 -0
  91. package/ftm-mind/references/routing-scenarios.md +35 -0
  92. package/ftm-mind.yml +2 -0
  93. package/ftm-pause/SKILL.md +133 -0
  94. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -0
  95. package/ftm-pause/references/protocols/VALIDATION.md +80 -0
  96. package/ftm-pause.yml +2 -0
  97. package/ftm-researcher.yml +2 -0
  98. package/ftm-resume/SKILL.md +166 -0
  99. package/ftm-resume/references/protocols/VALIDATION.md +172 -0
  100. package/ftm-resume.yml +2 -0
  101. package/ftm-retro/SKILL.md +189 -0
  102. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -0
  103. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -0
  104. package/ftm-retro.yml +2 -0
  105. package/ftm-routine.yml +4 -0
  106. package/ftm-state/blackboard/context.json +23 -0
  107. package/ftm-state/blackboard/experiences/index.json +9 -0
  108. package/ftm-state/blackboard/patterns.json +6 -0
  109. package/ftm-state/schemas/context.schema.json +130 -0
  110. package/ftm-state/schemas/experience-index.schema.json +77 -0
  111. package/ftm-state/schemas/experience.schema.json +78 -0
  112. package/ftm-state/schemas/patterns.schema.json +44 -0
  113. package/ftm-upgrade/SKILL.md +153 -0
  114. package/ftm-upgrade/scripts/check-version.sh +76 -0
  115. package/ftm-upgrade/scripts/upgrade.sh +143 -0
  116. package/ftm-upgrade.yml +2 -0
  117. package/ftm.yml +2 -0
  118. package/install.sh +102 -0
  119. package/package.json +74 -0
  120. package/uninstall.sh +25 -0
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { createHash } from 'crypto';
5
+ import { homedir } from 'os';
6
+
7
+ const STATE_DIR = resolve(homedir(), '.claude', 'ftm-state');
8
+ const STATE_FILE = resolve(STATE_DIR, 'runtime-state.json');
9
+
10
+ // --- Parsing ---
11
+
12
+ export function parsePlan(markdown) {
13
+ const tasks = [];
14
+ const blocks = markdown.split(/(?=^### Task \d+:)/m).filter(b => b.trim());
15
+
16
+ for (const block of blocks) {
17
+ const idMatch = block.match(/^### Task (\d+):\s*(.+)/m);
18
+ if (!idMatch) continue;
19
+
20
+ const id = parseInt(idMatch[1], 10);
21
+ const title = idMatch[2].trim();
22
+
23
+ const get = (field) => {
24
+ const m = block.match(new RegExp(`\\*\\*${field}:\\*\\*\\s*(.+)`));
25
+ return m ? m[1].trim() : '';
26
+ };
27
+ const splitList = (raw) => (!raw || raw.toLowerCase() === 'none')
28
+ ? [] : raw.split(',').map(s => s.trim()).filter(Boolean);
29
+
30
+ const rawDeps = get('Dependencies');
31
+ const dependencies = splitList(rawDeps).map(d => {
32
+ const m = d.match(/Task\s+(\d+)/i);
33
+ return m ? parseInt(m[1], 10) : null;
34
+ }).filter(Boolean);
35
+
36
+ const files = splitList(get('Files'));
37
+
38
+ const criteriaMatches = [...block.matchAll(/^-\s*\[[ x]\]\s*(.+)/gm)];
39
+ const acceptance_criteria = criteriaMatches.map(m => m[1].trim());
40
+
41
+ tasks.push({
42
+ id,
43
+ title,
44
+ description: get('Description'),
45
+ files,
46
+ dependencies,
47
+ agent_type: get('Agent type'),
48
+ acceptance_criteria,
49
+ });
50
+ }
51
+
52
+ tasks.sort((a, b) => a.id - b.id);
53
+ return tasks;
54
+ }
55
+
56
+ // --- Wave grouping ---
57
+
58
+ export function computeWaves(tasks) {
59
+ const allIds = new Set(tasks.map(t => t.id));
60
+
61
+ for (const task of tasks) {
62
+ for (const dep of task.dependencies) {
63
+ if (!allIds.has(dep)) {
64
+ throw new Error(`Task ${task.id} references unknown dependency Task ${dep}`);
65
+ }
66
+ }
67
+ }
68
+
69
+ const remaining = new Set(tasks.map(t => t.id));
70
+ const completed = new Set();
71
+ const waves = [];
72
+ const taskMap = Object.fromEntries(tasks.map(t => [t.id, t]));
73
+
74
+ while (remaining.size > 0) {
75
+ const wave = [...remaining].filter(id =>
76
+ taskMap[id].dependencies.every(dep => completed.has(dep))
77
+ );
78
+
79
+ if (wave.length === 0) {
80
+ const cycle = [...remaining].join(', ');
81
+ throw new Error(`Circular dependency detected among tasks: ${cycle}`);
82
+ }
83
+
84
+ waves.push(wave.sort((a, b) => a - b));
85
+ for (const id of wave) {
86
+ completed.add(id);
87
+ remaining.delete(id);
88
+ }
89
+ }
90
+
91
+ return waves;
92
+ }
93
+
94
+ // --- State I/O ---
95
+
96
+ function readState() {
97
+ if (!existsSync(STATE_FILE)) return null;
98
+ return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
99
+ }
100
+
101
+ function requireState() {
102
+ const s = readState();
103
+ if (!s) { console.error('Error: No active plan. Run plan-index first.'); process.exit(1); }
104
+ return s;
105
+ }
106
+
107
+ function writeState(state) {
108
+ mkdirSync(STATE_DIR, { recursive: true });
109
+ state.updated_at = new Date().toISOString();
110
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
111
+ }
112
+
113
+ function hashContent(content) {
114
+ return createHash('sha256').update(content).digest('hex');
115
+ }
116
+
117
+ // --- Commands ---
118
+
119
+ function cmdPlanIndex(planPath) {
120
+ const absPath = resolve(planPath);
121
+ if (!existsSync(absPath)) {
122
+ console.error(`Error: Plan file not found: ${absPath}`);
123
+ process.exit(1);
124
+ }
125
+
126
+ const content = readFileSync(absPath, 'utf8');
127
+ const tasks = parsePlan(content);
128
+ const waves = computeWaves(tasks);
129
+
130
+ const taskState = {};
131
+ for (const task of tasks) {
132
+ taskState[task.id] = { ...task, status: 'pending', completed_at: null };
133
+ }
134
+
135
+ const state = {
136
+ plan_path: absPath,
137
+ plan_hash: hashContent(content),
138
+ tasks: taskState,
139
+ waves,
140
+ current_wave: 0,
141
+ started_at: new Date().toISOString(),
142
+ updated_at: new Date().toISOString(),
143
+ };
144
+
145
+ writeState(state);
146
+
147
+ console.log(JSON.stringify({
148
+ tasks,
149
+ waves,
150
+ total_tasks: tasks.length,
151
+ total_waves: waves.length,
152
+ }, null, 2));
153
+ }
154
+
155
+ function cmdNextWave() {
156
+ const state = requireState();
157
+ const done = { wave_number: null, tasks: [], remaining_waves: 0, complete: true };
158
+
159
+ for (let i = 0; i < state.waves.length; i++) {
160
+ const pending = state.waves[i].filter(id => state.tasks[id]?.status !== 'completed');
161
+ if (pending.length > 0) {
162
+ const remaining_waves = state.waves.slice(i + 1)
163
+ .filter(w => w.some(id => state.tasks[id]?.status !== 'completed')).length;
164
+ console.log(JSON.stringify({ wave_number: i + 1, tasks: pending.map(id => state.tasks[id]), remaining_waves }, null, 2));
165
+ return;
166
+ }
167
+ }
168
+ console.log(JSON.stringify(done, null, 2));
169
+ }
170
+
171
+ function cmdMarkComplete(taskIdArg) {
172
+ const taskId = parseInt(taskIdArg, 10);
173
+ if (isNaN(taskId)) { console.error(`Error: Invalid task ID: ${taskIdArg}`); process.exit(1); }
174
+
175
+ const state = requireState();
176
+ if (!state.tasks[taskId]) {
177
+ console.error(`Error: Task ${taskId} not found in current plan.`); process.exit(1);
178
+ }
179
+
180
+ state.tasks[taskId].status = 'completed';
181
+ state.tasks[taskId].completed_at = new Date().toISOString();
182
+
183
+ const waveIdx = state.waves.findIndex(w => w.includes(taskId));
184
+ const wave = waveIdx >= 0 ? state.waves[waveIdx] : [];
185
+ const waveCompleted = wave.filter(id => state.tasks[id]?.status === 'completed').length;
186
+ const totalCompleted = Object.values(state.tasks).filter(t => t.status === 'completed').length;
187
+
188
+ writeState(state);
189
+
190
+ console.log(JSON.stringify({
191
+ task_id: taskId,
192
+ status: 'completed',
193
+ wave_progress: `${waveCompleted}/${wave.length}`,
194
+ plan_progress: `${totalCompleted}/${Object.keys(state.tasks).length}`,
195
+ }, null, 2));
196
+ }
197
+
198
+ function cmdStatus() {
199
+ const state = requireState();
200
+
201
+ const tasks = Object.values(state.tasks);
202
+ const completed = tasks.filter(t => t.status === 'completed').length;
203
+ const pending = tasks.filter(t => t.status === 'pending').length;
204
+
205
+ let current_wave = null;
206
+ let waves_remaining = 0;
207
+
208
+ for (let i = 0; i < state.waves.length; i++) {
209
+ const wave = state.waves[i];
210
+ const hasPending = wave.some(id => state.tasks[id]?.status !== 'completed');
211
+ if (hasPending) {
212
+ current_wave = i + 1;
213
+ waves_remaining = state.waves.slice(i).filter(w =>
214
+ w.some(id => state.tasks[id]?.status !== 'completed')
215
+ ).length;
216
+ break;
217
+ }
218
+ }
219
+
220
+ console.log(JSON.stringify({
221
+ total_tasks: tasks.length,
222
+ completed_tasks: completed,
223
+ current_wave,
224
+ waves_remaining,
225
+ tasks_by_status: { pending, completed },
226
+ }, null, 2));
227
+ }
228
+
229
+ // --- Entry point ---
230
+
231
+ const [,, command, ...args] = process.argv;
232
+
233
+ switch (command) {
234
+ case 'plan-index':
235
+ if (!args[0]) { console.error('Usage: ftm-runtime plan-index <plan-path>'); process.exit(1); }
236
+ cmdPlanIndex(args[0]);
237
+ break;
238
+ case 'next-wave':
239
+ cmdNextWave();
240
+ break;
241
+ case 'mark-complete':
242
+ if (!args[0]) { console.error('Usage: ftm-runtime mark-complete <task-id>'); process.exit(1); }
243
+ cmdMarkComplete(args[0]);
244
+ break;
245
+ case 'status':
246
+ cmdStatus();
247
+ break;
248
+ default:
249
+ console.error(`Unknown command: ${command}`);
250
+ console.error('Commands: plan-index, next-wave, mark-complete, status');
251
+ process.exit(1);
252
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "ftm-runtime",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "ftm-runtime": "./ftm-runtime.mjs"
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ name: ftm-executor
2
+ description: Autonomous plan execution engine. Takes any plan document and executes it end-to-end with a dynamically assembled agent team — analyzing tasks, creating purpose-built agents, dispatching them in parallel worktrees, and running each through a commit-review-fix loop until complete. Use this skill whenever the user wants to execute a plan, run a plan doc, launch an agent team on tasks, or says things like "execute this plan", "run this", "launch agents on this doc", "take this plan and go", or points to a plan file and wants it implemented autonomously. Even if they just paste a plan path and say "go" — this is the skill.
@@ -0,0 +1,195 @@
1
+ ---
2
+ name: ftm-git
3
+ description: Secret scanning and credential safety gate for git operations. Prevents API keys, tokens, passwords, and other secrets from ever being committed or pushed to remote repositories. Scans staged files, working tree, and git history for hardcoded credentials using regex pattern matching, then auto-remediates by extracting secrets to gitignored .env files and replacing hardcoded values with env var references. Use when user says "scan for secrets", "check for keys", "audit credentials", "ftm-git", "secret scan", "remove api keys", "check before push", or any time git commit/push operations are about to happen. Also auto-invoked by ftm-executor and ftm-mind before any commit or push operation. Even if the user just says "commit this" or "push to remote", this skill MUST run first. Do NOT use for general git workflow operations like branching or merging — that's git-workflow territory. This skill is specifically the security gate.
4
+ ---
5
+
6
+ ## Events
7
+
8
+ ### Emits
9
+ - `secrets_found` — when scan detects hardcoded credentials in staged files or working tree
10
+ - `secrets_clear` — when scan completes with no findings (safe to proceed with commit/push)
11
+ - `secrets_remediated` — when auto-fix successfully extracts secrets to .env and refactors source files
12
+ - `task_completed` — when full scan + remediation cycle finishes
13
+
14
+ ### Listens To
15
+ - `code_changed` — run a quick scan on modified files before they get staged
16
+ - `code_committed` — verify the commit doesn't contain secrets (post-commit safety net)
17
+
18
+ ## Blackboard Read
19
+
20
+ Before starting, load context from the blackboard:
21
+
22
+ 1. Read `~/.claude/ftm-state/blackboard/context.json` — check current_task, recent_decisions, active_constraints
23
+ 2. Read `~/.claude/ftm-state/blackboard/experiences/index.json` — filter entries by task_type="security" or tags matching "secrets", "credentials", "api-keys", or "git-safety"
24
+ 3. Load top 3-5 matching experience files for previously found secret patterns and effective remediation strategies
25
+ 4. Read `~/.claude/ftm-state/blackboard/patterns.json` — check recurring_issues for repeated secret leaks and execution_patterns for which files/directories tend to accumulate secrets
26
+
27
+ If index.json is empty or no matches found, proceed normally without experience-informed shortcuts.
28
+
29
+ # FTM Git — Secret Scanning & Credential Safety Gate
30
+
31
+ This skill exists because secrets pushed to GitHub are compromised the instant they hit the remote — even if you force-push a clean history seconds later. Bots scrape public repos continuously, and private repos are one permissions mistake away from exposure. The only safe secret is one that never enters git history.
32
+
33
+ This is not a nice-to-have audit. This is a hard gate. Nothing gets committed or pushed until this skill says it's clean.
34
+
35
+ ## Why This Matters
36
+
37
+ Yesterday we pushed API keys to the repo. That's the kind of mistake that leads to compromised accounts, unexpected bills, and emergency credential rotations. This skill makes it structurally impossible for that to happen again by scanning every file that's about to be committed and blocking the operation if secrets are present — then auto-fixing what it can.
38
+
39
+ ## Phase -1: Install Git Hook (First Invocation Only)
40
+
41
+ The first time ftm-git runs in a repo, install a pre-commit hook as a hard safety net. This hook runs independently of Claude — it's a shell script that blocks `git commit` if staged files contain Tier 1 secret patterns. Even if this skill is not invoked, or someone runs git directly from the terminal, the hook catches it.
42
+
43
+ **Check if the hook is already installed:**
44
+
45
+ ```bash
46
+ # Look for ftm-git marker in existing pre-commit hook
47
+ grep -q "ftm-git" .git/hooks/pre-commit 2>/dev/null
48
+ ```
49
+
50
+ **If not installed**, copy the hook script:
51
+
52
+ ```bash
53
+ cp ~/.claude/skills/ftm-git/scripts/pre-commit-secrets.sh .git/hooks/pre-commit
54
+ chmod +x .git/hooks/pre-commit
55
+ ```
56
+
57
+ **If a pre-commit hook already exists** (from husky, pre-commit framework, etc.), don't overwrite it. Instead, append the ftm-git scan to the end of the existing hook:
58
+
59
+ ```bash
60
+ echo "" >> .git/hooks/pre-commit
61
+ echo "# --- ftm-git secret scanner ---" >> .git/hooks/pre-commit
62
+ cat ~/.claude/skills/ftm-git/scripts/pre-commit-secrets.sh >> .git/hooks/pre-commit
63
+ ```
64
+
65
+ Tell the user: "Installed ftm-git pre-commit hook. Commits with hardcoded secrets will be blocked automatically, even outside of Claude."
66
+
67
+ This only needs to happen once per repo. On subsequent invocations, skip this phase.
68
+
69
+ ## Phase 0: Determine Scan Scope
70
+
71
+ Before scanning, figure out what needs scanning and why you were invoked.
72
+
73
+ **Invocation context determines scope:**
74
+
75
+ | Context | Scope |
76
+ |---|---|
77
+ | Pre-commit (explicit or auto-triggered) | Staged files (`git diff --cached --name-only`) + any files about to be staged |
78
+ | Pre-push | All commits not yet on remote (`git log @{upstream}..HEAD --name-only`) |
79
+ | Manual invocation ("scan for secrets") | Full working tree sweep |
80
+ | Post-commit safety net | The commit that just landed (`git diff-tree --no-commit-id -r HEAD`) |
81
+
82
+ **Always also check these regardless of invocation context:**
83
+ - Any `.env` file that is NOT in `.gitignore` — this is itself a finding
84
+ - Any file matching `*credentials*`, `*secret*`, `*token*` in the filename
85
+
86
+ ## Phase 1: Pattern Scan
87
+
88
+ Scan the in-scope files using regex patterns. The goal is zero false negatives — a few false positives are acceptable and will be filtered in Phase 2.
89
+
90
+ Read `references/patterns/SECRET-PATTERNS.md` for the full Tier 1 and Tier 2 pattern library, the false positive suppression list, severity classifications, and per-finding record format.
91
+
92
+ **Core Tier 1 patterns** (the most common — memorize these, consult the reference for the full set):
93
+
94
+ ```
95
+ AKIA[0-9A-Z]{16} # AWS Access Key ID
96
+ ghp_[A-Za-z0-9_]{36} # GitHub PAT (classic)
97
+ sk_live_[0-9a-zA-Z]{24,} # Stripe secret key (live)
98
+ AIza[0-9A-Za-z\-_]{35} # Google API key
99
+ xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24} # Slack bot token
100
+ -----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY----- # Private keys
101
+ ```
102
+
103
+ Run Tier 1 patterns in parallel since they're independent. For Tier 2, check surrounding context before confirming.
104
+
105
+ ## Phase 2: Validate Findings
106
+
107
+ For each Tier 2 match, read the surrounding context (5 lines before and after) and determine:
108
+
109
+ 1. **Is the value a real secret or a placeholder?** — Check against the ignore list in `references/patterns/SECRET-PATTERNS.md`.
110
+ 2. **Is it already using an env var?** — If the code does `key = os.environ.get("API_KEY", "sk_live_abc...")`, the hardcoded value is a fallback default. Still a finding — fallback defaults with real secrets are dangerous.
111
+ 3. **Is it in a file that should be gitignored?** — If the secret is in `.env` and `.env` is in `.gitignore`, it's fine. If `.env` is NOT in `.gitignore`, that's a separate finding.
112
+
113
+ After validation, produce a findings list sorted by severity (CRITICAL → HIGH → MEDIUM → LOW). See `references/patterns/SECRET-PATTERNS.md` for the severity table.
114
+
115
+ If zero findings after validation: emit `secrets_clear` and proceed. The commit/push is safe.
116
+
117
+ If any CRITICAL or HIGH findings: **STOP. The commit/push is BLOCKED.** Say this explicitly to the user before doing anything else:
118
+
119
+ ```
120
+ ftm-git: BLOCKED — <N> secret(s) found. Commit/push halted. Attempting auto-remediation...
121
+ ```
122
+
123
+ Then proceed to Phase 3. The commit/push does NOT happen until Phase 3 completes and a re-scan comes back clean.
124
+
125
+ ## Phase 3: Auto-Remediate
126
+
127
+ Read `references/protocols/REMEDIATION.md` for the full step-by-step remediation protocol, language-specific env var patterns, report formats (clean/remediated/blocked), and the Phase 5 git history deep scan procedure.
128
+
129
+ **Summary of steps:**
130
+ 1. Ensure `.env` and `.gitignore` infrastructure exists
131
+ 2. Extract each secret to `.env` with a SCREAMING_SNAKE_CASE var name
132
+ 3. Add placeholder to `.env.example`
133
+ 4. Refactor source files to reference the env var (match language pattern)
134
+ 5. Unstage `.env`, re-stage refactored source files
135
+ 6. Verify: re-run Phase 1 on refactored files — do not proceed until clean
136
+
137
+ ## Phase 4: Report
138
+
139
+ After remediation or clean scan, produce the summary. Read `references/protocols/REMEDIATION.md` for the exact report formats.
140
+
141
+ ## Integration Points
142
+
143
+ ### With ftm-executor
144
+ ftm-executor should invoke ftm-git before every commit operation in its task execution loop. If ftm-git emits `secrets_found`, the executor must pause and remediate before proceeding.
145
+
146
+ ### With ftm-mind
147
+ When ftm-mind routes a commit or push request, it should run ftm-git as a prerequisite gate. The commit/push only proceeds after `secrets_clear` or `secrets_remediated`.
148
+
149
+ ### With git-workflow agent
150
+ The git-workflow agent should check with ftm-git before executing any commit or push command. If you're about to run `git commit` or `git push`, ftm-git goes first.
151
+
152
+ ## Post-Commit Experience Recording
153
+
154
+ FTM includes a post-commit hook that guarantees every commit produces an experience entry in the blackboard.
155
+
156
+ ### How It Works
157
+
158
+ 1. After every `git commit`, the hook checks if an experience was recorded in the last 2 minutes
159
+ 2. If yes (the LLM already recorded a detailed experience) → skip, no duplicate
160
+ 3. If no → create a minimal experience from commit metadata (hash, message, files, branch)
161
+ 4. Update the experience index
162
+
163
+ ### Installation
164
+
165
+ The hook is at `ftm-git/hooks/post-commit-experience.sh`. To install:
166
+
167
+ ```bash
168
+ cp ~/.claude/skills/ftm-git/hooks/post-commit-experience.sh .git/hooks/post-commit
169
+ chmod +x .git/hooks/post-commit
170
+ ```
171
+
172
+ Or add to your project's husky config if using husky.
173
+
174
+ ### Minimal vs Rich Experiences
175
+
176
+ - **Minimal** (from hook): commit metadata only, confidence 0.5, tags: `auto-recorded`
177
+ - **Rich** (from LLM): full task context, lessons learned, higher confidence, domain-specific tags
178
+
179
+ The hook ensures no commit goes unrecorded, while the LLM produces richer entries during active sessions.
180
+
181
+ ## Blackboard Write
182
+
183
+ After completing, update the blackboard:
184
+
185
+ 1. Update `~/.claude/ftm-state/blackboard/context.json`:
186
+ - Set current_task status to "complete"
187
+ - Append scan summary to recent_decisions (cap at 10)
188
+ - Update session_metadata.skills_invoked and last_updated
189
+ 2. Write an experience file to `experiences/YYYY-MM-DD_secret-scan-<slug>.json` with:
190
+ - Number of files scanned
191
+ - Findings by severity
192
+ - Remediation actions taken
193
+ - Which patterns matched (to improve future scans)
194
+ 3. Update `experiences/index.json` with the new entry
195
+ 4. Emit `secrets_clear` or `secrets_remediated` or `secrets_blocked`
@@ -0,0 +1,26 @@
1
+ {
2
+ "skill_name": "ftm-git",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "name": "python-stripe-aws",
7
+ "prompt": "I just finished the payments module. commit this and push it up",
8
+ "expected_output": "Should detect sk_live_ Stripe key, AKIA AWS access key, and AWS secret. Should extract all three to .env, refactor payments.py to use os.environ/os.getenv, add .env to .gitignore, and block the commit until remediated.",
9
+ "files": ["test-fixtures/payments.py"]
10
+ },
11
+ {
12
+ "id": 2,
13
+ "name": "js-config-unignored-env",
14
+ "prompt": "hey can you scan this repo for any secrets before I share it with the new contractor",
15
+ "expected_output": "Should find Google API key, Slack bot token, Slack webhook, and SendGrid key in config.js. Should also flag .env not being in .gitignore. Should extract secrets to .env, refactor config.js to use process.env, ensure .gitignore covers .env files.",
16
+ "files": ["test-fixtures/config.js", "test-fixtures/.env"]
17
+ },
18
+ {
19
+ "id": 3,
20
+ "name": "yaml-google-key-in-ci",
21
+ "prompt": "push to main",
22
+ "expected_output": "Should detect Google API key hardcoded in deploy.yml env block, the RSA private key, and the Slack webhook URL. Should flag that CI files need to use GitHub Actions secrets (${{ secrets.VAR }}) instead of hardcoded values. Should block the push.",
23
+ "files": ["test-fixtures/deploy.yml"]
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,75 @@
1
+ description: "ftm-git secret detection and remediation"
2
+
3
+ prompts:
4
+ - "{{input}}"
5
+
6
+ providers:
7
+ - id: "exec:claude --print"
8
+ label: "claude-code"
9
+
10
+ tests:
11
+ # Eval 1: python-stripe-aws
12
+ - description: "detects Stripe live key and AWS credentials, extracts to .env, blocks commit"
13
+ vars:
14
+ input: "I just finished the payments module. commit this and push it up"
15
+ assert:
16
+ - type: regex
17
+ value: "sk_live_|stripe|Stripe"
18
+ description: "Detects the Stripe live key"
19
+ - type: regex
20
+ value: "AKIA|AWS|aws"
21
+ description: "Detects the AWS access key"
22
+ - type: regex
23
+ value: "\\.env"
24
+ description: "References .env file for secret extraction"
25
+ - type: regex
26
+ value: "\\.gitignore"
27
+ description: "References .gitignore to prevent committing secrets"
28
+ - type: regex
29
+ value: "block|blocked|cannot commit|will not commit|stop|halt|remediat"
30
+ description: "Blocks the commit until secrets are remediated"
31
+
32
+ # Eval 2: js-config-unignored-env
33
+ - description: "detects multiple secrets in config.js, flags missing .gitignore entry, extracts to .env"
34
+ vars:
35
+ input: "hey can you scan this repo for any secrets before I share it with the new contractor"
36
+ assert:
37
+ - type: regex
38
+ value: "Google|GOOGLE|AIza"
39
+ description: "Detects the Google API key"
40
+ - type: regex
41
+ value: "Slack|slack|xoxb"
42
+ description: "Detects the Slack token"
43
+ - type: regex
44
+ value: "SendGrid|sendgrid|SG\\."
45
+ description: "Detects the SendGrid key"
46
+ - type: regex
47
+ value: "\\.env"
48
+ description: "References .env file for secret extraction"
49
+ - type: regex
50
+ value: "\\.gitignore"
51
+ description: "Flags .gitignore coverage issue"
52
+ - type: regex
53
+ value: "process\\.env"
54
+ description: "Recommends using process.env in refactored code"
55
+
56
+ # Eval 3: yaml-google-key-in-ci
57
+ - description: "detects hardcoded secrets in CI yaml, recommends GitHub Actions secrets, blocks push"
58
+ vars:
59
+ input: "push to main"
60
+ assert:
61
+ - type: regex
62
+ value: "Google|GOOGLE|AIza|deploy\\.yml|CI|ci"
63
+ description: "Detects the Google API key in the CI file"
64
+ - type: regex
65
+ value: "RSA|private key|BEGIN RSA|-----BEGIN"
66
+ description: "Detects the RSA private key"
67
+ - type: regex
68
+ value: "Slack|slack|webhook"
69
+ description: "Detects the Slack webhook URL"
70
+ - type: regex
71
+ value: "secrets\\.|\\$\\{\\{|GitHub Actions|github actions|Actions secret"
72
+ description: "Recommends using GitHub Actions secrets instead of hardcoded values"
73
+ - type: regex
74
+ value: "block|blocked|cannot push|will not push|stop|halt|remediat"
75
+ description: "Blocks the push until secrets are remediated"
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # FTM Post-Commit Experience Recorder
4
+ # Ensures every commit produces at least a minimal experience entry.
5
+ # Only creates an entry if one hasn't been recorded recently (2+ min gap).
6
+
7
+ set -euo pipefail
8
+
9
+ STATE_DIR="$HOME/.claude/ftm-state/blackboard"
10
+ EXPERIENCES_DIR="$STATE_DIR/experiences"
11
+ INDEX_FILE="$EXPERIENCES_DIR/index.json"
12
+
13
+ # Ensure directories exist
14
+ mkdir -p "$EXPERIENCES_DIR"
15
+
16
+ # Check if an experience was recorded in the last 2 minutes
17
+ RECENT_THRESHOLD=$(($(date +%s) - 120))
18
+ LATEST_EXPERIENCE=""
19
+
20
+ if [ -d "$EXPERIENCES_DIR" ]; then
21
+ LATEST_EXPERIENCE=$(find "$EXPERIENCES_DIR" -name "*.json" -not -name "index.json" -newer /dev/null -maxdepth 1 2>/dev/null | sort -r | head -1)
22
+ fi
23
+
24
+ if [ -n "$LATEST_EXPERIENCE" ]; then
25
+ # Check if the latest experience file was modified within the last 2 minutes
26
+ if [ "$(uname)" = "Darwin" ]; then
27
+ FILE_TIME=$(stat -f %m "$LATEST_EXPERIENCE" 2>/dev/null || echo 0)
28
+ else
29
+ FILE_TIME=$(stat -c %Y "$LATEST_EXPERIENCE" 2>/dev/null || echo 0)
30
+ fi
31
+
32
+ if [ "$FILE_TIME" -gt "$RECENT_THRESHOLD" ]; then
33
+ # Experience was recently recorded by the LLM — skip
34
+ exit 0
35
+ fi
36
+ fi
37
+
38
+ # Extract commit metadata
39
+ COMMIT_HASH=$(git rev-parse --short HEAD)
40
+ COMMIT_MSG=$(git log -1 --pretty=%s)
41
+ COMMIT_DATE=$(date +%Y-%m-%d)
42
+ COMMIT_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
43
+ FILES_CHANGED=$(git diff-tree --no-commit-id --name-only -r HEAD | tr '\n' ', ' | sed 's/,$//')
44
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
45
+
46
+ # Generate a slug from commit message
47
+ SLUG=$(echo "$COMMIT_MSG" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | cut -c1-50)
48
+ FILENAME="${COMMIT_DATE}_${SLUG}.json"
49
+
50
+ # Don't create duplicate if file already exists
51
+ if [ -f "$EXPERIENCES_DIR/$FILENAME" ]; then
52
+ exit 0
53
+ fi
54
+
55
+ # Create minimal experience entry
56
+ cat > "$EXPERIENCES_DIR/$FILENAME" << EXPEOF
57
+ {
58
+ "task_type": "commit",
59
+ "description": "$COMMIT_MSG",
60
+ "source": "git-hook",
61
+ "timestamp": "$COMMIT_TIME",
62
+ "commit_hash": "$COMMIT_HASH",
63
+ "branch": "$BRANCH",
64
+ "files_changed": "$FILES_CHANGED",
65
+ "complexity_estimated": "micro",
66
+ "complexity_actual": "micro",
67
+ "outcome": "success",
68
+ "confidence": 0.5,
69
+ "tags": ["auto-recorded", "git-commit"],
70
+ "lessons": []
71
+ }
72
+ EXPEOF
73
+
74
+ # Update index.json
75
+ # Read existing index, add new entry, write back
76
+ if [ -f "$INDEX_FILE" ]; then
77
+ # Use node for reliable JSON manipulation (available in FTM environments)
78
+ node -e "
79
+ const fs = require('fs');
80
+ const idx = JSON.parse(fs.readFileSync('$INDEX_FILE', 'utf-8'));
81
+ idx.entries.push({
82
+ file: '$FILENAME',
83
+ task_type: 'commit',
84
+ tags: ['auto-recorded', 'git-commit'],
85
+ timestamp: '$COMMIT_TIME',
86
+ confidence: 0.5
87
+ });
88
+ idx.metadata.total_count = idx.entries.length;
89
+ idx.metadata.last_updated = '$COMMIT_TIME';
90
+ fs.writeFileSync('$INDEX_FILE', JSON.stringify(idx, null, 2));
91
+ " 2>/dev/null || true
92
+ fi