feed-the-machine 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +170 -170
  3. package/bin/brain.py +1340 -0
  4. package/bin/convert_claude_skills_to_codex.py +490 -0
  5. package/bin/generate-manifest.mjs +463 -463
  6. package/bin/harden_codex_skills.py +141 -0
  7. package/bin/install.mjs +491 -491
  8. package/bin/migrate-eng-buddy-data.py +875 -0
  9. package/bin/playbook_engine/__init__.py +1 -0
  10. package/bin/playbook_engine/conftest.py +8 -0
  11. package/bin/playbook_engine/extractor.py +33 -0
  12. package/bin/playbook_engine/manager.py +102 -0
  13. package/bin/playbook_engine/models.py +84 -0
  14. package/bin/playbook_engine/registry.py +35 -0
  15. package/bin/playbook_engine/test_extractor.py +72 -0
  16. package/bin/playbook_engine/test_integration.py +129 -0
  17. package/bin/playbook_engine/test_manager.py +85 -0
  18. package/bin/playbook_engine/test_models.py +166 -0
  19. package/bin/playbook_engine/test_registry.py +67 -0
  20. package/bin/playbook_engine/test_tracer.py +86 -0
  21. package/bin/playbook_engine/tracer.py +93 -0
  22. package/bin/tasks_db.py +456 -0
  23. package/docs/HOOKS.md +243 -243
  24. package/docs/INBOX.md +233 -233
  25. package/ftm/SKILL.md +125 -122
  26. package/ftm-audit/SKILL.md +623 -623
  27. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
  28. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
  29. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
  30. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
  31. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
  32. package/ftm-audit/scripts/run-knip.sh +23 -23
  33. package/ftm-audit.yml +2 -2
  34. package/ftm-brainstorm/SKILL.md +1003 -498
  35. package/ftm-brainstorm/evals/evals.json +180 -100
  36. package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
  37. package/ftm-brainstorm/references/agent-prompts.md +552 -224
  38. package/ftm-brainstorm/references/plan-template.md +209 -121
  39. package/ftm-brainstorm.yml +2 -2
  40. package/ftm-browse/SKILL.md +454 -454
  41. package/ftm-browse/daemon/browser-manager.ts +206 -206
  42. package/ftm-browse/daemon/bun.lock +30 -30
  43. package/ftm-browse/daemon/cli.ts +347 -347
  44. package/ftm-browse/daemon/commands.ts +410 -410
  45. package/ftm-browse/daemon/main.ts +357 -357
  46. package/ftm-browse/daemon/package.json +17 -17
  47. package/ftm-browse/daemon/server.ts +189 -189
  48. package/ftm-browse/daemon/snapshot.ts +519 -519
  49. package/ftm-browse/daemon/tsconfig.json +22 -22
  50. package/ftm-browse.yml +4 -4
  51. package/ftm-capture/SKILL.md +370 -370
  52. package/ftm-capture.yml +4 -4
  53. package/ftm-codex-gate/SKILL.md +361 -361
  54. package/ftm-codex-gate.yml +2 -2
  55. package/ftm-config/SKILL.md +422 -345
  56. package/ftm-config.default.yml +125 -82
  57. package/ftm-config.yml +44 -2
  58. package/ftm-council/SKILL.md +416 -416
  59. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
  60. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
  61. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
  62. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
  63. package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
  64. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
  65. package/ftm-council.yml +2 -2
  66. package/ftm-dashboard/SKILL.md +163 -163
  67. package/ftm-dashboard.yml +4 -4
  68. package/ftm-debug/SKILL.md +1037 -1037
  69. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
  70. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
  71. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
  72. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
  73. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
  74. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
  75. package/ftm-debug.yml +2 -2
  76. package/ftm-diagram/SKILL.md +277 -277
  77. package/ftm-diagram.yml +2 -2
  78. package/ftm-executor/SKILL.md +777 -777
  79. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
  80. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
  81. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
  82. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
  83. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
  84. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
  85. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
  86. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
  87. package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
  88. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
  89. package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
  90. package/ftm-executor/runtime/package.json +8 -8
  91. package/ftm-executor.yml +2 -2
  92. package/ftm-git/SKILL.md +441 -441
  93. package/ftm-git/evals/evals.json +26 -26
  94. package/ftm-git/evals/promptfoo.yaml +75 -75
  95. package/ftm-git/hooks/post-commit-experience.sh +92 -92
  96. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
  97. package/ftm-git/references/protocols/REMEDIATION.md +139 -139
  98. package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
  99. package/ftm-git.yml +2 -2
  100. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  101. package/ftm-inbox/backend/adapters/_retry.py +64 -64
  102. package/ftm-inbox/backend/adapters/base.py +230 -230
  103. package/ftm-inbox/backend/adapters/freshservice.py +104 -104
  104. package/ftm-inbox/backend/adapters/gmail.py +125 -125
  105. package/ftm-inbox/backend/adapters/jira.py +136 -136
  106. package/ftm-inbox/backend/adapters/registry.py +192 -192
  107. package/ftm-inbox/backend/adapters/slack.py +110 -110
  108. package/ftm-inbox/backend/db/connection.py +54 -54
  109. package/ftm-inbox/backend/db/schema.py +78 -78
  110. package/ftm-inbox/backend/executor/__init__.py +7 -7
  111. package/ftm-inbox/backend/executor/engine.py +149 -149
  112. package/ftm-inbox/backend/executor/step_runner.py +98 -98
  113. package/ftm-inbox/backend/main.py +103 -103
  114. package/ftm-inbox/backend/models/__init__.py +1 -1
  115. package/ftm-inbox/backend/models/unified_task.py +36 -36
  116. package/ftm-inbox/backend/planner/__init__.py +6 -6
  117. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  118. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  119. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  120. package/ftm-inbox/backend/planner/generator.py +127 -127
  121. package/ftm-inbox/backend/planner/schema.py +34 -34
  122. package/ftm-inbox/backend/requirements.txt +5 -5
  123. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  124. package/ftm-inbox/backend/routes/execute.py +186 -186
  125. package/ftm-inbox/backend/routes/health.py +52 -52
  126. package/ftm-inbox/backend/routes/inbox.py +68 -68
  127. package/ftm-inbox/backend/routes/plan.py +271 -271
  128. package/ftm-inbox/bin/launchagent.mjs +91 -91
  129. package/ftm-inbox/bin/setup.mjs +188 -188
  130. package/ftm-inbox/bin/start.sh +10 -10
  131. package/ftm-inbox/bin/status.sh +17 -17
  132. package/ftm-inbox/bin/stop.sh +8 -8
  133. package/ftm-inbox/config.example.yml +55 -55
  134. package/ftm-inbox/package-lock.json +2898 -2898
  135. package/ftm-inbox/package.json +26 -26
  136. package/ftm-inbox/postcss.config.js +6 -6
  137. package/ftm-inbox/src/app.css +199 -199
  138. package/ftm-inbox/src/app.html +18 -18
  139. package/ftm-inbox/src/lib/api.ts +166 -166
  140. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
  141. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
  142. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
  143. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
  144. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
  145. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
  146. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
  147. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
  148. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
  149. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
  150. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
  151. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
  152. package/ftm-inbox/src/lib/theme.ts +47 -47
  153. package/ftm-inbox/src/routes/+layout.svelte +76 -76
  154. package/ftm-inbox/src/routes/+page.svelte +401 -401
  155. package/ftm-inbox/svelte.config.js +12 -12
  156. package/ftm-inbox/tailwind.config.ts +63 -63
  157. package/ftm-inbox/tsconfig.json +13 -13
  158. package/ftm-inbox/vite.config.ts +6 -6
  159. package/ftm-intent/SKILL.md +241 -241
  160. package/ftm-intent.yml +2 -2
  161. package/ftm-manifest.json +3794 -3794
  162. package/ftm-map/SKILL.md +291 -291
  163. package/ftm-map/scripts/db.py +712 -712
  164. package/ftm-map/scripts/index.py +415 -415
  165. package/ftm-map/scripts/parser.py +224 -224
  166. package/ftm-map/scripts/queries/go-tags.scm +20 -20
  167. package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
  168. package/ftm-map/scripts/queries/python-tags.scm +31 -31
  169. package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
  170. package/ftm-map/scripts/queries/rust-tags.scm +37 -37
  171. package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
  172. package/ftm-map/scripts/query.py +301 -301
  173. package/ftm-map/scripts/ranker.py +377 -377
  174. package/ftm-map/scripts/requirements.txt +5 -5
  175. package/ftm-map/scripts/setup-hooks.sh +27 -27
  176. package/ftm-map/scripts/setup.sh +56 -56
  177. package/ftm-map/scripts/test_db.py +364 -364
  178. package/ftm-map/scripts/test_parser.py +174 -174
  179. package/ftm-map/scripts/test_query.py +183 -183
  180. package/ftm-map/scripts/test_ranker.py +199 -199
  181. package/ftm-map/scripts/views.py +591 -591
  182. package/ftm-map.yml +2 -2
  183. package/ftm-mind/SKILL.md +201 -1943
  184. package/ftm-mind/evals/promptfoo.yaml +142 -142
  185. package/ftm-mind/references/blackboard-protocol.md +110 -0
  186. package/ftm-mind/references/blackboard-schema.md +328 -328
  187. package/ftm-mind/references/complexity-guide.md +110 -110
  188. package/ftm-mind/references/complexity-sizing.md +138 -0
  189. package/ftm-mind/references/decide-act-protocol.md +172 -0
  190. package/ftm-mind/references/direct-execution.md +51 -0
  191. package/ftm-mind/references/environment-discovery.md +77 -0
  192. package/ftm-mind/references/event-registry.md +319 -319
  193. package/ftm-mind/references/mcp-inventory.md +300 -296
  194. package/ftm-mind/references/ops-routing.md +47 -0
  195. package/ftm-mind/references/orient-protocol.md +234 -0
  196. package/ftm-mind/references/personality.md +40 -0
  197. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
  198. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
  199. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
  200. package/ftm-mind/references/reflexion-protocol.md +249 -249
  201. package/ftm-mind/references/routing/SCENARIOS.md +22 -22
  202. package/ftm-mind/references/routing-scenarios.md +35 -35
  203. package/ftm-mind.yml +2 -2
  204. package/ftm-ops.yml +4 -0
  205. package/ftm-pause/SKILL.md +395 -395
  206. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
  207. package/ftm-pause/references/protocols/VALIDATION.md +80 -80
  208. package/ftm-pause.yml +2 -2
  209. package/ftm-researcher/SKILL.md +275 -275
  210. package/ftm-researcher/evals/agent-diversity.yaml +17 -17
  211. package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
  212. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
  213. package/ftm-researcher/references/adaptive-search.md +116 -116
  214. package/ftm-researcher/references/agent-prompts.md +193 -193
  215. package/ftm-researcher/references/council-integration.md +193 -193
  216. package/ftm-researcher/references/output-format.md +203 -203
  217. package/ftm-researcher/references/synthesis-pipeline.md +165 -165
  218. package/ftm-researcher/scripts/score_credibility.py +234 -234
  219. package/ftm-researcher/scripts/validate_research.py +92 -92
  220. package/ftm-researcher.yml +2 -2
  221. package/ftm-resume/SKILL.md +518 -518
  222. package/ftm-resume/references/protocols/VALIDATION.md +172 -172
  223. package/ftm-resume.yml +2 -2
  224. package/ftm-retro/SKILL.md +380 -380
  225. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
  226. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
  227. package/ftm-retro.yml +2 -2
  228. package/ftm-routine/SKILL.md +170 -170
  229. package/ftm-routine.yml +4 -4
  230. package/ftm-state/blackboard/capabilities.json +5 -5
  231. package/ftm-state/blackboard/capabilities.schema.json +27 -27
  232. package/ftm-state/blackboard/context.json +37 -23
  233. package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
  234. package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
  235. package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
  236. package/ftm-state/blackboard/experiences/index.json +58 -9
  237. package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
  238. package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
  239. package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
  240. package/ftm-state/blackboard/patterns.json +6 -6
  241. package/ftm-state/schemas/context.schema.json +130 -130
  242. package/ftm-state/schemas/experience-index.schema.json +77 -77
  243. package/ftm-state/schemas/experience.schema.json +78 -78
  244. package/ftm-state/schemas/patterns.schema.json +44 -44
  245. package/ftm-upgrade/SKILL.md +194 -194
  246. package/ftm-upgrade/scripts/check-version.sh +76 -76
  247. package/ftm-upgrade/scripts/upgrade.sh +143 -143
  248. package/ftm-upgrade.yml +2 -2
  249. package/ftm-verify.yml +2 -2
  250. package/ftm.yml +2 -2
  251. package/hooks/ftm-auto-log.sh +137 -0
  252. package/hooks/ftm-blackboard-enforcer.sh +93 -93
  253. package/hooks/ftm-discovery-reminder.sh +90 -90
  254. package/hooks/ftm-drafts-gate.sh +61 -61
  255. package/hooks/ftm-event-logger.mjs +107 -107
  256. package/hooks/ftm-install-hooks.sh +240 -0
  257. package/hooks/ftm-learning-capture.sh +117 -0
  258. package/hooks/ftm-map-autodetect.sh +79 -79
  259. package/hooks/ftm-pending-sync-check.sh +22 -22
  260. package/hooks/ftm-plan-gate.sh +92 -92
  261. package/hooks/ftm-post-commit-trigger.sh +57 -57
  262. package/hooks/ftm-post-compaction.sh +138 -0
  263. package/hooks/ftm-pre-compaction.sh +147 -0
  264. package/hooks/ftm-session-end.sh +52 -0
  265. package/hooks/ftm-session-snapshot.sh +213 -0
  266. package/hooks/settings-template.json +81 -81
  267. package/install.sh +363 -363
  268. package/package.json +84 -84
  269. package/uninstall.sh +25 -25
@@ -1,139 +1,139 @@
1
- # Remediation Protocol — Auto-Fix Steps
2
-
3
- Detailed remediation steps for secrets found during Phase 1–2 scanning. Apply in sequence for each finding.
4
-
5
- ---
6
-
7
- ## Phase 3: Auto-Remediate
8
-
9
- For each finding, apply the appropriate fix automatically. The goal is to make the code safe without breaking functionality.
10
-
11
- ### Step 1: Ensure .env infrastructure exists
12
-
13
- Check for a `.env` file in the project root. If it doesn't exist, create one with a header comment:
14
-
15
- ```
16
- # Environment variables — DO NOT COMMIT THIS FILE
17
- # Copy .env.example for the template, fill in real values locally
18
- ```
19
-
20
- Check `.gitignore` for `.env` coverage. If missing, add:
21
- ```
22
- # Environment files with secrets
23
- .env
24
- .env.local
25
- .env.production
26
- .env.staging
27
- .env.*.local
28
- ```
29
-
30
- ### Step 2: Extract secrets to .env
31
-
32
- For each finding:
33
-
34
- 1. **Choose an env var name** — derive it from the context. If the code says `STRIPE_API_KEY = "sk_live_..."`, the env var is `STRIPE_API_KEY`. If it says `api_key: "AIza..."`, infer from the file/service context (e.g., `GOOGLE_API_KEY`). Use SCREAMING_SNAKE_CASE.
35
-
36
- 2. **Add to .env** — append `VAR_NAME=<actual-secret-value>` to `.env`. If the var already exists, don't duplicate it.
37
-
38
- 3. **Add to .env.example** — create or update `.env.example` with `VAR_NAME=your-value-here` so other developers know the variable exists without seeing the real value.
39
-
40
- ### Step 3: Refactor source files
41
-
42
- Replace the hardcoded secret with an env var reference. Match the language/framework:
43
-
44
- | Language | Pattern |
45
- |---|---|
46
- | Python | `os.environ["VAR_NAME"]` or `os.getenv("VAR_NAME")` (match existing style in file) |
47
- | JavaScript/TypeScript | `process.env.VAR_NAME` |
48
- | Ruby | `ENV["VAR_NAME"]` or `ENV.fetch("VAR_NAME")` |
49
- | Go | `os.Getenv("VAR_NAME")` |
50
- | Java | `System.getenv("VAR_NAME")` |
51
- | Shell/Bash | `$VAR_NAME` or `${VAR_NAME}` |
52
- | YAML/JSON config | `${VAR_NAME}` (if the framework supports interpolation) or add a comment pointing to the env var |
53
-
54
- If the file doesn't already import the env-reading module (e.g., `import os` in Python, `require('dotenv').config()` in Node), add the import. Check if the project uses `python-dotenv`, `dotenv` (Node), or similar — if so, use the project's existing pattern for loading env vars.
55
-
56
- ### Step 4: Unstage remediated files
57
-
58
- After refactoring, make sure the `.env` file (with real secrets) is NOT staged:
59
-
60
- ```bash
61
- git reset HEAD .env 2>/dev/null # unstage if accidentally staged
62
- ```
63
-
64
- Stage the refactored source files (which now reference env vars instead of hardcoded secrets):
65
-
66
- ```bash
67
- git add <refactored-files>
68
- ```
69
-
70
- ### Step 5: Verify the fix
71
-
72
- Re-run Phase 1 scan on the refactored files to confirm the secrets are gone. If any remain, loop back and fix. Do not proceed until the scan is clean.
73
-
74
- ---
75
-
76
- ## Phase 4: Report
77
-
78
- After remediation (or if the scan was clean from the start), produce a summary:
79
-
80
- **Clean scan:**
81
- ```
82
- ftm-git: Clean scan. 0 secrets found in <N> files scanned. Safe to commit.
83
- ```
84
-
85
- **After remediation:**
86
- ```
87
- ftm-git: Found <N> hardcoded secrets. Auto-remediated:
88
-
89
- CRITICAL: sk_live_**** in src/payments.py:42 -> STRIPE_SECRET_KEY
90
- HIGH: AIza**** in config/google.ts:18 -> GOOGLE_API_KEY
91
- MEDIUM: .env was not in .gitignore -> added
92
-
93
- Actions taken:
94
- - Extracted <N> secrets to .env (gitignored)
95
- - Created/updated .env.example with placeholder vars
96
- - Refactored <N> source files to use env var references
97
- - Updated .gitignore
98
-
99
- Verify the app still works with the new env var setup, then commit.
100
- ```
101
-
102
- **Blocked (auto-fix not possible):**
103
-
104
- Some secrets can't be auto-fixed — for example, a private key embedded in a binary file, or a secret in a format the skill can't safely refactor. In these cases:
105
-
106
- ```
107
- ftm-git: BLOCKED. Found secrets that require manual remediation:
108
-
109
- CRITICAL: Private key in assets/cert.pem:1
110
- -> Move this file outside the repo and reference via path env var
111
-
112
- Action required: Fix the above manually, then run ftm-git again.
113
- ```
114
-
115
- ---
116
-
117
- ## Phase 5: Git History Check (Manual Invocation Only)
118
-
119
- When explicitly asked to do a deep scan (e.g., "scan the repo history for secrets"), also check past commits. This is expensive so it only runs on explicit request, not as part of the pre-commit gate.
120
-
121
- ```bash
122
- git log --all --diff-filter=A --name-only --pretty=format:"%H" -- "*.env" "*.pem" "*.key" "*credentials*" "*secret*"
123
- ```
124
-
125
- For each historically added sensitive file, check if it's still in the current tree. If it was added and later removed, warn that the secret is still in git history and suggest:
126
-
127
- 1. Rotate the credential immediately (it's compromised)
128
- 2. Use `git filter-repo` or BFG Repo Cleaner to purge from history if needed
129
-
130
- ---
131
-
132
- ## Operating Principles
133
-
134
- 1. **Block first, fix second.** Never let a secret through while figuring out the fix. The commit waits.
135
- 2. **Zero false negatives over zero false positives.** It's better to flag something that turns out to be harmless than to miss a real key.
136
- 3. **Never log full secrets.** In all output, mask secret values. Show only enough to identify which secret it is (first 8 + last 4 chars).
137
- 4. **Env vars are the escape hatch.** The remediation pattern is always: secret goes to gitignored .env, code references the env var.
138
- 5. **Existing patterns win.** If the project already uses dotenv, Vault, AWS Secrets Manager, or any other secret management system, match that pattern rather than introducing a new one.
139
- 6. **Test files are not exempt.** A real `sk_live_*` key in a test file is just as dangerous as one in production code. Only `sk_test_*` with obviously fake values get a pass.
1
+ # Remediation Protocol — Auto-Fix Steps
2
+
3
+ Detailed remediation steps for secrets found during Phase 1–2 scanning. Apply in sequence for each finding.
4
+
5
+ ---
6
+
7
+ ## Phase 3: Auto-Remediate
8
+
9
+ For each finding, apply the appropriate fix automatically. The goal is to make the code safe without breaking functionality.
10
+
11
+ ### Step 1: Ensure .env infrastructure exists
12
+
13
+ Check for a `.env` file in the project root. If it doesn't exist, create one with a header comment:
14
+
15
+ ```
16
+ # Environment variables — DO NOT COMMIT THIS FILE
17
+ # Copy .env.example for the template, fill in real values locally
18
+ ```
19
+
20
+ Check `.gitignore` for `.env` coverage. If missing, add:
21
+ ```
22
+ # Environment files with secrets
23
+ .env
24
+ .env.local
25
+ .env.production
26
+ .env.staging
27
+ .env.*.local
28
+ ```
29
+
30
+ ### Step 2: Extract secrets to .env
31
+
32
+ For each finding:
33
+
34
+ 1. **Choose an env var name** — derive it from the context. If the code says `STRIPE_API_KEY = "sk_live_..."`, the env var is `STRIPE_API_KEY`. If it says `api_key: "AIza..."`, infer from the file/service context (e.g., `GOOGLE_API_KEY`). Use SCREAMING_SNAKE_CASE.
35
+
36
+ 2. **Add to .env** — append `VAR_NAME=<actual-secret-value>` to `.env`. If the var already exists, don't duplicate it.
37
+
38
+ 3. **Add to .env.example** — create or update `.env.example` with `VAR_NAME=your-value-here` so other developers know the variable exists without seeing the real value.
39
+
40
+ ### Step 3: Refactor source files
41
+
42
+ Replace the hardcoded secret with an env var reference. Match the language/framework:
43
+
44
+ | Language | Pattern |
45
+ |---|---|
46
+ | Python | `os.environ["VAR_NAME"]` or `os.getenv("VAR_NAME")` (match existing style in file) |
47
+ | JavaScript/TypeScript | `process.env.VAR_NAME` |
48
+ | Ruby | `ENV["VAR_NAME"]` or `ENV.fetch("VAR_NAME")` |
49
+ | Go | `os.Getenv("VAR_NAME")` |
50
+ | Java | `System.getenv("VAR_NAME")` |
51
+ | Shell/Bash | `$VAR_NAME` or `${VAR_NAME}` |
52
+ | YAML/JSON config | `${VAR_NAME}` (if the framework supports interpolation) or add a comment pointing to the env var |
53
+
54
+ If the file doesn't already import the env-reading module (e.g., `import os` in Python, `require('dotenv').config()` in Node), add the import. Check if the project uses `python-dotenv`, `dotenv` (Node), or similar — if so, use the project's existing pattern for loading env vars.
55
+
56
+ ### Step 4: Unstage remediated files
57
+
58
+ After refactoring, make sure the `.env` file (with real secrets) is NOT staged:
59
+
60
+ ```bash
61
+ git reset HEAD .env 2>/dev/null # unstage if accidentally staged
62
+ ```
63
+
64
+ Stage the refactored source files (which now reference env vars instead of hardcoded secrets):
65
+
66
+ ```bash
67
+ git add <refactored-files>
68
+ ```
69
+
70
+ ### Step 5: Verify the fix
71
+
72
+ Re-run Phase 1 scan on the refactored files to confirm the secrets are gone. If any remain, loop back and fix. Do not proceed until the scan is clean.
73
+
74
+ ---
75
+
76
+ ## Phase 4: Report
77
+
78
+ After remediation (or if the scan was clean from the start), produce a summary:
79
+
80
+ **Clean scan:**
81
+ ```
82
+ ftm-git: Clean scan. 0 secrets found in <N> files scanned. Safe to commit.
83
+ ```
84
+
85
+ **After remediation:**
86
+ ```
87
+ ftm-git: Found <N> hardcoded secrets. Auto-remediated:
88
+
89
+ CRITICAL: sk_live_**** in src/payments.py:42 -> STRIPE_SECRET_KEY
90
+ HIGH: AIza**** in config/google.ts:18 -> GOOGLE_API_KEY
91
+ MEDIUM: .env was not in .gitignore -> added
92
+
93
+ Actions taken:
94
+ - Extracted <N> secrets to .env (gitignored)
95
+ - Created/updated .env.example with placeholder vars
96
+ - Refactored <N> source files to use env var references
97
+ - Updated .gitignore
98
+
99
+ Verify the app still works with the new env var setup, then commit.
100
+ ```
101
+
102
+ **Blocked (auto-fix not possible):**
103
+
104
+ Some secrets can't be auto-fixed — for example, a private key embedded in a binary file, or a secret in a format the skill can't safely refactor. In these cases:
105
+
106
+ ```
107
+ ftm-git: BLOCKED. Found secrets that require manual remediation:
108
+
109
+ CRITICAL: Private key in assets/cert.pem:1
110
+ -> Move this file outside the repo and reference via path env var
111
+
112
+ Action required: Fix the above manually, then run ftm-git again.
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Phase 5: Git History Check (Manual Invocation Only)
118
+
119
+ When explicitly asked to do a deep scan (e.g., "scan the repo history for secrets"), also check past commits. This is expensive so it only runs on explicit request, not as part of the pre-commit gate.
120
+
121
+ ```bash
122
+ git log --all --diff-filter=A --name-only --pretty=format:"%H" -- "*.env" "*.pem" "*.key" "*credentials*" "*secret*"
123
+ ```
124
+
125
+ For each historically added sensitive file, check if it's still in the current tree. If it was added and later removed, warn that the secret is still in git history and suggest:
126
+
127
+ 1. Rotate the credential immediately (it's compromised)
128
+ 2. Use `git filter-repo` or BFG Repo Cleaner to purge from history if needed
129
+
130
+ ---
131
+
132
+ ## Operating Principles
133
+
134
+ 1. **Block first, fix second.** Never let a secret through while figuring out the fix. The commit waits.
135
+ 2. **Zero false negatives over zero false positives.** It's better to flag something that turns out to be harmless than to miss a real key.
136
+ 3. **Never log full secrets.** In all output, mask secret values. Show only enough to identify which secret it is (first 8 + last 4 chars).
137
+ 4. **Env vars are the escape hatch.** The remediation pattern is always: secret goes to gitignored .env, code references the env var.
138
+ 5. **Existing patterns win.** If the project already uses dotenv, Vault, AWS Secrets Manager, or any other secret management system, match that pattern rather than introducing a new one.
139
+ 6. **Test files are not exempt.** A real `sk_live_*` key in a test file is just as dangerous as one in production code. Only `sk_test_*` with obviously fake values get a pass.
@@ -1,110 +1,110 @@
1
- #!/usr/bin/env bash
2
- # ftm-git pre-commit hook — blocks commits containing hardcoded secrets
3
- # Installed by the ftm-git skill on first invocation. Safe to remove with:
4
- # rm .git/hooks/pre-commit (or edit to remove the ftm-git section)
5
- #
6
- # This hook scans staged files only (fast). The full ftm-git skill does
7
- # deeper scanning with context validation and auto-remediation — this is
8
- # the safety net that catches what slips through.
9
-
10
- set -euo pipefail
11
-
12
- RED='\033[0;31m'
13
- YELLOW='\033[0;33m'
14
- NC='\033[0m'
15
-
16
- # Get list of staged files (excluding deletions)
17
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
18
- if [ -z "$STAGED_FILES" ]; then
19
- exit 0
20
- fi
21
-
22
- # Skip binary files, lock files, and vendored directories
23
- FILTERED_FILES=""
24
- for f in $STAGED_FILES; do
25
- case "$f" in
26
- node_modules/*|vendor/*|.git/*|__pycache__/*|dist/*|build/*) continue ;;
27
- package-lock.json|yarn.lock|Gemfile.lock|poetry.lock|pnpm-lock.yaml) continue ;;
28
- *.png|*.jpg|*.gif|*.ico|*.woff|*.woff2|*.ttf|*.eot|*.pdf|*.zip|*.tar*) continue ;;
29
- .env.example|.env.sample|.env.template) continue ;;
30
- *) FILTERED_FILES="$FILTERED_FILES $f" ;;
31
- esac
32
- done
33
-
34
- if [ -z "$FILTERED_FILES" ]; then
35
- exit 0
36
- fi
37
-
38
- FOUND=0
39
- FINDINGS=""
40
-
41
- # Tier 1: High-confidence patterns — these almost never false-positive
42
- # We scan staged content (not working tree) to catch exactly what would be committed
43
- PATTERNS=(
44
- 'AKIA[0-9A-Z]{16}' # AWS Access Key ID
45
- 'ghp_[A-Za-z0-9_]{36}' # GitHub PAT
46
- 'gho_[A-Za-z0-9_]{36}' # GitHub OAuth
47
- 'ghu_[A-Za-z0-9_]{36}' # GitHub user token
48
- 'ghs_[A-Za-z0-9_]{36}' # GitHub server token
49
- 'github_pat_[A-Za-z0-9_]{82}' # GitHub fine-grained PAT
50
- 'xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}' # Slack bot token
51
- 'xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,34}' # Slack user token
52
- 'xoxa-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,34}' # Slack app token
53
- 'AIza[0-9A-Za-z\-_]{35}' # Google API key
54
- 'sk_live_[0-9a-zA-Z]{24,}' # Stripe live secret
55
- 'sk_test_[0-9a-zA-Z]{24,}' # Stripe test secret
56
- 'rk_live_[0-9a-zA-Z]{24,}' # Stripe restricted
57
- 'SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}' # SendGrid
58
- 'SK[0-9a-fA-F]{32}' # Twilio
59
- 'npm_[A-Za-z0-9]{36}' # npm token
60
- 'glpat-[A-Za-z0-9\-_]{20,}' # GitLab PAT
61
- '-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----' # Private keys
62
- )
63
-
64
- for f in $FILTERED_FILES; do
65
- # Get the staged version of the file (what would actually be committed)
66
- CONTENT=$(git show ":$f" 2>/dev/null || true)
67
- if [ -z "$CONTENT" ]; then
68
- continue
69
- fi
70
-
71
- for pattern in "${PATTERNS[@]}"; do
72
- MATCHES=$(echo "$CONTENT" | grep -nE "$pattern" 2>/dev/null || true)
73
- if [ -n "$MATCHES" ]; then
74
- while IFS= read -r match; do
75
- LINE_NUM=$(echo "$match" | cut -d: -f1)
76
- # Mask the secret value in output (show first 8 chars only)
77
- MASKED=$(echo "$match" | cut -d: -f2- | sed -E 's/([A-Za-z0-9_\-]{8})[A-Za-z0-9_\-]{8,}/\1****/g')
78
- FINDINGS="${FINDINGS}\n ${RED}BLOCKED${NC} $f:$LINE_NUM $MASKED"
79
- FOUND=$((FOUND + 1))
80
- done <<< "$MATCHES"
81
- fi
82
- done
83
- done
84
-
85
- # Also check: is a .env file being committed?
86
- for f in $STAGED_FILES; do
87
- case "$f" in
88
- .env|.env.local|.env.production|.env.staging|.env.*.local)
89
- FINDINGS="${FINDINGS}\n ${YELLOW}WARNING${NC} $f is staged — this file typically contains secrets and should be gitignored"
90
- FOUND=$((FOUND + 1))
91
- ;;
92
- esac
93
- done
94
-
95
- if [ "$FOUND" -gt 0 ]; then
96
- echo ""
97
- echo -e "${RED}ftm-git: COMMIT BLOCKED — $FOUND secret(s) detected in staged files${NC}"
98
- echo ""
99
- echo -e "$FINDINGS"
100
- echo ""
101
- echo "To fix: run /ftm-git for auto-remediation, or manually:"
102
- echo " 1. Move secrets to .env (gitignored)"
103
- echo " 2. Replace hardcoded values with env var references"
104
- echo " 3. Stage the cleaned files and commit again"
105
- echo ""
106
- echo "To bypass (NOT recommended): git commit --no-verify"
107
- exit 1
108
- fi
109
-
110
- exit 0
1
+ #!/usr/bin/env bash
2
+ # ftm-git pre-commit hook — blocks commits containing hardcoded secrets
3
+ # Installed by the ftm-git skill on first invocation. Safe to remove with:
4
+ # rm .git/hooks/pre-commit (or edit to remove the ftm-git section)
5
+ #
6
+ # This hook scans staged files only (fast). The full ftm-git skill does
7
+ # deeper scanning with context validation and auto-remediation — this is
8
+ # the safety net that catches what slips through.
9
+
10
+ set -euo pipefail
11
+
12
+ RED='\033[0;31m'
13
+ YELLOW='\033[0;33m'
14
+ NC='\033[0m'
15
+
16
+ # Get list of staged files (excluding deletions)
17
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
18
+ if [ -z "$STAGED_FILES" ]; then
19
+ exit 0
20
+ fi
21
+
22
+ # Skip binary files, lock files, and vendored directories
23
+ FILTERED_FILES=""
24
+ for f in $STAGED_FILES; do
25
+ case "$f" in
26
+ node_modules/*|vendor/*|.git/*|__pycache__/*|dist/*|build/*) continue ;;
27
+ package-lock.json|yarn.lock|Gemfile.lock|poetry.lock|pnpm-lock.yaml) continue ;;
28
+ *.png|*.jpg|*.gif|*.ico|*.woff|*.woff2|*.ttf|*.eot|*.pdf|*.zip|*.tar*) continue ;;
29
+ .env.example|.env.sample|.env.template) continue ;;
30
+ *) FILTERED_FILES="$FILTERED_FILES $f" ;;
31
+ esac
32
+ done
33
+
34
+ if [ -z "$FILTERED_FILES" ]; then
35
+ exit 0
36
+ fi
37
+
38
+ FOUND=0
39
+ FINDINGS=""
40
+
41
+ # Tier 1: High-confidence patterns — these almost never false-positive
42
+ # We scan staged content (not working tree) to catch exactly what would be committed
43
+ PATTERNS=(
44
+ 'AKIA[0-9A-Z]{16}' # AWS Access Key ID
45
+ 'ghp_[A-Za-z0-9_]{36}' # GitHub PAT
46
+ 'gho_[A-Za-z0-9_]{36}' # GitHub OAuth
47
+ 'ghu_[A-Za-z0-9_]{36}' # GitHub user token
48
+ 'ghs_[A-Za-z0-9_]{36}' # GitHub server token
49
+ 'github_pat_[A-Za-z0-9_]{82}' # GitHub fine-grained PAT
50
+ 'xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}' # Slack bot token
51
+ 'xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,34}' # Slack user token
52
+ 'xoxa-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,34}' # Slack app token
53
+ 'AIza[0-9A-Za-z\-_]{35}' # Google API key
54
+ 'sk_live_[0-9a-zA-Z]{24,}' # Stripe live secret
55
+ 'sk_test_[0-9a-zA-Z]{24,}' # Stripe test secret
56
+ 'rk_live_[0-9a-zA-Z]{24,}' # Stripe restricted
57
+ 'SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}' # SendGrid
58
+ 'SK[0-9a-fA-F]{32}' # Twilio
59
+ 'npm_[A-Za-z0-9]{36}' # npm token
60
+ 'glpat-[A-Za-z0-9\-_]{20,}' # GitLab PAT
61
+ '-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----' # Private keys
62
+ )
63
+
64
+ for f in $FILTERED_FILES; do
65
+ # Get the staged version of the file (what would actually be committed)
66
+ CONTENT=$(git show ":$f" 2>/dev/null || true)
67
+ if [ -z "$CONTENT" ]; then
68
+ continue
69
+ fi
70
+
71
+ for pattern in "${PATTERNS[@]}"; do
72
+ MATCHES=$(echo "$CONTENT" | grep -nE "$pattern" 2>/dev/null || true)
73
+ if [ -n "$MATCHES" ]; then
74
+ while IFS= read -r match; do
75
+ LINE_NUM=$(echo "$match" | cut -d: -f1)
76
+ # Mask the secret value in output (show first 8 chars only)
77
+ MASKED=$(echo "$match" | cut -d: -f2- | sed -E 's/([A-Za-z0-9_\-]{8})[A-Za-z0-9_\-]{8,}/\1****/g')
78
+ FINDINGS="${FINDINGS}\n ${RED}BLOCKED${NC} $f:$LINE_NUM $MASKED"
79
+ FOUND=$((FOUND + 1))
80
+ done <<< "$MATCHES"
81
+ fi
82
+ done
83
+ done
84
+
85
+ # Also check: is a .env file being committed?
86
+ for f in $STAGED_FILES; do
87
+ case "$f" in
88
+ .env|.env.local|.env.production|.env.staging|.env.*.local)
89
+ FINDINGS="${FINDINGS}\n ${YELLOW}WARNING${NC} $f is staged — this file typically contains secrets and should be gitignored"
90
+ FOUND=$((FOUND + 1))
91
+ ;;
92
+ esac
93
+ done
94
+
95
+ if [ "$FOUND" -gt 0 ]; then
96
+ echo ""
97
+ echo -e "${RED}ftm-git: COMMIT BLOCKED — $FOUND secret(s) detected in staged files${NC}"
98
+ echo ""
99
+ echo -e "$FINDINGS"
100
+ echo ""
101
+ echo "To fix: run /ftm-git for auto-remediation, or manually:"
102
+ echo " 1. Move secrets to .env (gitignored)"
103
+ echo " 2. Replace hardcoded values with env var references"
104
+ echo " 3. Stage the cleaned files and commit again"
105
+ echo ""
106
+ echo "To bypass (NOT recommended): git commit --no-verify"
107
+ exit 1
108
+ fi
109
+
110
+ exit 0
package/ftm-git.yml CHANGED
@@ -1,2 +1,2 @@
1
- name: ftm-git
2
- 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.
1
+ name: ftm-git
2
+ 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.
@@ -1,64 +1,64 @@
1
- """
2
- Exponential-backoff retry decorator for HTTP adapter calls.
3
-
4
- Usage:
5
- @retry(max_attempts=3, base_delay=1.0)
6
- def poll(self) -> list[dict]:
7
- ...
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import functools
13
- import logging
14
- import time
15
- from typing import Callable, TypeVar
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
- F = TypeVar("F", bound=Callable)
20
-
21
-
22
- def retry(
23
- max_attempts: int = 3,
24
- base_delay: float = 1.0,
25
- backoff_factor: float = 2.0,
26
- exceptions: tuple[type[Exception], ...] = (Exception,),
27
- ) -> Callable[[F], F]:
28
- """
29
- Decorator: retry on exception with exponential backoff.
30
-
31
- Args:
32
- max_attempts: Total number of attempts (including the first).
33
- base_delay: Initial sleep duration in seconds.
34
- backoff_factor: Multiplier applied to delay after each failure.
35
- exceptions: Exception types that trigger a retry.
36
- """
37
-
38
- def decorator(func: F) -> F:
39
- @functools.wraps(func)
40
- def wrapper(*args, **kwargs):
41
- delay = base_delay
42
- last_exc: Exception | None = None
43
- for attempt in range(1, max_attempts + 1):
44
- try:
45
- return func(*args, **kwargs)
46
- except exceptions as exc:
47
- last_exc = exc
48
- if attempt == max_attempts:
49
- break
50
- logger.warning(
51
- "%s failed (attempt %d/%d): %s — retrying in %.1fs",
52
- func.__qualname__,
53
- attempt,
54
- max_attempts,
55
- exc,
56
- delay,
57
- )
58
- time.sleep(delay)
59
- delay *= backoff_factor
60
- raise last_exc # type: ignore[misc]
61
-
62
- return wrapper # type: ignore[return-value]
63
-
64
- return decorator
1
+ """
2
+ Exponential-backoff retry decorator for HTTP adapter calls.
3
+
4
+ Usage:
5
+ @retry(max_attempts=3, base_delay=1.0)
6
+ def poll(self) -> list[dict]:
7
+ ...
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import functools
13
+ import logging
14
+ import time
15
+ from typing import Callable, TypeVar
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ F = TypeVar("F", bound=Callable)
20
+
21
+
22
+ def retry(
23
+ max_attempts: int = 3,
24
+ base_delay: float = 1.0,
25
+ backoff_factor: float = 2.0,
26
+ exceptions: tuple[type[Exception], ...] = (Exception,),
27
+ ) -> Callable[[F], F]:
28
+ """
29
+ Decorator: retry on exception with exponential backoff.
30
+
31
+ Args:
32
+ max_attempts: Total number of attempts (including the first).
33
+ base_delay: Initial sleep duration in seconds.
34
+ backoff_factor: Multiplier applied to delay after each failure.
35
+ exceptions: Exception types that trigger a retry.
36
+ """
37
+
38
+ def decorator(func: F) -> F:
39
+ @functools.wraps(func)
40
+ def wrapper(*args, **kwargs):
41
+ delay = base_delay
42
+ last_exc: Exception | None = None
43
+ for attempt in range(1, max_attempts + 1):
44
+ try:
45
+ return func(*args, **kwargs)
46
+ except exceptions as exc:
47
+ last_exc = exc
48
+ if attempt == max_attempts:
49
+ break
50
+ logger.warning(
51
+ "%s failed (attempt %d/%d): %s — retrying in %.1fs",
52
+ func.__qualname__,
53
+ attempt,
54
+ max_attempts,
55
+ exc,
56
+ delay,
57
+ )
58
+ time.sleep(delay)
59
+ delay *= backoff_factor
60
+ raise last_exc # type: ignore[misc]
61
+
62
+ return wrapper # type: ignore[return-value]
63
+
64
+ return decorator