feed-the-machine 1.6.0 → 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
|
@@ -1,186 +1,186 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Execution API routes — start, pause, resume, retry, audit log, SSE streaming.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
import json
|
|
9
|
-
|
|
10
|
-
from fastapi import APIRouter, HTTPException, Query
|
|
11
|
-
from fastapi.responses import StreamingResponse
|
|
12
|
-
|
|
13
|
-
from backend.db.connection import get_connection
|
|
14
|
-
from backend.executor.engine import ExecutionEngine
|
|
15
|
-
|
|
16
|
-
router = APIRouter(prefix="/api", tags=["execution"])
|
|
17
|
-
|
|
18
|
-
# In-memory registry of active engines (single-process; swap for Redis in production)
|
|
19
|
-
_active_engines: dict[int, ExecutionEngine] = {}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@router.post("/tasks/{task_id}/execute")
|
|
23
|
-
async def start_execution(task_id: int):
|
|
24
|
-
"""Start execution of approved plan steps."""
|
|
25
|
-
conn = get_connection()
|
|
26
|
-
plan_row = conn.execute(
|
|
27
|
-
"SELECT id, status FROM plans WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
|
28
|
-
(task_id,),
|
|
29
|
-
).fetchone()
|
|
30
|
-
|
|
31
|
-
if not plan_row:
|
|
32
|
-
raise HTTPException(404, "No plan found for this task")
|
|
33
|
-
|
|
34
|
-
plan_id = plan_row["id"]
|
|
35
|
-
engine = ExecutionEngine(task_id, plan_id)
|
|
36
|
-
_active_engines[task_id] = engine
|
|
37
|
-
|
|
38
|
-
result = await engine.execute()
|
|
39
|
-
_active_engines.pop(task_id, None)
|
|
40
|
-
return result
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@router.get("/tasks/{task_id}/execution-stream")
|
|
44
|
-
async def execution_stream(task_id: int):
|
|
45
|
-
"""SSE stream of execution output."""
|
|
46
|
-
conn = get_connection()
|
|
47
|
-
plan_row = conn.execute(
|
|
48
|
-
"SELECT id FROM plans WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
|
49
|
-
(task_id,),
|
|
50
|
-
).fetchone()
|
|
51
|
-
|
|
52
|
-
if not plan_row:
|
|
53
|
-
raise HTTPException(404, "No plan found for this task")
|
|
54
|
-
|
|
55
|
-
plan_id = plan_row["id"]
|
|
56
|
-
engine = ExecutionEngine(task_id, plan_id)
|
|
57
|
-
_active_engines[task_id] = engine
|
|
58
|
-
|
|
59
|
-
output_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
60
|
-
|
|
61
|
-
def on_output(text: str):
|
|
62
|
-
output_queue.put_nowait(text)
|
|
63
|
-
|
|
64
|
-
engine.on_output(on_output)
|
|
65
|
-
|
|
66
|
-
async def event_generator():
|
|
67
|
-
# Start execution in background
|
|
68
|
-
task = asyncio.create_task(engine.execute())
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
while not task.done():
|
|
72
|
-
try:
|
|
73
|
-
text = await asyncio.wait_for(output_queue.get(), timeout=1.0)
|
|
74
|
-
yield f"data: {json.dumps({'type': 'chunk', 'text': text})}\n\n"
|
|
75
|
-
except asyncio.TimeoutError:
|
|
76
|
-
yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
|
|
77
|
-
|
|
78
|
-
# Drain remaining messages
|
|
79
|
-
while not output_queue.empty():
|
|
80
|
-
text = output_queue.get_nowait()
|
|
81
|
-
yield f"data: {json.dumps({'type': 'chunk', 'text': text})}\n\n"
|
|
82
|
-
|
|
83
|
-
result = task.result()
|
|
84
|
-
yield f"data: {json.dumps({'type': 'done', 'result': result})}\n\n"
|
|
85
|
-
except Exception as exc:
|
|
86
|
-
yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n"
|
|
87
|
-
finally:
|
|
88
|
-
_active_engines.pop(task_id, None)
|
|
89
|
-
|
|
90
|
-
return StreamingResponse(
|
|
91
|
-
event_generator(),
|
|
92
|
-
media_type="text/event-stream",
|
|
93
|
-
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@router.post("/tasks/{task_id}/pause")
|
|
98
|
-
async def pause_execution(task_id: int):
|
|
99
|
-
"""Pause execution after the current step completes."""
|
|
100
|
-
engine = _active_engines.get(task_id)
|
|
101
|
-
if not engine:
|
|
102
|
-
raise HTTPException(404, "No active execution for this task")
|
|
103
|
-
engine.pause()
|
|
104
|
-
return {"status": "pausing", "message": "Execution will pause after current step"}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@router.post("/tasks/{task_id}/resume")
|
|
108
|
-
async def resume_execution(task_id: int):
|
|
109
|
-
"""Resume a paused execution."""
|
|
110
|
-
engine = _active_engines.get(task_id)
|
|
111
|
-
if engine:
|
|
112
|
-
engine.resume()
|
|
113
|
-
result = await engine.execute()
|
|
114
|
-
return result
|
|
115
|
-
|
|
116
|
-
# No active engine — restart from where we left off
|
|
117
|
-
conn = get_connection()
|
|
118
|
-
plan_row = conn.execute(
|
|
119
|
-
"SELECT id FROM plans WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
|
120
|
-
(task_id,),
|
|
121
|
-
).fetchone()
|
|
122
|
-
if not plan_row:
|
|
123
|
-
raise HTTPException(404, "No plan found for this task")
|
|
124
|
-
|
|
125
|
-
engine = ExecutionEngine(task_id, plan_row["id"])
|
|
126
|
-
_active_engines[task_id] = engine
|
|
127
|
-
result = await engine.execute()
|
|
128
|
-
_active_engines.pop(task_id, None)
|
|
129
|
-
return result
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
@router.post("/tasks/{task_id}/steps/{step_id}/retry")
|
|
133
|
-
async def retry_step(task_id: int, step_id: int):
|
|
134
|
-
"""Retry a failed step."""
|
|
135
|
-
conn = get_connection()
|
|
136
|
-
plan_row = conn.execute(
|
|
137
|
-
"SELECT id, yaml_content FROM plans WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
|
138
|
-
(task_id,),
|
|
139
|
-
).fetchone()
|
|
140
|
-
if not plan_row:
|
|
141
|
-
raise HTTPException(404, "No plan found")
|
|
142
|
-
|
|
143
|
-
import yaml
|
|
144
|
-
plan_data = yaml.safe_load(plan_row["yaml_content"]) or {}
|
|
145
|
-
for step in plan_data.get("steps", []):
|
|
146
|
-
if step.get("id") == step_id:
|
|
147
|
-
step["status"] = "approved"
|
|
148
|
-
|
|
149
|
-
updated = yaml.dump(plan_data, default_flow_style=False)
|
|
150
|
-
conn.execute(
|
|
151
|
-
"UPDATE plans SET yaml_content = ?, updated_at = datetime('now') WHERE id = ?",
|
|
152
|
-
(updated, plan_row["id"]),
|
|
153
|
-
)
|
|
154
|
-
conn.commit()
|
|
155
|
-
|
|
156
|
-
return {"status": "reset", "step_id": step_id, "message": "Step reset to approved, ready for re-execution"}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
@router.get("/tasks/{task_id}/audit-log")
|
|
160
|
-
async def get_audit_log(task_id: int, limit: int = Query(100, ge=1, le=1000)):
|
|
161
|
-
"""Get audit log entries for a task's execution."""
|
|
162
|
-
conn = get_connection()
|
|
163
|
-
|
|
164
|
-
# Get all plan IDs for this task
|
|
165
|
-
plan_rows = conn.execute(
|
|
166
|
-
"SELECT id FROM plans WHERE task_id = ?", (task_id,)
|
|
167
|
-
).fetchall()
|
|
168
|
-
if not plan_rows:
|
|
169
|
-
return {"entries": []}
|
|
170
|
-
|
|
171
|
-
plan_ids = [r["id"] for r in plan_rows]
|
|
172
|
-
|
|
173
|
-
# Get step_ids from the plans to filter audit_log
|
|
174
|
-
rows = conn.execute(
|
|
175
|
-
f"SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ?",
|
|
176
|
-
(limit,),
|
|
177
|
-
).fetchall()
|
|
178
|
-
|
|
179
|
-
entries = []
|
|
180
|
-
for row in rows:
|
|
181
|
-
entry = dict(row)
|
|
182
|
-
if entry.get("result") and isinstance(entry["result"], str):
|
|
183
|
-
entry["result"] = json.loads(entry["result"])
|
|
184
|
-
entries.append(entry)
|
|
185
|
-
|
|
186
|
-
return {"entries": entries}
|
|
1
|
+
"""
|
|
2
|
+
Execution API routes — start, pause, resume, retry, audit log, SSE streaming.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
|
|
13
|
+
from backend.db.connection import get_connection
|
|
14
|
+
from backend.executor.engine import ExecutionEngine
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api", tags=["execution"])
|
|
17
|
+
|
|
18
|
+
# In-memory registry of active engines (single-process; swap for Redis in production)
|
|
19
|
+
_active_engines: dict[int, ExecutionEngine] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.post("/tasks/{task_id}/execute")
|
|
23
|
+
async def start_execution(task_id: int):
|
|
24
|
+
"""Start execution of approved plan steps."""
|
|
25
|
+
conn = get_connection()
|
|
26
|
+
plan_row = conn.execute(
|
|
27
|
+
"SELECT id, status FROM plans WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
|
28
|
+
(task_id,),
|
|
29
|
+
).fetchone()
|
|
30
|
+
|
|
31
|
+
if not plan_row:
|
|
32
|
+
raise HTTPException(404, "No plan found for this task")
|
|
33
|
+
|
|
34
|
+
plan_id = plan_row["id"]
|
|
35
|
+
engine = ExecutionEngine(task_id, plan_id)
|
|
36
|
+
_active_engines[task_id] = engine
|
|
37
|
+
|
|
38
|
+
result = await engine.execute()
|
|
39
|
+
_active_engines.pop(task_id, None)
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.get("/tasks/{task_id}/execution-stream")
|
|
44
|
+
async def execution_stream(task_id: int):
|
|
45
|
+
"""SSE stream of execution output."""
|
|
46
|
+
conn = get_connection()
|
|
47
|
+
plan_row = conn.execute(
|
|
48
|
+
"SELECT id FROM plans WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
|
49
|
+
(task_id,),
|
|
50
|
+
).fetchone()
|
|
51
|
+
|
|
52
|
+
if not plan_row:
|
|
53
|
+
raise HTTPException(404, "No plan found for this task")
|
|
54
|
+
|
|
55
|
+
plan_id = plan_row["id"]
|
|
56
|
+
engine = ExecutionEngine(task_id, plan_id)
|
|
57
|
+
_active_engines[task_id] = engine
|
|
58
|
+
|
|
59
|
+
output_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
60
|
+
|
|
61
|
+
def on_output(text: str):
|
|
62
|
+
output_queue.put_nowait(text)
|
|
63
|
+
|
|
64
|
+
engine.on_output(on_output)
|
|
65
|
+
|
|
66
|
+
async def event_generator():
|
|
67
|
+
# Start execution in background
|
|
68
|
+
task = asyncio.create_task(engine.execute())
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
while not task.done():
|
|
72
|
+
try:
|
|
73
|
+
text = await asyncio.wait_for(output_queue.get(), timeout=1.0)
|
|
74
|
+
yield f"data: {json.dumps({'type': 'chunk', 'text': text})}\n\n"
|
|
75
|
+
except asyncio.TimeoutError:
|
|
76
|
+
yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
|
|
77
|
+
|
|
78
|
+
# Drain remaining messages
|
|
79
|
+
while not output_queue.empty():
|
|
80
|
+
text = output_queue.get_nowait()
|
|
81
|
+
yield f"data: {json.dumps({'type': 'chunk', 'text': text})}\n\n"
|
|
82
|
+
|
|
83
|
+
result = task.result()
|
|
84
|
+
yield f"data: {json.dumps({'type': 'done', 'result': result})}\n\n"
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n"
|
|
87
|
+
finally:
|
|
88
|
+
_active_engines.pop(task_id, None)
|
|
89
|
+
|
|
90
|
+
return StreamingResponse(
|
|
91
|
+
event_generator(),
|
|
92
|
+
media_type="text/event-stream",
|
|
93
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.post("/tasks/{task_id}/pause")
|
|
98
|
+
async def pause_execution(task_id: int):
|
|
99
|
+
"""Pause execution after the current step completes."""
|
|
100
|
+
engine = _active_engines.get(task_id)
|
|
101
|
+
if not engine:
|
|
102
|
+
raise HTTPException(404, "No active execution for this task")
|
|
103
|
+
engine.pause()
|
|
104
|
+
return {"status": "pausing", "message": "Execution will pause after current step"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.post("/tasks/{task_id}/resume")
|
|
108
|
+
async def resume_execution(task_id: int):
|
|
109
|
+
"""Resume a paused execution."""
|
|
110
|
+
engine = _active_engines.get(task_id)
|
|
111
|
+
if engine:
|
|
112
|
+
engine.resume()
|
|
113
|
+
result = await engine.execute()
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
# No active engine — restart from where we left off
|
|
117
|
+
conn = get_connection()
|
|
118
|
+
plan_row = conn.execute(
|
|
119
|
+
"SELECT id FROM plans WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
|
120
|
+
(task_id,),
|
|
121
|
+
).fetchone()
|
|
122
|
+
if not plan_row:
|
|
123
|
+
raise HTTPException(404, "No plan found for this task")
|
|
124
|
+
|
|
125
|
+
engine = ExecutionEngine(task_id, plan_row["id"])
|
|
126
|
+
_active_engines[task_id] = engine
|
|
127
|
+
result = await engine.execute()
|
|
128
|
+
_active_engines.pop(task_id, None)
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@router.post("/tasks/{task_id}/steps/{step_id}/retry")
|
|
133
|
+
async def retry_step(task_id: int, step_id: int):
|
|
134
|
+
"""Retry a failed step."""
|
|
135
|
+
conn = get_connection()
|
|
136
|
+
plan_row = conn.execute(
|
|
137
|
+
"SELECT id, yaml_content FROM plans WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
|
138
|
+
(task_id,),
|
|
139
|
+
).fetchone()
|
|
140
|
+
if not plan_row:
|
|
141
|
+
raise HTTPException(404, "No plan found")
|
|
142
|
+
|
|
143
|
+
import yaml
|
|
144
|
+
plan_data = yaml.safe_load(plan_row["yaml_content"]) or {}
|
|
145
|
+
for step in plan_data.get("steps", []):
|
|
146
|
+
if step.get("id") == step_id:
|
|
147
|
+
step["status"] = "approved"
|
|
148
|
+
|
|
149
|
+
updated = yaml.dump(plan_data, default_flow_style=False)
|
|
150
|
+
conn.execute(
|
|
151
|
+
"UPDATE plans SET yaml_content = ?, updated_at = datetime('now') WHERE id = ?",
|
|
152
|
+
(updated, plan_row["id"]),
|
|
153
|
+
)
|
|
154
|
+
conn.commit()
|
|
155
|
+
|
|
156
|
+
return {"status": "reset", "step_id": step_id, "message": "Step reset to approved, ready for re-execution"}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@router.get("/tasks/{task_id}/audit-log")
|
|
160
|
+
async def get_audit_log(task_id: int, limit: int = Query(100, ge=1, le=1000)):
|
|
161
|
+
"""Get audit log entries for a task's execution."""
|
|
162
|
+
conn = get_connection()
|
|
163
|
+
|
|
164
|
+
# Get all plan IDs for this task
|
|
165
|
+
plan_rows = conn.execute(
|
|
166
|
+
"SELECT id FROM plans WHERE task_id = ?", (task_id,)
|
|
167
|
+
).fetchall()
|
|
168
|
+
if not plan_rows:
|
|
169
|
+
return {"entries": []}
|
|
170
|
+
|
|
171
|
+
plan_ids = [r["id"] for r in plan_rows]
|
|
172
|
+
|
|
173
|
+
# Get step_ids from the plans to filter audit_log
|
|
174
|
+
rows = conn.execute(
|
|
175
|
+
f"SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ?",
|
|
176
|
+
(limit,),
|
|
177
|
+
).fetchall()
|
|
178
|
+
|
|
179
|
+
entries = []
|
|
180
|
+
for row in rows:
|
|
181
|
+
entry = dict(row)
|
|
182
|
+
if entry.get("result") and isinstance(entry["result"], str):
|
|
183
|
+
entry["result"] = json.loads(entry["result"])
|
|
184
|
+
entries.append(entry)
|
|
185
|
+
|
|
186
|
+
return {"entries": entries}
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Health check endpoint for ftm-inbox backend.
|
|
3
|
-
|
|
4
|
-
GET /health
|
|
5
|
-
Returns 200 with status, version, and DB connectivity check.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import sqlite3
|
|
9
|
-
from datetime import datetime, timezone
|
|
10
|
-
|
|
11
|
-
from fastapi import APIRouter, Depends
|
|
12
|
-
from fastapi.responses import JSONResponse
|
|
13
|
-
|
|
14
|
-
router = APIRouter()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _check_db(conn: sqlite3.Connection) -> bool:
|
|
18
|
-
try:
|
|
19
|
-
conn.execute("SELECT 1").fetchone()
|
|
20
|
-
return True
|
|
21
|
-
except Exception:
|
|
22
|
-
return False
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@router.get("/health")
|
|
26
|
-
async def health_check() -> JSONResponse:
|
|
27
|
-
"""
|
|
28
|
-
Returns backend health status.
|
|
29
|
-
|
|
30
|
-
Does not depend on the adapter registry — just confirms the process
|
|
31
|
-
is alive and the database is reachable.
|
|
32
|
-
"""
|
|
33
|
-
from backend.db.connection import get_connection
|
|
34
|
-
|
|
35
|
-
db_ok = False
|
|
36
|
-
try:
|
|
37
|
-
conn = get_connection()
|
|
38
|
-
db_ok = _check_db(conn)
|
|
39
|
-
except Exception:
|
|
40
|
-
db_ok = False
|
|
41
|
-
|
|
42
|
-
status = "ok" if db_ok else "degraded"
|
|
43
|
-
code = 200 if db_ok else 503
|
|
44
|
-
|
|
45
|
-
return JSONResponse(
|
|
46
|
-
status_code=code,
|
|
47
|
-
content={
|
|
48
|
-
"status": status,
|
|
49
|
-
"db": "ok" if db_ok else "unreachable",
|
|
50
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
51
|
-
},
|
|
52
|
-
)
|
|
1
|
+
"""
|
|
2
|
+
Health check endpoint for ftm-inbox backend.
|
|
3
|
+
|
|
4
|
+
GET /health
|
|
5
|
+
Returns 200 with status, version, and DB connectivity check.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sqlite3
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, Depends
|
|
12
|
+
from fastapi.responses import JSONResponse
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _check_db(conn: sqlite3.Connection) -> bool:
|
|
18
|
+
try:
|
|
19
|
+
conn.execute("SELECT 1").fetchone()
|
|
20
|
+
return True
|
|
21
|
+
except Exception:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/health")
|
|
26
|
+
async def health_check() -> JSONResponse:
|
|
27
|
+
"""
|
|
28
|
+
Returns backend health status.
|
|
29
|
+
|
|
30
|
+
Does not depend on the adapter registry — just confirms the process
|
|
31
|
+
is alive and the database is reachable.
|
|
32
|
+
"""
|
|
33
|
+
from backend.db.connection import get_connection
|
|
34
|
+
|
|
35
|
+
db_ok = False
|
|
36
|
+
try:
|
|
37
|
+
conn = get_connection()
|
|
38
|
+
db_ok = _check_db(conn)
|
|
39
|
+
except Exception:
|
|
40
|
+
db_ok = False
|
|
41
|
+
|
|
42
|
+
status = "ok" if db_ok else "degraded"
|
|
43
|
+
code = 200 if db_ok else 503
|
|
44
|
+
|
|
45
|
+
return JSONResponse(
|
|
46
|
+
status_code=code,
|
|
47
|
+
content={
|
|
48
|
+
"status": status,
|
|
49
|
+
"db": "ok" if db_ok else "unreachable",
|
|
50
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
51
|
+
},
|
|
52
|
+
)
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Inbox API routes — paginated task listing with source/status filtering.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
|
|
9
|
-
from fastapi import APIRouter, Query
|
|
10
|
-
|
|
11
|
-
from backend.db.connection import get_connection
|
|
12
|
-
|
|
13
|
-
router = APIRouter(prefix="/api", tags=["inbox"])
|
|
14
|
-
|
|
15
|
-
_JSON_FIELDS = ("tags", "custom_fields", "raw_payload")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@router.get("/inbox")
|
|
19
|
-
async def list_inbox(
|
|
20
|
-
source: str | None = Query(None, description="Filter by source"),
|
|
21
|
-
status: str | None = Query(None, description="Filter by status"),
|
|
22
|
-
page: int = Query(1, ge=1),
|
|
23
|
-
per_page: int = Query(50, ge=1, le=200),
|
|
24
|
-
):
|
|
25
|
-
"""Return paginated inbox tasks with optional filters."""
|
|
26
|
-
conn = get_connection()
|
|
27
|
-
conditions: list[str] = []
|
|
28
|
-
params: list[str] = []
|
|
29
|
-
|
|
30
|
-
if source:
|
|
31
|
-
conditions.append("source = ?")
|
|
32
|
-
params.append(source)
|
|
33
|
-
if status:
|
|
34
|
-
conditions.append("status = ?")
|
|
35
|
-
params.append(status)
|
|
36
|
-
|
|
37
|
-
where = (" WHERE " + " AND ".join(conditions)) if conditions else ""
|
|
38
|
-
offset = (page - 1) * per_page
|
|
39
|
-
|
|
40
|
-
rows = conn.execute(
|
|
41
|
-
f"SELECT * FROM inbox{where} ORDER BY ingested_at DESC LIMIT ? OFFSET ?",
|
|
42
|
-
params + [per_page, offset],
|
|
43
|
-
).fetchall()
|
|
44
|
-
|
|
45
|
-
tasks = []
|
|
46
|
-
for row in rows:
|
|
47
|
-
task = dict(row)
|
|
48
|
-
for field in _JSON_FIELDS:
|
|
49
|
-
val = task.get(field)
|
|
50
|
-
if val and isinstance(val, str):
|
|
51
|
-
task[field] = json.loads(val)
|
|
52
|
-
tasks.append(task)
|
|
53
|
-
|
|
54
|
-
total = conn.execute(
|
|
55
|
-
f"SELECT COUNT(*) as total FROM inbox{where}", params
|
|
56
|
-
).fetchone()["total"]
|
|
57
|
-
|
|
58
|
-
return {"tasks": tasks, "total": total, "page": page, "per_page": per_page}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@router.get("/inbox/sources")
|
|
62
|
-
async def list_sources():
|
|
63
|
-
"""Return distinct source names with task counts."""
|
|
64
|
-
conn = get_connection()
|
|
65
|
-
rows = conn.execute(
|
|
66
|
-
"SELECT source, COUNT(*) as count FROM inbox GROUP BY source ORDER BY count DESC"
|
|
67
|
-
).fetchall()
|
|
68
|
-
return {"sources": [{"name": r["source"], "count": r["count"]} for r in rows]}
|
|
1
|
+
"""
|
|
2
|
+
Inbox API routes — paginated task listing with source/status filtering.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Query
|
|
10
|
+
|
|
11
|
+
from backend.db.connection import get_connection
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/api", tags=["inbox"])
|
|
14
|
+
|
|
15
|
+
_JSON_FIELDS = ("tags", "custom_fields", "raw_payload")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/inbox")
|
|
19
|
+
async def list_inbox(
|
|
20
|
+
source: str | None = Query(None, description="Filter by source"),
|
|
21
|
+
status: str | None = Query(None, description="Filter by status"),
|
|
22
|
+
page: int = Query(1, ge=1),
|
|
23
|
+
per_page: int = Query(50, ge=1, le=200),
|
|
24
|
+
):
|
|
25
|
+
"""Return paginated inbox tasks with optional filters."""
|
|
26
|
+
conn = get_connection()
|
|
27
|
+
conditions: list[str] = []
|
|
28
|
+
params: list[str] = []
|
|
29
|
+
|
|
30
|
+
if source:
|
|
31
|
+
conditions.append("source = ?")
|
|
32
|
+
params.append(source)
|
|
33
|
+
if status:
|
|
34
|
+
conditions.append("status = ?")
|
|
35
|
+
params.append(status)
|
|
36
|
+
|
|
37
|
+
where = (" WHERE " + " AND ".join(conditions)) if conditions else ""
|
|
38
|
+
offset = (page - 1) * per_page
|
|
39
|
+
|
|
40
|
+
rows = conn.execute(
|
|
41
|
+
f"SELECT * FROM inbox{where} ORDER BY ingested_at DESC LIMIT ? OFFSET ?",
|
|
42
|
+
params + [per_page, offset],
|
|
43
|
+
).fetchall()
|
|
44
|
+
|
|
45
|
+
tasks = []
|
|
46
|
+
for row in rows:
|
|
47
|
+
task = dict(row)
|
|
48
|
+
for field in _JSON_FIELDS:
|
|
49
|
+
val = task.get(field)
|
|
50
|
+
if val and isinstance(val, str):
|
|
51
|
+
task[field] = json.loads(val)
|
|
52
|
+
tasks.append(task)
|
|
53
|
+
|
|
54
|
+
total = conn.execute(
|
|
55
|
+
f"SELECT COUNT(*) as total FROM inbox{where}", params
|
|
56
|
+
).fetchone()["total"]
|
|
57
|
+
|
|
58
|
+
return {"tasks": tasks, "total": total, "page": page, "per_page": per_page}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.get("/inbox/sources")
|
|
62
|
+
async def list_sources():
|
|
63
|
+
"""Return distinct source names with task counts."""
|
|
64
|
+
conn = get_connection()
|
|
65
|
+
rows = conn.execute(
|
|
66
|
+
"SELECT source, COUNT(*) as count FROM inbox GROUP BY source ORDER BY count DESC"
|
|
67
|
+
).fetchall()
|
|
68
|
+
return {"sources": [{"name": r["source"], "count": r["count"]} for r in rows]}
|