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 @@
1
+ # playbook_engine - Reusable workflow playbooks for eng-buddy
@@ -0,0 +1,8 @@
1
+ """Configure sys.path so tests can run from project root or module directory."""
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ # Add playbook_engine directory to sys.path so `from models import ...` works
6
+ _this_dir = Path(__file__).parent
7
+ if str(_this_dir) not in sys.path:
8
+ sys.path.insert(0, str(_this_dir))
@@ -0,0 +1,33 @@
1
+ """PlaybookExtractor - extracts playbooks from completed traces."""
2
+
3
+ from .models import Playbook, PlaybookStep
4
+ from .registry import ToolRegistry
5
+ from .tracer import Trace
6
+
7
+
8
+ class PlaybookExtractor:
9
+ """Extracts a draft playbook from a workflow trace."""
10
+
11
+ def __init__(self, registry: ToolRegistry):
12
+ self.registry = registry
13
+
14
+ def extract_from_trace(self, trace: Trace, name: str = "Untitled") -> Playbook:
15
+ steps = []
16
+ for i, event in enumerate(trace.events, 1):
17
+ step = PlaybookStep(
18
+ number=i,
19
+ description=event.human_instruction or event.action or f"Step {i}",
20
+ tool=event.tool,
21
+ tool_params=event.params,
22
+ requires_human=bool(event.correction),
23
+ notes=event.correction or "",
24
+ )
25
+ steps.append(step)
26
+
27
+ return Playbook(
28
+ name=name,
29
+ description=f"Extracted from trace {trace.id}",
30
+ steps=steps,
31
+ source="extracted",
32
+ confidence=0.7, # draft confidence
33
+ )
@@ -0,0 +1,102 @@
1
+ """PlaybookManager - CRUD and matching for playbooks."""
2
+
3
+ import json
4
+ import os
5
+ from typing import List, Optional
6
+
7
+ from .models import Playbook
8
+
9
+
10
+ class PlaybookManager:
11
+ """Manages approved and draft playbooks on disk as JSON files."""
12
+
13
+ def __init__(self, playbooks_dir: str):
14
+ self.playbooks_dir = playbooks_dir
15
+ self.approved_dir = playbooks_dir # approved live at root
16
+ self.drafts_dir = os.path.join(playbooks_dir, "drafts")
17
+ self.archive_dir = os.path.join(playbooks_dir, "archive")
18
+ os.makedirs(self.approved_dir, exist_ok=True)
19
+ os.makedirs(self.drafts_dir, exist_ok=True)
20
+ os.makedirs(self.archive_dir, exist_ok=True)
21
+
22
+ # --- Read ---
23
+
24
+ def _load_from_dir(self, directory: str) -> List[Playbook]:
25
+ playbooks = []
26
+ for f in sorted(os.listdir(directory)):
27
+ if not f.endswith(".json"):
28
+ continue
29
+ path = os.path.join(directory, f)
30
+ try:
31
+ with open(path) as fh:
32
+ playbooks.append(Playbook.from_dict(json.load(fh)))
33
+ except (json.JSONDecodeError, KeyError):
34
+ continue
35
+ return playbooks
36
+
37
+ def list_playbooks(self) -> List[Playbook]:
38
+ return self._load_from_dir(self.approved_dir)
39
+
40
+ def list_drafts(self) -> List[Playbook]:
41
+ return self._load_from_dir(self.drafts_dir)
42
+
43
+ def get(self, playbook_id: str) -> Optional[Playbook]:
44
+ for pb in self.list_playbooks():
45
+ if pb.id == playbook_id:
46
+ return pb
47
+ return None
48
+
49
+ def get_draft(self, playbook_id: str) -> Optional[Playbook]:
50
+ for pb in self.list_drafts():
51
+ if pb.id == playbook_id:
52
+ return pb
53
+ return None
54
+
55
+ # --- Write ---
56
+
57
+ def _save(self, pb: Playbook, directory: str) -> str:
58
+ path = os.path.join(directory, f"{pb.id}.json")
59
+ with open(path, "w") as fh:
60
+ json.dump(pb.to_dict(), fh, indent=2)
61
+ return path
62
+
63
+ def save_playbook(self, pb: Playbook) -> str:
64
+ return self._save(pb, self.approved_dir)
65
+
66
+ def save_draft(self, pb: Playbook) -> str:
67
+ return self._save(pb, self.drafts_dir)
68
+
69
+ def promote_draft(self, playbook_id: str) -> Optional[Playbook]:
70
+ pb = self.get_draft(playbook_id)
71
+ if not pb:
72
+ return None
73
+ # Move from drafts to approved
74
+ draft_path = os.path.join(self.drafts_dir, f"{pb.id}.json")
75
+ self.save_playbook(pb)
76
+ if os.path.exists(draft_path):
77
+ os.remove(draft_path)
78
+ return pb
79
+
80
+ def delete_draft(self, playbook_id: str) -> bool:
81
+ path = os.path.join(self.drafts_dir, f"{playbook_id}.json")
82
+ if os.path.exists(path):
83
+ os.remove(path)
84
+ return True
85
+ return False
86
+
87
+ # --- Matching ---
88
+
89
+ def match_ticket(self, ticket_type: str = "", text: str = "", source: str = "") -> List[Playbook]:
90
+ """Find playbooks whose trigger_keywords match the given text."""
91
+ text_lower = text.lower()
92
+ matches = []
93
+ for pb in self.list_playbooks():
94
+ score = 0
95
+ for kw in pb.trigger_keywords:
96
+ if kw.lower() in text_lower:
97
+ score += 1
98
+ if score > 0:
99
+ # Temporarily set confidence based on keyword hit ratio
100
+ pb.confidence = round(score / max(len(pb.trigger_keywords), 1), 2)
101
+ matches.append(pb)
102
+ return sorted(matches, key=lambda p: p.confidence, reverse=True)
@@ -0,0 +1,84 @@
1
+ """Core data models for playbooks."""
2
+
3
+ import uuid
4
+ from dataclasses import dataclass, field
5
+ from typing import List, Optional
6
+
7
+
8
+ @dataclass
9
+ class PlaybookStep:
10
+ """A single step in a playbook."""
11
+ number: int
12
+ description: str
13
+ tool: str = "" # e.g. "browser", "freshservice-api", "manual"
14
+ tool_params: dict = field(default_factory=dict)
15
+ requires_human: bool = False
16
+ notes: str = ""
17
+
18
+ def to_dict(self) -> dict:
19
+ return {
20
+ "number": self.number,
21
+ "description": self.description,
22
+ "tool": self.tool,
23
+ "tool_params": self.tool_params,
24
+ "requires_human": self.requires_human,
25
+ "notes": self.notes,
26
+ }
27
+
28
+ @classmethod
29
+ def from_dict(cls, d: dict) -> "PlaybookStep":
30
+ return cls(
31
+ number=d["number"],
32
+ description=d["description"],
33
+ tool=d.get("tool", ""),
34
+ tool_params=d.get("tool_params", {}),
35
+ requires_human=d.get("requires_human", False),
36
+ notes=d.get("notes", ""),
37
+ )
38
+
39
+
40
+ @dataclass
41
+ class Playbook:
42
+ """A reusable workflow playbook."""
43
+ id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
44
+ name: str = ""
45
+ description: str = ""
46
+ trigger_keywords: List[str] = field(default_factory=list)
47
+ steps: List[PlaybookStep] = field(default_factory=list)
48
+ confidence: float = 1.0
49
+ version: int = 1
50
+ executions: int = 0
51
+ source: str = "manual" # "manual", "extracted", "observed"
52
+ runbook_path: str = "" # link to full runbook doc
53
+ related_links: dict = field(default_factory=dict)
54
+
55
+ def to_dict(self) -> dict:
56
+ return {
57
+ "id": self.id,
58
+ "name": self.name,
59
+ "description": self.description,
60
+ "trigger_keywords": self.trigger_keywords,
61
+ "steps": [s.to_dict() for s in self.steps],
62
+ "confidence": self.confidence,
63
+ "version": self.version,
64
+ "executions": self.executions,
65
+ "source": self.source,
66
+ "runbook_path": self.runbook_path,
67
+ "related_links": self.related_links,
68
+ }
69
+
70
+ @classmethod
71
+ def from_dict(cls, d: dict) -> "Playbook":
72
+ return cls(
73
+ id=d["id"],
74
+ name=d.get("name", ""),
75
+ description=d.get("description", ""),
76
+ trigger_keywords=d.get("trigger_keywords", []),
77
+ steps=[PlaybookStep.from_dict(s) for s in d.get("steps", [])],
78
+ confidence=d.get("confidence", 1.0),
79
+ version=d.get("version", 1),
80
+ executions=d.get("executions", 0),
81
+ source=d.get("source", "manual"),
82
+ runbook_path=d.get("runbook_path", ""),
83
+ related_links=d.get("related_links", {}),
84
+ )
@@ -0,0 +1,35 @@
1
+ """ToolRegistry - maps tools to their capabilities and defaults."""
2
+
3
+ import json
4
+ import os
5
+ from typing import Optional
6
+
7
+
8
+ class ToolRegistry:
9
+ """Loads tool definitions from the registry directory."""
10
+
11
+ def __init__(self, registry_dir: str):
12
+ self.registry_dir = registry_dir
13
+ self._tools: dict = {}
14
+ self._load()
15
+
16
+ def _load(self):
17
+ if not os.path.exists(self.registry_dir):
18
+ return
19
+ for f in os.listdir(self.registry_dir):
20
+ if not f.endswith(".json"):
21
+ continue
22
+ path = os.path.join(self.registry_dir, f)
23
+ try:
24
+ with open(path) as fh:
25
+ data = json.load(fh)
26
+ tool_name = data.get("name", f.replace(".json", ""))
27
+ self._tools[tool_name] = data
28
+ except (json.JSONDecodeError, KeyError):
29
+ continue
30
+
31
+ def get_tool(self, name: str) -> Optional[dict]:
32
+ return self._tools.get(name)
33
+
34
+ def list_tools(self) -> list:
35
+ return list(self._tools.keys())
@@ -0,0 +1,72 @@
1
+ import pytest
2
+ import tempfile
3
+ import yaml
4
+ from tracer import WorkflowTracer, TraceEvent
5
+ from registry import ToolRegistry
6
+ from extractor import PlaybookExtractor
7
+ from models import Playbook
8
+
9
+ def make_registry(tmp_path):
10
+ reg_dir = tmp_path / "tool-registry"
11
+ reg_dir.mkdir()
12
+ (reg_dir / "_registry.yml").write_text(yaml.dump({
13
+ "tools": {
14
+ "jira": {"type": "mcp", "prefix": "mcp__mcp-atlassian__jira_", "capabilities": ["create_issue"], "auth": "persistent", "domains": ["ticket_management"]},
15
+ "slack": {"type": "mcp", "prefix": "mcp__slack__", "capabilities": ["post_message"], "auth": "persistent", "domains": ["communication"]},
16
+ "freshservice": {"type": "mcp", "prefix": "mcp__freshservice-mcp__", "capabilities": ["update_ticket"], "auth": "persistent", "domains": ["service_desk"]},
17
+ }
18
+ }))
19
+ (reg_dir / "jira.defaults.yml").write_text(yaml.dump({"create_issue": {"assignee": "test@test.com"}}))
20
+ return ToolRegistry(str(reg_dir))
21
+
22
+ def test_extract_playbook_from_trace(tmp_path):
23
+ registry = make_registry(tmp_path)
24
+ tracer = WorkflowTracer(traces_dir=str(tmp_path / "traces"))
25
+
26
+ tracer.start_trace("ITWORK2-100")
27
+ tracer.add_event(TraceEvent(type="user_instruction", content="Do SSO onboarding for Linear"))
28
+ tracer.add_event(TraceEvent(type="tool_call", tool="mcp__mcp-atlassian__jira_create_issue", params={"project": "ITWORK2", "summary": "[SSO] Linear"}))
29
+ tracer.add_event(TraceEvent(type="user_manual_action", content="Configured SAML in Okta", inferred_step="Configure SAML", action_binding="playwright", auth_note="needs Okta admin"))
30
+ tracer.add_event(TraceEvent(type="tool_call", tool="mcp__slack__post_message", params={"channel": "C123", "text": "SSO configured"}))
31
+ tracer.add_event(TraceEvent(type="tool_call", tool="mcp__freshservice-mcp__update_ticket", params={"ticket_id": 456, "status": "resolved"}))
32
+
33
+ extractor = PlaybookExtractor(registry=registry)
34
+ pb = extractor.extract_from_trace(tracer.get_trace("ITWORK2-100"), name="SSO Onboarding")
35
+
36
+ assert pb.id == "sso-onboarding"
37
+ assert pb.confidence == "low"
38
+ assert pb.created_from == "session"
39
+ assert len(pb.steps) == 4 # jira + manual + slack + freshservice
40
+ assert pb.steps[0].action.tool == "mcp__mcp-atlassian__jira_create_issue"
41
+ assert pb.steps[1].human_required is True # manual action
42
+ assert pb.steps[1].action.tool == "playwright"
43
+
44
+ def test_extract_identifies_dynamic_params(tmp_path):
45
+ registry = make_registry(tmp_path)
46
+ tracer = WorkflowTracer(traces_dir=str(tmp_path / "traces"))
47
+
48
+ tracer.start_trace("t1")
49
+ tracer.add_event(TraceEvent(type="tool_call", tool="mcp__mcp-atlassian__jira_create_issue", params={"project": "ITWORK2", "summary": "[SSO] Linear", "assignee": "test@test.com"}))
50
+
51
+ extractor = PlaybookExtractor(registry=registry)
52
+ pb = extractor.extract_from_trace(tracer.get_trace("t1"), name="Test")
53
+
54
+ # assignee matches default, so should NOT be in playbook params (it comes from defaults)
55
+ # summary is ticket-specific, so should be a param with a source
56
+ step = pb.steps[0]
57
+ assert "assignee" not in step.action.params # comes from defaults
58
+ assert "summary" in step.action.params or "summary" in step.action.param_sources
59
+
60
+ def test_extract_captures_user_rules(tmp_path):
61
+ registry = make_registry(tmp_path)
62
+ tracer = WorkflowTracer(traces_dir=str(tmp_path / "traces"))
63
+
64
+ tracer.start_trace("t1")
65
+ tracer.add_event(TraceEvent(type="user_rule", content="Always add SSO label", applies_to=["jira.create_issue"], persist=True))
66
+ tracer.add_event(TraceEvent(type="tool_call", tool="mcp__mcp-atlassian__jira_create_issue", params={"project": "ITWORK2"}))
67
+
68
+ extractor = PlaybookExtractor(registry=registry)
69
+ pb = extractor.extract_from_trace(tracer.get_trace("t1"), name="Test")
70
+ assert pb is not None
71
+ # The extractor should note persistent rules for default updates
72
+ assert len(extractor.pending_default_updates) > 0
@@ -0,0 +1,129 @@
1
+ """End-to-end test: trace capture -> extraction -> storage -> matching -> execution dispatch."""
2
+
3
+ import pytest
4
+ import yaml
5
+ import tempfile
6
+ import json
7
+ from tracer import WorkflowTracer, TraceEvent
8
+ from registry import ToolRegistry
9
+ from extractor import PlaybookExtractor
10
+ from manager import PlaybookManager
11
+ from models import Playbook
12
+
13
+ @pytest.fixture
14
+ def env(tmp_path):
15
+ """Set up a complete playbook environment."""
16
+ playbooks_dir = tmp_path / "playbooks"
17
+ playbooks_dir.mkdir()
18
+ (playbooks_dir / "drafts").mkdir()
19
+ (playbooks_dir / "archive").mkdir()
20
+
21
+ reg_dir = playbooks_dir / "tool-registry"
22
+ reg_dir.mkdir()
23
+ (reg_dir / "_registry.yml").write_text(yaml.dump({
24
+ "tools": {
25
+ "jira": {"type": "mcp", "prefix": "mcp__mcp-atlassian__jira_", "capabilities": ["create_issue", "transition_issue"], "auth": "persistent", "domains": ["ticket_management"]},
26
+ "slack": {"type": "mcp", "prefix": "mcp__slack__", "capabilities": ["post_message"], "auth": "persistent", "domains": ["communication"]},
27
+ "freshservice": {"type": "mcp", "prefix": "mcp__freshservice-mcp__", "capabilities": ["update_ticket"], "auth": "persistent", "domains": ["service_desk"]},
28
+ "playwright_cli": {"type": "browser", "tool": "playwright_cli", "capabilities": ["navigate", "click", "fill", "snapshot", "eval"], "auth": "per_domain", "domains": ["standard_web_ui"]},
29
+ }
30
+ }))
31
+ (reg_dir / "jira.defaults.yml").write_text(yaml.dump({
32
+ "create_issue": {"assignee": "kioja@test.com", "board_id": 70},
33
+ }))
34
+
35
+ traces_dir = tmp_path / "traces"
36
+ return {
37
+ "playbooks_dir": str(playbooks_dir),
38
+ "traces_dir": str(traces_dir),
39
+ "registry_dir": str(reg_dir),
40
+ }
41
+
42
+ def test_full_flow(env):
43
+ """Simulate: work SSO ticket -> extract playbook -> match new ticket -> dispatch."""
44
+
45
+ # 1. Capture a workflow trace
46
+ tracer = WorkflowTracer(traces_dir=env["traces_dir"])
47
+ tracer.start_trace("ITWORK2-100")
48
+
49
+ tracer.add_event(TraceEvent(type="user_instruction", content="Do SSO onboarding for Linear"))
50
+ tracer.add_event(TraceEvent(type="tool_call", tool="mcp__mcp-atlassian__jira_create_issue",
51
+ params={"project": "ITWORK2", "summary": "[SSO] Linear", "assignee": "kioja@test.com", "board_id": 70}))
52
+ tracer.add_event(TraceEvent(type="user_manual_action", content="Configured SAML in Okta",
53
+ inferred_step="Configure SAML in IdP", action_binding="playwright", auth_note="Okta admin"))
54
+ tracer.add_event(TraceEvent(type="tool_call", tool="mcp__slack__post_message",
55
+ params={"channel": "C123", "text": "SSO ready for Linear"}))
56
+ tracer.add_event(TraceEvent(type="tool_call", tool="mcp__freshservice-mcp__update_ticket",
57
+ params={"ticket_id": 456, "status": 5}))
58
+ tracer.flush("ITWORK2-100")
59
+
60
+ # 2. Extract a playbook
61
+ registry = ToolRegistry(env["registry_dir"])
62
+ extractor = PlaybookExtractor(registry=registry)
63
+ trace = tracer.get_trace("ITWORK2-100")
64
+ pb = extractor.extract_from_trace(trace, name="SSO Onboarding")
65
+
66
+ assert pb.id == "sso-onboarding"
67
+ assert pb.confidence == "low"
68
+ assert len(pb.steps) == 4
69
+ assert pb.steps[1].human_required is True # manual SAML config
70
+
71
+ # 3. Save as draft, review, promote
72
+ manager = PlaybookManager(env["playbooks_dir"])
73
+ manager.save_draft(pb)
74
+ assert len(manager.list_drafts()) == 1
75
+
76
+ manager.promote_draft("sso-onboarding")
77
+ assert len(manager.list_drafts()) == 0
78
+ assert len(manager.list_playbooks()) == 1
79
+
80
+ # 4. Match against a new ticket
81
+ matches = manager.match_ticket(
82
+ ticket_type="Service Request",
83
+ text="Please set up SSO for Notion",
84
+ source="freshservice",
85
+ )
86
+ assert len(matches) == 1
87
+ assert matches[0].id == "sso-onboarding"
88
+
89
+ # 5. Record execution and check confidence progression
90
+ promoted = manager.get("sso-onboarding")
91
+ promoted.record_execution(success=True)
92
+ assert promoted.confidence == "medium"
93
+ assert promoted.executions == 1
94
+ manager.save(promoted)
95
+
96
+ # Verify persistence
97
+ reloaded = manager.get("sso-onboarding")
98
+ assert reloaded.confidence == "medium"
99
+ assert reloaded.executions == 1
100
+
101
+ def test_no_match_returns_empty(env):
102
+ manager = PlaybookManager(env["playbooks_dir"])
103
+ matches = manager.match_ticket(text="New laptop request", source="freshservice")
104
+ assert matches == []
105
+
106
+ def test_dictated_playbook_flow(env):
107
+ """Path 2: User describes steps, engine creates playbook."""
108
+ registry = ToolRegistry(env["registry_dir"])
109
+ extractor = PlaybookExtractor(registry=registry)
110
+
111
+ pb = extractor.extract_from_description(
112
+ name="Employee Offboarding",
113
+ steps_text=[
114
+ "Disable user account in Okta",
115
+ "Remove from all Slack channels",
116
+ "Archive Jira tickets",
117
+ "Send confirmation email to manager",
118
+ ],
119
+ )
120
+
121
+ assert pb.id == "employee-offboarding"
122
+ assert pb.confidence == "medium" # dictated starts at medium
123
+ assert len(pb.steps) == 4
124
+ assert pb.created_from == "dictated"
125
+
126
+ manager = PlaybookManager(env["playbooks_dir"])
127
+ manager.save(pb)
128
+ loaded = manager.get("employee-offboarding")
129
+ assert loaded is not None
@@ -0,0 +1,85 @@
1
+ import pytest
2
+ import tempfile
3
+ import os
4
+ from manager import PlaybookManager
5
+ from models import Playbook, PlaybookStep, ActionBinding, TriggerPattern
6
+
7
+ def make_playbook(id="test", name="Test", confidence="high", keywords=None, steps=None):
8
+ return Playbook(
9
+ id=id, name=name, version=1, confidence=confidence,
10
+ trigger_patterns=[TriggerPattern(keywords=keywords or ["SSO"], source=["freshservice"])],
11
+ created_from="session", executions=3,
12
+ steps=steps or [],
13
+ )
14
+
15
+ def test_save_and_load(tmp_path):
16
+ mgr = PlaybookManager(str(tmp_path))
17
+ pb = make_playbook()
18
+ mgr.save(pb)
19
+ loaded = mgr.get("test")
20
+ assert loaded.id == "test"
21
+ assert loaded.confidence == "high"
22
+
23
+ def test_list_playbooks(tmp_path):
24
+ mgr = PlaybookManager(str(tmp_path))
25
+ mgr.save(make_playbook(id="a", name="A"))
26
+ mgr.save(make_playbook(id="b", name="B"))
27
+ pbs = mgr.list_playbooks()
28
+ assert len(pbs) == 2
29
+ assert {p.id for p in pbs} == {"a", "b"}
30
+
31
+ def test_match_ticket(tmp_path):
32
+ mgr = PlaybookManager(str(tmp_path))
33
+ mgr.save(make_playbook(id="sso", keywords=["SSO", "SAML"]))
34
+ mgr.save(make_playbook(id="cert", keywords=["certificate", "renewal"]))
35
+ matches = mgr.match_ticket(ticket_type="Service Request", text="Set up SSO for Linear", source="freshservice")
36
+ assert len(matches) == 1
37
+ assert matches[0].id == "sso"
38
+
39
+ def test_save_draft(tmp_path):
40
+ mgr = PlaybookManager(str(tmp_path))
41
+ pb = make_playbook(id="draft-test", confidence="low")
42
+ mgr.save_draft(pb)
43
+ drafts = mgr.list_drafts()
44
+ assert len(drafts) == 1
45
+ assert drafts[0].id == "draft-test"
46
+
47
+ def test_promote_draft(tmp_path):
48
+ mgr = PlaybookManager(str(tmp_path))
49
+ pb = make_playbook(id="promote-test", confidence="low")
50
+ mgr.save_draft(pb)
51
+ mgr.promote_draft("promote-test")
52
+ assert mgr.get("promote-test") is not None
53
+ assert len(mgr.list_drafts()) == 0
54
+
55
+ def test_archive_version(tmp_path):
56
+ mgr = PlaybookManager(str(tmp_path))
57
+ pb = make_playbook(id="versioned", confidence="high")
58
+ mgr.save(pb)
59
+ pb.version = 2
60
+ pb.update_history.append({"version": 2, "reason": "added step"})
61
+ mgr.save(pb, archive_previous=True)
62
+ loaded = mgr.get("versioned")
63
+ assert loaded.version == 2
64
+ archives = mgr.list_archive("versioned")
65
+ assert len(archives) == 1
66
+
67
+ def test_invalid_playbook_id_rejected(tmp_path):
68
+ mgr = PlaybookManager(str(tmp_path))
69
+ import pytest
70
+ with pytest.raises(ValueError):
71
+ mgr.get("../../../etc/passwd")
72
+ with pytest.raises(ValueError):
73
+ mgr.get("UPPER_CASE")
74
+ with pytest.raises(ValueError):
75
+ mgr.get("")
76
+ with pytest.raises(ValueError):
77
+ mgr.delete_draft("../../bad")
78
+
79
+ def test_delete_nonexistent_draft(tmp_path):
80
+ mgr = PlaybookManager(str(tmp_path))
81
+ assert mgr.delete_draft("nonexistent") is False
82
+
83
+ def test_promote_nonexistent_draft(tmp_path):
84
+ mgr = PlaybookManager(str(tmp_path))
85
+ assert mgr.promote_draft("nonexistent") is None