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
@@ -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