feed-the-machine 1.6.1 → 1.7.1

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 (272) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +262 -170
  3. package/bin/__pycache__/tasks_db.cpython-314.pyc +0 -0
  4. package/bin/brain.py +1340 -0
  5. package/bin/convert_claude_skills_to_codex.py +490 -0
  6. package/bin/generate-manifest.mjs +463 -463
  7. package/bin/harden_codex_skills.py +141 -0
  8. package/bin/install.mjs +491 -491
  9. package/bin/migrate-eng-buddy-data.py +875 -0
  10. package/bin/playbook_engine/__init__.py +1 -0
  11. package/bin/playbook_engine/conftest.py +8 -0
  12. package/bin/playbook_engine/extractor.py +33 -0
  13. package/bin/playbook_engine/manager.py +102 -0
  14. package/bin/playbook_engine/models.py +84 -0
  15. package/bin/playbook_engine/registry.py +35 -0
  16. package/bin/playbook_engine/test_extractor.py +72 -0
  17. package/bin/playbook_engine/test_integration.py +129 -0
  18. package/bin/playbook_engine/test_manager.py +85 -0
  19. package/bin/playbook_engine/test_models.py +166 -0
  20. package/bin/playbook_engine/test_registry.py +67 -0
  21. package/bin/playbook_engine/test_tracer.py +86 -0
  22. package/bin/playbook_engine/tracer.py +93 -0
  23. package/bin/tasks_db.py +456 -0
  24. package/docs/HOOKS.md +243 -243
  25. package/docs/INBOX.md +233 -233
  26. package/ftm/SKILL.md +125 -122
  27. package/ftm-audit/SKILL.md +673 -623
  28. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
  29. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
  30. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
  31. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
  32. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
  33. package/ftm-audit/scripts/run-knip.sh +23 -23
  34. package/ftm-audit.yml +2 -2
  35. package/ftm-brainstorm/SKILL.md +1003 -498
  36. package/ftm-brainstorm/evals/evals.json +180 -100
  37. package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
  38. package/ftm-brainstorm/references/agent-prompts.md +552 -224
  39. package/ftm-brainstorm/references/plan-template.md +209 -121
  40. package/ftm-brainstorm.yml +2 -2
  41. package/ftm-browse/SKILL.md +454 -454
  42. package/ftm-browse/daemon/browser-manager.ts +206 -206
  43. package/ftm-browse/daemon/bun.lock +30 -30
  44. package/ftm-browse/daemon/cli.ts +347 -347
  45. package/ftm-browse/daemon/commands.ts +410 -410
  46. package/ftm-browse/daemon/main.ts +357 -357
  47. package/ftm-browse/daemon/package.json +17 -17
  48. package/ftm-browse/daemon/server.ts +189 -189
  49. package/ftm-browse/daemon/snapshot.ts +519 -519
  50. package/ftm-browse/daemon/tsconfig.json +22 -22
  51. package/ftm-browse.yml +4 -4
  52. package/ftm-capture/SKILL.md +370 -370
  53. package/ftm-capture.yml +4 -4
  54. package/ftm-codex-gate/SKILL.md +361 -361
  55. package/ftm-codex-gate.yml +2 -2
  56. package/ftm-config/SKILL.md +422 -345
  57. package/ftm-config.default.yml +125 -82
  58. package/ftm-config.yml +44 -2
  59. package/ftm-council/SKILL.md +416 -416
  60. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
  61. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
  62. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
  63. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
  64. package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
  65. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
  66. package/ftm-council-chat.yml +2 -0
  67. package/ftm-council.yml +2 -2
  68. package/ftm-dashboard/SKILL.md +163 -163
  69. package/ftm-dashboard.yml +4 -4
  70. package/ftm-debug/SKILL.md +1037 -1037
  71. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
  72. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
  73. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
  74. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
  75. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
  76. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
  77. package/ftm-debug.yml +2 -2
  78. package/ftm-diagram/SKILL.md +277 -277
  79. package/ftm-diagram.yml +2 -2
  80. package/ftm-executor/SKILL.md +777 -777
  81. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
  82. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
  83. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
  84. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
  85. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +81 -72
  86. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
  87. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
  88. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
  89. package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
  90. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
  91. package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
  92. package/ftm-executor/runtime/package.json +8 -8
  93. package/ftm-executor.yml +2 -2
  94. package/ftm-git/SKILL.md +441 -441
  95. package/ftm-git/evals/evals.json +26 -26
  96. package/ftm-git/evals/promptfoo.yaml +75 -75
  97. package/ftm-git/hooks/post-commit-experience.sh +92 -92
  98. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
  99. package/ftm-git/references/protocols/REMEDIATION.md +139 -139
  100. package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
  101. package/ftm-git.yml +2 -2
  102. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  103. package/ftm-inbox/backend/adapters/_retry.py +64 -64
  104. package/ftm-inbox/backend/adapters/base.py +230 -230
  105. package/ftm-inbox/backend/adapters/freshservice.py +104 -104
  106. package/ftm-inbox/backend/adapters/gmail.py +125 -125
  107. package/ftm-inbox/backend/adapters/jira.py +136 -136
  108. package/ftm-inbox/backend/adapters/registry.py +192 -192
  109. package/ftm-inbox/backend/adapters/slack.py +110 -110
  110. package/ftm-inbox/backend/db/connection.py +54 -54
  111. package/ftm-inbox/backend/db/schema.py +78 -78
  112. package/ftm-inbox/backend/executor/__init__.py +7 -7
  113. package/ftm-inbox/backend/executor/engine.py +149 -149
  114. package/ftm-inbox/backend/executor/step_runner.py +98 -98
  115. package/ftm-inbox/backend/main.py +103 -103
  116. package/ftm-inbox/backend/models/__init__.py +1 -1
  117. package/ftm-inbox/backend/models/unified_task.py +36 -36
  118. package/ftm-inbox/backend/planner/__init__.py +6 -6
  119. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  120. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  121. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  122. package/ftm-inbox/backend/planner/generator.py +127 -127
  123. package/ftm-inbox/backend/planner/schema.py +34 -34
  124. package/ftm-inbox/backend/requirements.txt +5 -5
  125. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  126. package/ftm-inbox/backend/routes/execute.py +186 -186
  127. package/ftm-inbox/backend/routes/health.py +52 -52
  128. package/ftm-inbox/backend/routes/inbox.py +68 -68
  129. package/ftm-inbox/backend/routes/plan.py +271 -271
  130. package/ftm-inbox/bin/launchagent.mjs +91 -91
  131. package/ftm-inbox/bin/setup.mjs +188 -188
  132. package/ftm-inbox/bin/start.sh +10 -10
  133. package/ftm-inbox/bin/status.sh +17 -17
  134. package/ftm-inbox/bin/stop.sh +8 -8
  135. package/ftm-inbox/config.example.yml +55 -55
  136. package/ftm-inbox/package-lock.json +2898 -2898
  137. package/ftm-inbox/package.json +26 -26
  138. package/ftm-inbox/postcss.config.js +6 -6
  139. package/ftm-inbox/src/app.css +199 -199
  140. package/ftm-inbox/src/app.html +18 -18
  141. package/ftm-inbox/src/lib/api.ts +166 -166
  142. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
  143. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
  144. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
  145. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
  146. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
  147. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
  148. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
  149. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
  150. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
  151. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
  152. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
  153. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
  154. package/ftm-inbox/src/lib/theme.ts +47 -47
  155. package/ftm-inbox/src/routes/+layout.svelte +76 -76
  156. package/ftm-inbox/src/routes/+page.svelte +401 -401
  157. package/ftm-inbox/svelte.config.js +12 -12
  158. package/ftm-inbox/tailwind.config.ts +63 -63
  159. package/ftm-inbox/tsconfig.json +13 -13
  160. package/ftm-inbox/vite.config.ts +6 -6
  161. package/ftm-intent/SKILL.md +241 -241
  162. package/ftm-intent.yml +2 -2
  163. package/ftm-manifest.json +3794 -3794
  164. package/ftm-map/SKILL.md +291 -291
  165. package/ftm-map/scripts/db.py +712 -712
  166. package/ftm-map/scripts/index.py +415 -415
  167. package/ftm-map/scripts/parser.py +224 -224
  168. package/ftm-map/scripts/queries/go-tags.scm +20 -20
  169. package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
  170. package/ftm-map/scripts/queries/python-tags.scm +31 -31
  171. package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
  172. package/ftm-map/scripts/queries/rust-tags.scm +37 -37
  173. package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
  174. package/ftm-map/scripts/query.py +301 -301
  175. package/ftm-map/scripts/ranker.py +377 -377
  176. package/ftm-map/scripts/requirements.txt +5 -5
  177. package/ftm-map/scripts/setup-hooks.sh +27 -27
  178. package/ftm-map/scripts/setup.sh +56 -56
  179. package/ftm-map/scripts/test_db.py +364 -364
  180. package/ftm-map/scripts/test_parser.py +174 -174
  181. package/ftm-map/scripts/test_query.py +183 -183
  182. package/ftm-map/scripts/test_ranker.py +199 -199
  183. package/ftm-map/scripts/views.py +591 -591
  184. package/ftm-map.yml +2 -2
  185. package/ftm-mind/SKILL.md +201 -1943
  186. package/ftm-mind/evals/promptfoo.yaml +142 -142
  187. package/ftm-mind/references/blackboard-protocol.md +110 -0
  188. package/ftm-mind/references/blackboard-schema.md +328 -328
  189. package/ftm-mind/references/complexity-guide.md +110 -110
  190. package/ftm-mind/references/complexity-sizing.md +138 -0
  191. package/ftm-mind/references/decide-act-protocol.md +172 -0
  192. package/ftm-mind/references/direct-execution.md +51 -0
  193. package/ftm-mind/references/environment-discovery.md +77 -0
  194. package/ftm-mind/references/event-registry.md +319 -319
  195. package/ftm-mind/references/mcp-inventory.md +300 -296
  196. package/ftm-mind/references/ops-routing.md +47 -0
  197. package/ftm-mind/references/orient-protocol.md +234 -0
  198. package/ftm-mind/references/personality.md +40 -0
  199. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
  200. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
  201. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
  202. package/ftm-mind/references/reflexion-protocol.md +249 -249
  203. package/ftm-mind/references/routing/SCENARIOS.md +22 -22
  204. package/ftm-mind/references/routing-scenarios.md +35 -35
  205. package/ftm-mind.yml +2 -2
  206. package/ftm-ops.yml +4 -0
  207. package/ftm-pause/SKILL.md +395 -395
  208. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
  209. package/ftm-pause/references/protocols/VALIDATION.md +80 -80
  210. package/ftm-pause.yml +2 -2
  211. package/ftm-researcher/SKILL.md +275 -275
  212. package/ftm-researcher/evals/agent-diversity.yaml +17 -17
  213. package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
  214. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
  215. package/ftm-researcher/references/adaptive-search.md +116 -116
  216. package/ftm-researcher/references/agent-prompts.md +193 -193
  217. package/ftm-researcher/references/council-integration.md +193 -193
  218. package/ftm-researcher/references/output-format.md +203 -203
  219. package/ftm-researcher/references/synthesis-pipeline.md +165 -165
  220. package/ftm-researcher/scripts/score_credibility.py +234 -234
  221. package/ftm-researcher/scripts/validate_research.py +92 -92
  222. package/ftm-researcher.yml +2 -2
  223. package/ftm-resume/SKILL.md +518 -518
  224. package/ftm-resume/references/protocols/VALIDATION.md +172 -172
  225. package/ftm-resume.yml +2 -2
  226. package/ftm-retro/SKILL.md +380 -380
  227. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
  228. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
  229. package/ftm-retro.yml +2 -2
  230. package/ftm-routine/SKILL.md +170 -170
  231. package/ftm-routine.yml +4 -4
  232. package/ftm-state/blackboard/capabilities.json +5 -5
  233. package/ftm-state/blackboard/capabilities.schema.json +27 -27
  234. package/ftm-state/blackboard/context.json +37 -23
  235. package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
  236. package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
  237. package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
  238. package/ftm-state/blackboard/experiences/index.json +58 -9
  239. package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
  240. package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
  241. package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
  242. package/ftm-state/blackboard/patterns.json +6 -6
  243. package/ftm-state/schemas/context.schema.json +130 -130
  244. package/ftm-state/schemas/experience-index.schema.json +77 -77
  245. package/ftm-state/schemas/experience.schema.json +78 -78
  246. package/ftm-state/schemas/patterns.schema.json +44 -44
  247. package/ftm-upgrade/SKILL.md +194 -194
  248. package/ftm-upgrade/scripts/check-version.sh +76 -76
  249. package/ftm-upgrade/scripts/upgrade.sh +143 -143
  250. package/ftm-upgrade.yml +2 -2
  251. package/ftm-verify.yml +2 -2
  252. package/ftm.yml +2 -2
  253. package/hooks/ftm-auto-log.sh +137 -0
  254. package/hooks/ftm-blackboard-enforcer.sh +93 -93
  255. package/hooks/ftm-discovery-reminder.sh +90 -90
  256. package/hooks/ftm-drafts-gate.sh +61 -61
  257. package/hooks/ftm-event-logger.mjs +107 -107
  258. package/hooks/ftm-install-hooks.sh +240 -0
  259. package/hooks/ftm-learning-capture.sh +117 -0
  260. package/hooks/ftm-map-autodetect.sh +79 -79
  261. package/hooks/ftm-pending-sync-check.sh +22 -22
  262. package/hooks/ftm-plan-gate.sh +92 -92
  263. package/hooks/ftm-post-commit-trigger.sh +57 -57
  264. package/hooks/ftm-post-compaction.sh +138 -0
  265. package/hooks/ftm-pre-compaction.sh +147 -0
  266. package/hooks/ftm-session-end.sh +52 -0
  267. package/hooks/ftm-session-snapshot.sh +213 -0
  268. package/hooks/ftm-task-loader.sh +100 -0
  269. package/hooks/settings-template.json +91 -81
  270. package/install.sh +363 -363
  271. package/package.json +84 -84
  272. package/uninstall.sh +25 -25
@@ -0,0 +1,166 @@
1
+ import pytest
2
+ import yaml
3
+ import tempfile
4
+ import os
5
+ from models import Playbook, PlaybookStep, ActionBinding, ParamSource
6
+
7
+ def test_playbook_from_yaml():
8
+ raw = {
9
+ "id": "sso-onboarding",
10
+ "name": "SSO Onboarding",
11
+ "version": 1,
12
+ "confidence": "low",
13
+ "trigger_patterns": [
14
+ {"ticket_type": "Service Request", "keywords": ["SSO", "SAML"], "source": ["freshservice"]}
15
+ ],
16
+ "created_from": "session",
17
+ "executions": 0,
18
+ "steps": [
19
+ {
20
+ "id": 1,
21
+ "name": "Create Jira ticket",
22
+ "action": {
23
+ "tool": "mcp__mcp-atlassian__jira_create_issue",
24
+ "params": {"project": "ITWORK2", "summary": "[SSO] {{app_name}}"},
25
+ "param_sources": {"app_name": {"from": "trigger_ticket", "field": "subject", "extract": "app name"}}
26
+ },
27
+ "auth_required": False,
28
+ "human_required": False,
29
+ }
30
+ ],
31
+ }
32
+ pb = Playbook.from_dict(raw)
33
+ assert pb.id == "sso-onboarding"
34
+ assert pb.confidence == "low"
35
+ assert len(pb.steps) == 1
36
+ assert pb.steps[0].action.tool == "mcp__mcp-atlassian__jira_create_issue"
37
+ assert pb.steps[0].action.param_sources["app_name"].field == "subject"
38
+
39
+ def test_playbook_round_trip_yaml(tmp_path):
40
+ raw = {
41
+ "id": "test-pb",
42
+ "name": "Test Playbook",
43
+ "version": 1,
44
+ "confidence": "medium",
45
+ "trigger_patterns": [],
46
+ "created_from": "dictated",
47
+ "executions": 0,
48
+ "steps": [],
49
+ }
50
+ pb = Playbook.from_dict(raw)
51
+ path = tmp_path / "test-pb.yml"
52
+ pb.save(str(path))
53
+ loaded = Playbook.load(str(path))
54
+ assert loaded.id == pb.id
55
+ assert loaded.version == pb.version
56
+
57
+ def test_playbook_matches_ticket():
58
+ raw = {
59
+ "id": "sso",
60
+ "name": "SSO",
61
+ "version": 1,
62
+ "confidence": "high",
63
+ "trigger_patterns": [
64
+ {"ticket_type": "Service Request", "keywords": ["SSO", "SAML"], "source": ["freshservice"]}
65
+ ],
66
+ "created_from": "session",
67
+ "executions": 3,
68
+ "steps": [],
69
+ }
70
+ pb = Playbook.from_dict(raw)
71
+ assert pb.matches(ticket_type="Service Request", text="Set up SSO for Linear", source="freshservice")
72
+ assert not pb.matches(ticket_type="Incident", text="Server is down", source="freshservice")
73
+ assert not pb.matches(ticket_type="Service Request", text="New laptop request", source="freshservice")
74
+
75
+ def test_confidence_progression():
76
+ raw = {
77
+ "id": "t",
78
+ "name": "T",
79
+ "version": 1,
80
+ "confidence": "low",
81
+ "trigger_patterns": [],
82
+ "created_from": "session",
83
+ "executions": 0,
84
+ "steps": [],
85
+ }
86
+ pb = Playbook.from_dict(raw)
87
+ pb.record_execution(success=True)
88
+ assert pb.confidence == "medium"
89
+ assert pb.executions == 1
90
+ pb.record_execution(success=True)
91
+ pb.record_execution(success=True)
92
+ assert pb.confidence == "high"
93
+ assert pb.executions == 3
94
+
95
+ def test_confidence_degradation_on_failure():
96
+ raw = {
97
+ "id": "t",
98
+ "name": "T",
99
+ "version": 1,
100
+ "confidence": "high",
101
+ "trigger_patterns": [],
102
+ "created_from": "session",
103
+ "executions": 5,
104
+ "steps": [],
105
+ }
106
+ pb = Playbook.from_dict(raw)
107
+ pb.record_execution(success=False)
108
+ assert pb.confidence == "medium"
109
+ pb.record_execution(success=False)
110
+ assert pb.confidence == "low"
111
+ # Already at low — should stay at low
112
+ pb.record_execution(success=False)
113
+ assert pb.confidence == "low"
114
+
115
+ def test_empty_trigger_patterns_match_everything():
116
+ """Playbook with no trigger patterns should NOT match any ticket."""
117
+ raw = {
118
+ "id": "t",
119
+ "name": "T",
120
+ "version": 1,
121
+ "confidence": "low",
122
+ "trigger_patterns": [],
123
+ "created_from": "session",
124
+ "executions": 0,
125
+ "steps": [],
126
+ }
127
+ pb = Playbook.from_dict(raw)
128
+ # Empty trigger list means no patterns match (any() on empty returns False)
129
+ assert not pb.matches(text="anything", source="freshservice")
130
+
131
+ def test_to_dict_from_dict_round_trip():
132
+ """Verify full round-trip serialization symmetry."""
133
+ raw = {
134
+ "id": "round-trip",
135
+ "name": "Round Trip Test",
136
+ "version": 2,
137
+ "confidence": "medium",
138
+ "trigger_patterns": [
139
+ {"ticket_type": "Service Request", "keywords": ["SSO"], "source": ["freshservice"]}
140
+ ],
141
+ "created_from": "dictated",
142
+ "executions": 5,
143
+ "steps": [
144
+ {
145
+ "id": 1,
146
+ "name": "Do something",
147
+ "action": {
148
+ "tool": "test_tool",
149
+ "params": {"key": "value"},
150
+ "param_sources": {"key": {"from": "trigger_ticket", "field": "subject"}},
151
+ },
152
+ "auth_required": True,
153
+ "auth_method": "stored_session",
154
+ "human_required": False,
155
+ "optional": True,
156
+ }
157
+ ],
158
+ "last_executed": "2026-01-01T00:00:00Z",
159
+ "last_updated": "2026-01-02T00:00:00Z",
160
+ "update_history": [{"version": 2, "reason": "test"}],
161
+ }
162
+ pb = Playbook.from_dict(raw)
163
+ d = pb.to_dict()
164
+ pb2 = Playbook.from_dict(d)
165
+ d2 = pb2.to_dict()
166
+ assert d == d2
@@ -0,0 +1,67 @@
1
+ import pytest
2
+ import yaml
3
+ import tempfile
4
+ import os
5
+ from registry import ToolRegistry
6
+
7
+ def test_load_registry(tmp_path):
8
+ reg_dir = tmp_path / "tool-registry"
9
+ reg_dir.mkdir()
10
+
11
+ (reg_dir / "_registry.yml").write_text(yaml.dump({
12
+ "tools": {
13
+ "jira": {
14
+ "type": "mcp",
15
+ "prefix": "mcp__mcp-atlassian__jira_",
16
+ "capabilities": ["create_issue"],
17
+ "auth": "persistent",
18
+ "domains": ["ticket_management"],
19
+ }
20
+ }
21
+ }))
22
+
23
+ (reg_dir / "jira.defaults.yml").write_text(yaml.dump({
24
+ "create_issue": {"assignee": "test@test.com", "board_id": 70},
25
+ "field_mappings": {"sprint_field": "customfield_10020"},
26
+ }))
27
+
28
+ reg = ToolRegistry(str(reg_dir))
29
+ assert "jira" in reg.tools
30
+ assert reg.tools["jira"]["type"] == "mcp"
31
+ assert reg.get_defaults("jira", "create_issue")["assignee"] == "test@test.com"
32
+
33
+ def test_get_defaults_missing_tool(tmp_path):
34
+ reg_dir = tmp_path / "tool-registry"
35
+ reg_dir.mkdir()
36
+ (reg_dir / "_registry.yml").write_text(yaml.dump({"tools": {}}))
37
+ reg = ToolRegistry(str(reg_dir))
38
+ assert reg.get_defaults("nonexistent", "action") == {}
39
+
40
+ def test_merge_params(tmp_path):
41
+ reg_dir = tmp_path / "tool-registry"
42
+ reg_dir.mkdir()
43
+ (reg_dir / "_registry.yml").write_text(yaml.dump({
44
+ "tools": {"jira": {"type": "mcp", "prefix": "p_", "capabilities": [], "auth": "persistent", "domains": []}}
45
+ }))
46
+ (reg_dir / "jira.defaults.yml").write_text(yaml.dump({
47
+ "create_issue": {"assignee": "default@test.com", "board_id": 70},
48
+ }))
49
+ reg = ToolRegistry(str(reg_dir))
50
+ merged = reg.merge_params("jira", "create_issue", {"summary": "Test", "assignee": "override@test.com"})
51
+ assert merged["assignee"] == "override@test.com" # playbook overrides default
52
+ assert merged["board_id"] == 70 # default fills in
53
+ assert merged["summary"] == "Test" # playbook-specific preserved
54
+
55
+ def test_resolve_tool_from_mcp_name(tmp_path):
56
+ reg_dir = tmp_path / "tool-registry"
57
+ reg_dir.mkdir()
58
+ (reg_dir / "_registry.yml").write_text(yaml.dump({
59
+ "tools": {
60
+ "jira": {"type": "mcp", "prefix": "mcp__mcp-atlassian__jira_", "capabilities": [], "auth": "persistent", "domains": []},
61
+ "slack": {"type": "mcp", "prefix": "mcp__slack__", "capabilities": [], "auth": "persistent", "domains": []},
62
+ }
63
+ }))
64
+ reg = ToolRegistry(str(reg_dir))
65
+ assert reg.resolve_tool_name("mcp__mcp-atlassian__jira_create_issue") == ("jira", "create_issue")
66
+ assert reg.resolve_tool_name("mcp__slack__post_message") == ("slack", "post_message")
67
+ assert reg.resolve_tool_name("unknown_tool") == (None, None)
@@ -0,0 +1,86 @@
1
+ import pytest
2
+ import json
3
+ import os
4
+ from tracer import WorkflowTracer, TraceEvent
5
+
6
+ def test_add_tool_call_event(tmp_path):
7
+ tracer = WorkflowTracer(traces_dir=str(tmp_path))
8
+ tracer.start_trace("ITWORK2-1234")
9
+ tracer.add_event(TraceEvent(
10
+ type="tool_call",
11
+ tool="mcp__mcp-atlassian__jira_create_issue",
12
+ params={"project": "ITWORK2", "summary": "Test"},
13
+ ))
14
+ trace = tracer.get_trace("ITWORK2-1234")
15
+ assert len(trace["events"]) == 1
16
+ assert trace["events"][0]["type"] == "tool_call"
17
+
18
+ def test_add_user_instruction_event(tmp_path):
19
+ tracer = WorkflowTracer(traces_dir=str(tmp_path))
20
+ tracer.start_trace("ITWORK2-1234")
21
+ tracer.add_event(TraceEvent(
22
+ type="user_instruction",
23
+ content="Always set due date to 30 days out",
24
+ applies_to=["jira.create_issue"],
25
+ persist=True,
26
+ ))
27
+ trace = tracer.get_trace("ITWORK2-1234")
28
+ assert trace["events"][0]["type"] == "user_instruction"
29
+ assert trace["events"][0]["persist"] is True
30
+
31
+ def test_add_user_correction_event(tmp_path):
32
+ tracer = WorkflowTracer(traces_dir=str(tmp_path))
33
+ tracer.start_trace("ITWORK2-1234")
34
+ tracer.add_event(TraceEvent(
35
+ type="user_correction",
36
+ content="No, assign to next sprint",
37
+ corrects="tool_defaults.jira.create_issue.sprint",
38
+ new_value="next",
39
+ ))
40
+ trace = tracer.get_trace("ITWORK2-1234")
41
+ assert trace["events"][0]["corrects"] == "tool_defaults.jira.create_issue.sprint"
42
+
43
+ def test_add_manual_action_event(tmp_path):
44
+ tracer = WorkflowTracer(traces_dir=str(tmp_path))
45
+ tracer.start_trace("ITWORK2-1234")
46
+ tracer.add_event(TraceEvent(
47
+ type="user_manual_action",
48
+ content="I configured SAML in Okta",
49
+ inferred_step="Configure SAML in IdP",
50
+ action_binding="playwright",
51
+ auth_note="needs Okta admin",
52
+ ))
53
+ trace = tracer.get_trace("ITWORK2-1234")
54
+ assert trace["events"][0]["action_binding"] == "playwright"
55
+
56
+ def test_trace_persists_to_disk(tmp_path):
57
+ tracer = WorkflowTracer(traces_dir=str(tmp_path))
58
+ tracer.start_trace("ITWORK2-5678")
59
+ tracer.add_event(TraceEvent(type="tool_call", tool="test_tool"))
60
+ tracer.flush("ITWORK2-5678")
61
+ path = tmp_path / "active" / "ITWORK2-5678.json"
62
+ assert path.exists()
63
+ with open(path) as f:
64
+ data = json.load(f)
65
+ assert len(data["events"]) == 1
66
+
67
+ def test_similarity_score(tmp_path):
68
+ tracer = WorkflowTracer(traces_dir=str(tmp_path))
69
+ tracer.start_trace("t1")
70
+ tracer.add_event(TraceEvent(type="tool_call", tool="jira_create"))
71
+ tracer.add_event(TraceEvent(type="tool_call", tool="slack_post"))
72
+ tracer.add_event(TraceEvent(type="tool_call", tool="freshservice_update"))
73
+
74
+ tracer.start_trace("t2")
75
+ tracer.add_event(TraceEvent(type="tool_call", tool="jira_create"))
76
+ tracer.add_event(TraceEvent(type="tool_call", tool="slack_post"))
77
+ tracer.add_event(TraceEvent(type="tool_call", tool="freshservice_update"))
78
+
79
+ score = tracer.similarity("t1", "t2")
80
+ assert score >= 0.9 # nearly identical tool sequences
81
+
82
+ def test_from_dict_ignores_unknown_keys(tmp_path):
83
+ """TraceEvent.from_dict should ignore unknown keys gracefully."""
84
+ event = TraceEvent.from_dict({"type": "tool_call", "tool": "test", "unknown_field": "ignored"})
85
+ assert event.type == "tool_call"
86
+ assert event.tool == "test"
@@ -0,0 +1,93 @@
1
+ """WorkflowTracer - captures tool call traces for playbook extraction."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from dataclasses import dataclass, field, asdict
7
+ from typing import List, Optional
8
+
9
+
10
+ @dataclass
11
+ class TraceEvent:
12
+ """A single event in a workflow trace."""
13
+ timestamp: float = field(default_factory=time.time)
14
+ tool: str = ""
15
+ action: str = ""
16
+ params: dict = field(default_factory=dict)
17
+ result_summary: str = ""
18
+ human_instruction: str = ""
19
+ correction: str = ""
20
+
21
+ def to_dict(self) -> dict:
22
+ return asdict(self)
23
+
24
+ @classmethod
25
+ def from_dict(cls, d: dict) -> "TraceEvent":
26
+ return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
27
+
28
+
29
+ @dataclass
30
+ class Trace:
31
+ """A complete workflow trace."""
32
+ id: str = ""
33
+ events: List[TraceEvent] = field(default_factory=list)
34
+ started: float = field(default_factory=time.time)
35
+ completed: float = 0.0
36
+
37
+ def to_dict(self) -> dict:
38
+ return {
39
+ "id": self.id,
40
+ "events": [e.to_dict() for e in self.events],
41
+ "started": self.started,
42
+ "completed": self.completed,
43
+ }
44
+
45
+ @classmethod
46
+ def from_dict(cls, d: dict) -> "Trace":
47
+ return cls(
48
+ id=d["id"],
49
+ events=[TraceEvent.from_dict(e) for e in d.get("events", [])],
50
+ started=d.get("started", 0),
51
+ completed=d.get("completed", 0),
52
+ )
53
+
54
+
55
+ class WorkflowTracer:
56
+ """Manages workflow traces on disk."""
57
+
58
+ def __init__(self, traces_dir: str):
59
+ self.traces_dir = traces_dir
60
+ os.makedirs(traces_dir, exist_ok=True)
61
+ self._traces: dict = {}
62
+
63
+ def _path(self, trace_id: str) -> str:
64
+ return os.path.join(self.traces_dir, f"{trace_id}.json")
65
+
66
+ def start_trace(self, trace_id: str) -> Trace:
67
+ trace = Trace(id=trace_id)
68
+ self._traces[trace_id] = trace
69
+ return trace
70
+
71
+ def load_trace(self, trace_id: str) -> Optional[Trace]:
72
+ if trace_id in self._traces:
73
+ return self._traces[trace_id]
74
+ path = self._path(trace_id)
75
+ if not os.path.exists(path):
76
+ return None
77
+ with open(path) as fh:
78
+ trace = Trace.from_dict(json.load(fh))
79
+ self._traces[trace_id] = trace
80
+ return trace
81
+
82
+ def add_event(self, event: TraceEvent):
83
+ # Add to the most recent trace
84
+ for trace in reversed(list(self._traces.values())):
85
+ trace.events.append(event)
86
+ return
87
+
88
+ def flush(self, trace_id: str):
89
+ trace = self._traces.get(trace_id)
90
+ if not trace:
91
+ return
92
+ with open(self._path(trace_id), "w") as fh:
93
+ json.dump(trace.to_dict(), fh, indent=2)