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.
- package/LICENSE +21 -21
- package/README.md +170 -170
- package/bin/brain.py +1340 -0
- package/bin/convert_claude_skills_to_codex.py +490 -0
- package/bin/generate-manifest.mjs +463 -463
- package/bin/harden_codex_skills.py +141 -0
- package/bin/install.mjs +491 -491
- package/bin/migrate-eng-buddy-data.py +875 -0
- package/bin/playbook_engine/__init__.py +1 -0
- package/bin/playbook_engine/conftest.py +8 -0
- package/bin/playbook_engine/extractor.py +33 -0
- package/bin/playbook_engine/manager.py +102 -0
- package/bin/playbook_engine/models.py +84 -0
- package/bin/playbook_engine/registry.py +35 -0
- package/bin/playbook_engine/test_extractor.py +72 -0
- package/bin/playbook_engine/test_integration.py +129 -0
- package/bin/playbook_engine/test_manager.py +85 -0
- package/bin/playbook_engine/test_models.py +166 -0
- package/bin/playbook_engine/test_registry.py +67 -0
- package/bin/playbook_engine/test_tracer.py +86 -0
- package/bin/playbook_engine/tracer.py +93 -0
- package/bin/tasks_db.py +456 -0
- package/docs/HOOKS.md +243 -243
- package/docs/INBOX.md +233 -233
- package/ftm/SKILL.md +125 -122
- package/ftm-audit/SKILL.md +623 -623
- package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
- package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
- package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
- package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
- package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
- package/ftm-audit/scripts/run-knip.sh +23 -23
- package/ftm-audit.yml +2 -2
- package/ftm-brainstorm/SKILL.md +1003 -498
- package/ftm-brainstorm/evals/evals.json +180 -100
- package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
- package/ftm-brainstorm/references/agent-prompts.md +552 -224
- package/ftm-brainstorm/references/plan-template.md +209 -121
- package/ftm-brainstorm.yml +2 -2
- package/ftm-browse/SKILL.md +454 -454
- package/ftm-browse/daemon/browser-manager.ts +206 -206
- package/ftm-browse/daemon/bun.lock +30 -30
- package/ftm-browse/daemon/cli.ts +347 -347
- package/ftm-browse/daemon/commands.ts +410 -410
- package/ftm-browse/daemon/main.ts +357 -357
- package/ftm-browse/daemon/package.json +17 -17
- package/ftm-browse/daemon/server.ts +189 -189
- package/ftm-browse/daemon/snapshot.ts +519 -519
- package/ftm-browse/daemon/tsconfig.json +22 -22
- package/ftm-browse.yml +4 -4
- package/ftm-capture/SKILL.md +370 -370
- package/ftm-capture.yml +4 -4
- package/ftm-codex-gate/SKILL.md +361 -361
- package/ftm-codex-gate.yml +2 -2
- package/ftm-config/SKILL.md +422 -345
- package/ftm-config.default.yml +125 -82
- package/ftm-config.yml +44 -2
- package/ftm-council/SKILL.md +416 -416
- package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
- package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
- package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
- package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
- package/ftm-council.yml +2 -2
- package/ftm-dashboard/SKILL.md +163 -163
- package/ftm-dashboard.yml +4 -4
- package/ftm-debug/SKILL.md +1037 -1037
- package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
- package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
- package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
- package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
- package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
- package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
- package/ftm-debug.yml +2 -2
- package/ftm-diagram/SKILL.md +277 -277
- package/ftm-diagram.yml +2 -2
- package/ftm-executor/SKILL.md +777 -777
- package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
- package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
- package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
- package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
- package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
- package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
- package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
- package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
- package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
- package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
- package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
- package/ftm-executor/runtime/package.json +8 -8
- package/ftm-executor.yml +2 -2
- package/ftm-git/SKILL.md +441 -441
- package/ftm-git/evals/evals.json +26 -26
- package/ftm-git/evals/promptfoo.yaml +75 -75
- package/ftm-git/hooks/post-commit-experience.sh +92 -92
- package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
- package/ftm-git/references/protocols/REMEDIATION.md +139 -139
- package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
- package/ftm-git.yml +2 -2
- package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -64
- package/ftm-inbox/backend/adapters/base.py +230 -230
- package/ftm-inbox/backend/adapters/freshservice.py +104 -104
- package/ftm-inbox/backend/adapters/gmail.py +125 -125
- package/ftm-inbox/backend/adapters/jira.py +136 -136
- package/ftm-inbox/backend/adapters/registry.py +192 -192
- package/ftm-inbox/backend/adapters/slack.py +110 -110
- package/ftm-inbox/backend/db/connection.py +54 -54
- package/ftm-inbox/backend/db/schema.py +78 -78
- package/ftm-inbox/backend/executor/__init__.py +7 -7
- package/ftm-inbox/backend/executor/engine.py +149 -149
- package/ftm-inbox/backend/executor/step_runner.py +98 -98
- package/ftm-inbox/backend/main.py +103 -103
- package/ftm-inbox/backend/models/__init__.py +1 -1
- package/ftm-inbox/backend/models/unified_task.py +36 -36
- package/ftm-inbox/backend/planner/__init__.py +6 -6
- package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/generator.py +127 -127
- package/ftm-inbox/backend/planner/schema.py +34 -34
- package/ftm-inbox/backend/requirements.txt +5 -5
- package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -186
- package/ftm-inbox/backend/routes/health.py +52 -52
- package/ftm-inbox/backend/routes/inbox.py +68 -68
- package/ftm-inbox/backend/routes/plan.py +271 -271
- package/ftm-inbox/bin/launchagent.mjs +91 -91
- package/ftm-inbox/bin/setup.mjs +188 -188
- package/ftm-inbox/bin/start.sh +10 -10
- package/ftm-inbox/bin/status.sh +17 -17
- package/ftm-inbox/bin/stop.sh +8 -8
- package/ftm-inbox/config.example.yml +55 -55
- package/ftm-inbox/package-lock.json +2898 -2898
- package/ftm-inbox/package.json +26 -26
- package/ftm-inbox/postcss.config.js +6 -6
- package/ftm-inbox/src/app.css +199 -199
- package/ftm-inbox/src/app.html +18 -18
- package/ftm-inbox/src/lib/api.ts +166 -166
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
- package/ftm-inbox/src/lib/theme.ts +47 -47
- package/ftm-inbox/src/routes/+layout.svelte +76 -76
- package/ftm-inbox/src/routes/+page.svelte +401 -401
- package/ftm-inbox/svelte.config.js +12 -12
- package/ftm-inbox/tailwind.config.ts +63 -63
- package/ftm-inbox/tsconfig.json +13 -13
- package/ftm-inbox/vite.config.ts +6 -6
- package/ftm-intent/SKILL.md +241 -241
- package/ftm-intent.yml +2 -2
- package/ftm-manifest.json +3794 -3794
- package/ftm-map/SKILL.md +291 -291
- package/ftm-map/scripts/db.py +712 -712
- package/ftm-map/scripts/index.py +415 -415
- package/ftm-map/scripts/parser.py +224 -224
- package/ftm-map/scripts/queries/go-tags.scm +20 -20
- package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
- package/ftm-map/scripts/queries/python-tags.scm +31 -31
- package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
- package/ftm-map/scripts/queries/rust-tags.scm +37 -37
- package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
- package/ftm-map/scripts/query.py +301 -301
- package/ftm-map/scripts/ranker.py +377 -377
- package/ftm-map/scripts/requirements.txt +5 -5
- package/ftm-map/scripts/setup-hooks.sh +27 -27
- package/ftm-map/scripts/setup.sh +56 -56
- package/ftm-map/scripts/test_db.py +364 -364
- package/ftm-map/scripts/test_parser.py +174 -174
- package/ftm-map/scripts/test_query.py +183 -183
- package/ftm-map/scripts/test_ranker.py +199 -199
- package/ftm-map/scripts/views.py +591 -591
- package/ftm-map.yml +2 -2
- package/ftm-mind/SKILL.md +201 -1943
- package/ftm-mind/evals/promptfoo.yaml +142 -142
- package/ftm-mind/references/blackboard-protocol.md +110 -0
- package/ftm-mind/references/blackboard-schema.md +328 -328
- package/ftm-mind/references/complexity-guide.md +110 -110
- package/ftm-mind/references/complexity-sizing.md +138 -0
- package/ftm-mind/references/decide-act-protocol.md +172 -0
- package/ftm-mind/references/direct-execution.md +51 -0
- package/ftm-mind/references/environment-discovery.md +77 -0
- package/ftm-mind/references/event-registry.md +319 -319
- package/ftm-mind/references/mcp-inventory.md +300 -296
- package/ftm-mind/references/ops-routing.md +47 -0
- package/ftm-mind/references/orient-protocol.md +234 -0
- package/ftm-mind/references/personality.md +40 -0
- package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
- package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
- package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
- package/ftm-mind/references/reflexion-protocol.md +249 -249
- package/ftm-mind/references/routing/SCENARIOS.md +22 -22
- package/ftm-mind/references/routing-scenarios.md +35 -35
- package/ftm-mind.yml +2 -2
- package/ftm-ops.yml +4 -0
- package/ftm-pause/SKILL.md +395 -395
- package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
- package/ftm-pause/references/protocols/VALIDATION.md +80 -80
- package/ftm-pause.yml +2 -2
- package/ftm-researcher/SKILL.md +275 -275
- package/ftm-researcher/evals/agent-diversity.yaml +17 -17
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
- package/ftm-researcher/references/adaptive-search.md +116 -116
- package/ftm-researcher/references/agent-prompts.md +193 -193
- package/ftm-researcher/references/council-integration.md +193 -193
- package/ftm-researcher/references/output-format.md +203 -203
- package/ftm-researcher/references/synthesis-pipeline.md +165 -165
- package/ftm-researcher/scripts/score_credibility.py +234 -234
- package/ftm-researcher/scripts/validate_research.py +92 -92
- package/ftm-researcher.yml +2 -2
- package/ftm-resume/SKILL.md +518 -518
- package/ftm-resume/references/protocols/VALIDATION.md +172 -172
- package/ftm-resume.yml +2 -2
- package/ftm-retro/SKILL.md +380 -380
- package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
- package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
- package/ftm-retro.yml +2 -2
- package/ftm-routine/SKILL.md +170 -170
- package/ftm-routine.yml +4 -4
- package/ftm-state/blackboard/capabilities.json +5 -5
- package/ftm-state/blackboard/capabilities.schema.json +27 -27
- package/ftm-state/blackboard/context.json +37 -23
- package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
- package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
- package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
- package/ftm-state/blackboard/experiences/index.json +58 -9
- package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
- package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
- package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
- package/ftm-state/blackboard/patterns.json +6 -6
- package/ftm-state/schemas/context.schema.json +130 -130
- package/ftm-state/schemas/experience-index.schema.json +77 -77
- package/ftm-state/schemas/experience.schema.json +78 -78
- package/ftm-state/schemas/patterns.schema.json +44 -44
- package/ftm-upgrade/SKILL.md +194 -194
- package/ftm-upgrade/scripts/check-version.sh +76 -76
- package/ftm-upgrade/scripts/upgrade.sh +143 -143
- package/ftm-upgrade.yml +2 -2
- package/ftm-verify.yml +2 -2
- package/ftm.yml +2 -2
- package/hooks/ftm-auto-log.sh +137 -0
- package/hooks/ftm-blackboard-enforcer.sh +93 -93
- package/hooks/ftm-discovery-reminder.sh +90 -90
- package/hooks/ftm-drafts-gate.sh +61 -61
- package/hooks/ftm-event-logger.mjs +107 -107
- package/hooks/ftm-install-hooks.sh +240 -0
- package/hooks/ftm-learning-capture.sh +117 -0
- package/hooks/ftm-map-autodetect.sh +79 -79
- package/hooks/ftm-pending-sync-check.sh +22 -22
- package/hooks/ftm-plan-gate.sh +92 -92
- package/hooks/ftm-post-commit-trigger.sh +57 -57
- package/hooks/ftm-post-compaction.sh +138 -0
- package/hooks/ftm-pre-compaction.sh +147 -0
- package/hooks/ftm-session-end.sh +52 -0
- package/hooks/ftm-session-snapshot.sh +213 -0
- package/hooks/settings-template.json +81 -81
- package/install.sh +363 -363
- package/package.json +84 -84
- 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)
|