@mndrk/agx 2.0.38 → 2.0.40
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/cloud-runtime/standalone/.next/BUILD_ID +1 -1
- package/cloud-runtime/standalone/.next/build-manifest.json +4 -4
- package/cloud-runtime/standalone/.next/prerender-manifest.json +3 -3
- package/cloud-runtime/standalone/.next/required-server-files.json +4 -4
- package/cloud-runtime/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/_global-error.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/_global-error.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/_not-found.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/_not-found.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/agents/[id]/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/agents/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/agents.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/agents.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/agents.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/agents.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/agents.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/agents.segments/agents/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/agents.segments/agents.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/agent-specs/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/agents/[id]/profile/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/agents/export/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/automations/create/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/automations/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/chat/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/chat-runs/[id]/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/chat-runs/[id]/signal/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/chat-runs/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/file-search/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/graphs/[graphId]/nodes/[nodeId]/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/health/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/history/status/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/knowledge-notes/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/learnings/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/logs/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/memories/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/messages/[id]/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/migrate/teams-to-projects/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/migrate/workspaces-to-projects/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/orchestrator/tasks/[taskId]/cancel/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/orchestrator/tasks/[taskId]/signal/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/orchestrator/tasks/[taskId]/start/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/orchestrator/tasks/[taskId]/status/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/participants/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/[id]/agents/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/[id]/memory/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/[id]/migrate-v1/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/[id]/migrate-v2/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/[id]/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/[id]/skills/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/[id]/threads/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/[id]/variables/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/prompt-jobs/[id]/cancel/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/prompt-jobs/[id]/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/prompt-jobs/[id]/runs/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/prompt-jobs/agents/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/prompt-jobs/poll/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/prompt-jobs/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/queue/complete/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/reactions/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/repos/[id]/knowledge/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/schedules/debug/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/schedules/poll/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/schedules/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/search/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/skills/assign/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/skills/available/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/skills/detail/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/skills/history/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/skills/learn/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/skills/unlearn/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/summarize/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/system/db-status/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/task-drafts/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/comments/[commentId]/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/comments/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/costs/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/dependencies/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/events/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/history/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/metrics/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/pause/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/replan/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/restart/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/resume/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/rollback/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/schedule/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/start/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/graph/stop/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/heartbeat/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/history/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/logs/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/logs/stream/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/nodes/[nodeId]/comments/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/nodes/[nodeId]/complete/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/nodes/[nodeId]/fail/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/nodes/[nodeId]/resume/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/nodes/[nodeId]/start/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/nodes/[nodeId]/stop/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/nodes/[nodeId]/verify/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/[id]/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/assign-orphans/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/extract/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/tasks/stream/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/thread-repos/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/threads/knowledge/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/threads/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/api/user-settings/route.js.nft.json +1 -1
- package/cloud-runtime/standalone/.next/server/app/automations/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/automations.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/automations.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/automations.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/automations.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/automations.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/automations.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/automations.segments/automations/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/automations.segments/automations.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/board/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/board.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/board.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/board.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/board.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/board.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/board.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/board.segments/board.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/execution-graph/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/execution-graph.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/execution-graph.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/execution-graph.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/execution-graph.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/execution-graph.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/execution-graph.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/execution-graph.segments/execution-graph/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/execution-graph.segments/execution-graph.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/index.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/index.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects/[slug]/automations/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects/[slug]/graph/[taskId]/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects/[slug]/knowledge/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects/[slug]/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects/[slug]/thread/[threadId]/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects/orphans/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.segments/projects/orphans/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.segments/projects/orphans.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects/orphans.segments/projects.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/projects.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects.segments/projects/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/projects.segments/projects.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/settings.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/settings.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/settings.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/skills/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/skills.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/skills.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/skills.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/skills.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/skills.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/skills.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/skills.segments/skills/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/skills.segments/skills.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/status/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/status.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/status.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/status.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/status.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/status.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/status.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/status.segments/status/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/status.segments/status.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/thread/[id]/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/welcome/page/build-manifest.json +2 -2
- package/cloud-runtime/standalone/.next/server/app/welcome.html +2 -2
- package/cloud-runtime/standalone/.next/server/app/welcome.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/welcome.segments/_full.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/welcome.segments/_head.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/welcome.segments/_index.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/welcome.segments/_tree.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/welcome.segments/welcome/__PAGE__.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/app/welcome.segments/welcome.segment.rsc +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/{[externals]__986fcdb7._.js → [externals]__bc6c7111._.js} +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__01cd082e._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__0936925d._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__09ca81e1._.js +101 -0
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__0c3dd73b._.js +6 -6
- package/cloud-runtime/standalone/.next/server/chunks/{[root-of-the-server]__f9cff4b0._.js → [root-of-the-server]__1017e012._.js} +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__1ac3236d._.js +25 -0
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__1b0bb735._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__1c86bf6e._.js +5 -5
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__20c58b41._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__2126c763._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/{[root-of-the-server]__762ab29c._.js → [root-of-the-server]__236c35bb._.js} +4 -4
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__255b11f2._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__277ed37d._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__2f06f568._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__30bd0c87._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__374cd94c._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__3785024c._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__379604d4._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__3c8f1de6._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__3d0df5a8._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__4014ed70._.js +7 -7
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__42fcb81c._.js +16 -16
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__4a3cd6ac._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__4e522535._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__50c24784._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__50ddd3ce._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__529a6e1c._.js +12 -12
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__59d1cdd8._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__628d686b._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__644e6285._.js +3 -3
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__65755104._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__73c20995._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__8125bbc3._.js +6 -6
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__85275b88._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__89666394._.js +3 -3
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__8c0fb154._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__8f5cac13._.js +3 -3
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__91b22098._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__92924218._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__96ae701e._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__98ce983b._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__9a9fd39f._.js +6 -6
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__9ad4e385._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__a01ddd0b._.js +3 -3
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__a099c992._.js +14 -14
- package/cloud-runtime/standalone/.next/server/chunks/{[root-of-the-server]__ab7343c8._.js → [root-of-the-server]__a1e62918._.js} +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__a9b949c3._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__af7a73fd._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__bd43017b._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__bf6fb108._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__c122c54a._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__c4ea4921._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__d57e800e._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__db469f1b._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/{[root-of-the-server]__da3a1ce7._.js → [root-of-the-server]__dc542063._.js} +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__dec3e1b7._.js +4 -4
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__f1147a4a._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__f5597fea._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__f8c94cb3._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__f9f7f2df._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__fa79d53f._.js +11 -11
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__fb14cd4a._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__fcf0b40a._.js +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__ffb21023._.js +2 -2
- package/cloud-runtime/standalone/.next/server/chunks/_22fe5fd2._.js +3 -0
- package/cloud-runtime/standalone/.next/server/chunks/lib_2492d514._.js +81 -0
- package/cloud-runtime/standalone/.next/server/chunks/lib_281e5787._.js +16 -0
- package/cloud-runtime/standalone/.next/server/chunks/lib_63067e21._.js +16 -0
- package/cloud-runtime/standalone/.next/server/chunks/lib_orchestrator_chat-processor_ts_4c335719._.js +134 -0
- package/cloud-runtime/standalone/.next/server/chunks/lib_sqlite-query-adapter_ts_3ea4d849._.js +82 -21
- package/cloud-runtime/standalone/.next/server/chunks/ssr/node_modules_next_dist_61a87db9._.js +1 -1
- package/cloud-runtime/standalone/.next/server/instrumentation.js +1 -1
- package/cloud-runtime/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/cloud-runtime/standalone/.next/server/middleware-manifest.json +5 -5
- package/cloud-runtime/standalone/.next/server/pages/404.html +2 -2
- package/cloud-runtime/standalone/.next/server/pages/500.html +2 -2
- package/cloud-runtime/standalone/.next/server/server-reference-manifest.js +1 -1
- package/cloud-runtime/standalone/.next/server/server-reference-manifest.json +1 -1
- package/cloud-runtime/standalone/.next/static/chunks/{dfff51033c303fc7.js → ee5f1457fbc593e1.js} +1 -1
- package/cloud-runtime/standalone/.next/static/chunks/{turbopack-22475f0dd0c18f92.js → turbopack-e1d640f2fbe4fa5e.js} +1 -1
- package/cloud-runtime/standalone/coverage/clover.xml +1208 -0
- package/cloud-runtime/standalone/coverage/coverage-final.json +29 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/audit/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/audit/route.ts.html +208 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/auth/[...nextauth]/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/auth/[...nextauth]/route.ts.html +166 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/auth/daemon-secret/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/auth/daemon-secret/route.ts.html +532 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/auth/status/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/auth/status/route.ts.html +178 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/learnings/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/learnings/route.ts.html +262 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/logs/stream/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/logs/stream/route.ts.html +448 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/queue/complete/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/queue/complete/route.ts.html +331 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/queue/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/queue/route.ts.html +505 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/stage-prompts/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/stage-prompts/route.ts.html +412 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/[id]/advance/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/[id]/advance/route.ts.html +304 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/[id]/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/[id]/logs/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/[id]/logs/route.ts.html +202 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/[id]/route.ts.html +373 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/route.ts.html +499 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/stream/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/app/api/tasks/stream/route.ts.html +349 -0
- package/cloud-runtime/standalone/coverage/lcov-report/base.css +224 -0
- package/cloud-runtime/standalone/coverage/lcov-report/block-navigation.js +87 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/AuthProvider.tsx.html +259 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/ChatInterface.tsx.html +1228 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/KanbanBoard.tsx.html +1024 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/Layout.tsx.html +211 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/LearningsPanel.tsx.html +535 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/LogTimeline.tsx.html +415 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/SortableTaskCard.tsx.html +358 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/StagePills.tsx.html +439 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/TaskCard.tsx.html +514 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/TaskCardOverlay.tsx.html +256 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/TaskDetail.tsx.html +622 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/TaskList.tsx.html +253 -0
- package/cloud-runtime/standalone/coverage/lcov-report/components/index.html +281 -0
- package/cloud-runtime/standalone/coverage/lcov-report/favicon.png +0 -0
- package/cloud-runtime/standalone/coverage/lcov-report/hooks/index.html +116 -0
- package/cloud-runtime/standalone/coverage/lcov-report/hooks/useTasks.ts.html +1042 -0
- package/cloud-runtime/standalone/coverage/lcov-report/index.html +341 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/auth-client.ts.html +202 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/auth-server.ts.html +172 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/auth.ts.html +265 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/db.ts.html +1252 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/index.html +131 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/orchestrator.ts.html +409 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/security.ts.html +1165 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/supabase-server.ts.html +175 -0
- package/cloud-runtime/standalone/coverage/lcov-report/lib/supabase.ts.html +157 -0
- package/cloud-runtime/standalone/coverage/lcov-report/prettify.css +1 -0
- package/cloud-runtime/standalone/coverage/lcov-report/prettify.js +2 -0
- package/cloud-runtime/standalone/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/cloud-runtime/standalone/coverage/lcov-report/sorter.js +210 -0
- package/cloud-runtime/standalone/coverage/lcov.info +2386 -0
- package/cloud-runtime/standalone/docs/LIMITS.md +63 -0
- package/cloud-runtime/standalone/docs/architecture/ADR-001-hybrid-to-full-sqlite.md +345 -0
- package/cloud-runtime/standalone/docs/baseline/baseline-report.json +1009 -0
- package/cloud-runtime/standalone/docs/baseline/critical-queries.md +105 -0
- package/cloud-runtime/standalone/docs/baseline/lock-metrics.json +21 -0
- package/cloud-runtime/standalone/docs/baseline/read-latency.json +146 -0
- package/cloud-runtime/standalone/docs/baseline/restore-time.json +10 -0
- package/cloud-runtime/standalone/docs/baseline/write-metrics.json +803 -0
- package/cloud-runtime/standalone/docs/decisions/sqlite-migration-adr.md +327 -0
- package/cloud-runtime/standalone/docs/error-code-mapping.md +74 -0
- package/cloud-runtime/standalone/docs/migration-plan.md +120 -0
- package/cloud-runtime/standalone/docs/migration-spec.md +345 -0
- package/cloud-runtime/standalone/docs/pg-sqlite-compatibility-matrix.md +554 -0
- package/cloud-runtime/standalone/docs/project-agent-migration-status.md +229 -0
- package/cloud-runtime/standalone/docs/runbook-shadow-read.md +66 -0
- package/cloud-runtime/standalone/docs/runbook.md +155 -0
- package/cloud-runtime/standalone/docs/specs/cli-postgres-removal.md +69 -0
- package/cloud-runtime/standalone/docs/specs/thread-mentions.md +53 -0
- package/cloud-runtime/standalone/docs/superpowers/plans/2026-04-01-prompt-scheduler.md +1907 -0
- package/cloud-runtime/standalone/docs/superpowers/plans/2026-04-02-sqlite-migration.md +1047 -0
- package/cloud-runtime/standalone/docs/transcript.txt +282 -0
- package/cloud-runtime/standalone/docs/ux/GlobalChatFlow.storyboard +23 -0
- package/cloud-runtime/standalone/docs/ux/assistant-chat-cli.md +32 -0
- package/cloud-runtime/standalone/instrumentation.ts +34 -0
- package/cloud-runtime/standalone/mcp/dist/constants.js +66 -0
- package/cloud-runtime/standalone/mcp/dist/db.js +220 -0
- package/cloud-runtime/standalone/mcp/dist/index.js +7 -0
- package/cloud-runtime/standalone/mcp/dist/security.js +18 -0
- package/cloud-runtime/standalone/mcp/dist/server.js +240 -0
- package/cloud-runtime/standalone/mcp/dist/task-context.js +287 -0
- package/cloud-runtime/standalone/mcp/dist/test-client.js +82 -0
- package/cloud-runtime/standalone/mcp/dist/tools/audit.js +69 -0
- package/cloud-runtime/standalone/mcp/dist/tools/learnings.js +88 -0
- package/cloud-runtime/standalone/mcp/dist/tools/queue.js +312 -0
- package/cloud-runtime/standalone/mcp/dist/tools/tasks.js +244 -0
- package/cloud-runtime/standalone/mcp/dist/types.js +74 -0
- package/cloud-runtime/standalone/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
- package/cloud-runtime/standalone/node_modules/@img/{sharp-linux-x64 → sharp-darwin-arm64}/package.json +7 -13
- package/cloud-runtime/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/README.md +2 -2
- package/cloud-runtime/standalone/node_modules/@img/{sharp-libvips-linuxmusl-x64 → sharp-libvips-darwin-arm64}/lib/glib-2.0/include/glibconfig.h +8 -9
- package/cloud-runtime/standalone/node_modules/@img/{sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 → sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib} +0 -0
- package/cloud-runtime/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
- package/cloud-runtime/standalone/notes/comments-context-demo.md +141 -0
- package/cloud-runtime/standalone/notes/comments-context-plan.md +119 -0
- package/cloud-runtime/standalone/notes/context-audit.md +21 -0
- package/cloud-runtime/standalone/notes/project-layer-plan.md +30 -0
- package/cloud-runtime/standalone/notes/project-layer.md +123 -0
- package/cloud-runtime/standalone/notes/temporal-migration-design.md +199 -0
- package/cloud-runtime/standalone/playwright-report/data/00d55996f37c1506b90144c85493dd85032c13e5.png +0 -0
- package/cloud-runtime/standalone/playwright-report/data/0b9d409e57237ae111d7ba258d3dfe64dc368456.png +0 -0
- package/cloud-runtime/standalone/playwright-report/data/b33d5e80a15bd1deda4415b9d318ef73f581c950.png +0 -0
- package/cloud-runtime/standalone/playwright-report/data/b55684161aa440d0614595e13c91338f0420abbb.md +131 -0
- package/cloud-runtime/standalone/playwright-report/data/b9913957ae07e7565c38ddd71215be79b1ceb017.png +0 -0
- package/cloud-runtime/standalone/playwright-report/data/c3538be8ebbebc9fe4a7df8f12f04483af4a0d91.png +0 -0
- package/cloud-runtime/standalone/playwright-report/data/fe638f64ff5e36f1c30325564565662d3f57da87.md +180 -0
- package/cloud-runtime/standalone/playwright-report/index.html +85 -0
- package/cloud-runtime/standalone/server.js +1 -1
- package/cloud-runtime/standalone/test-results/auth-Authentication-Flow-S-aff25-sion-across-page-navigation-chromium/test-failed-1.png +0 -0
- package/cloud-runtime/standalone/test-results/comments-Task-comments-add-4dc59-nd-persists-it-for-the-task-chromium/error-context.md +131 -0
- package/cloud-runtime/standalone/test-results/comments-Task-comments-add-4dc59-nd-persists-it-for-the-task-chromium/test-failed-1.png +0 -0
- package/cloud-runtime/standalone/test-results/kanban-Kanban-Board-Stage--4082a-er-planning-after-ideation--chromium/error-context.md +180 -0
- package/cloud-runtime/standalone/test-results/kanban-Kanban-Board-Stage--4082a-er-planning-after-ideation--chromium/test-failed-1.png +0 -0
- package/cloud-runtime/standalone/test-results/kanban-Kanban-Board-Stage-Columns-displays-all-9-SDLC-stages-chromium/error-context.md +180 -0
- package/cloud-runtime/standalone/test-results/kanban-Kanban-Board-Stage-Columns-displays-all-9-SDLC-stages-chromium/test-failed-1.png +0 -0
- package/cloud-runtime/standalone/test-results/kanban-Kanban-Board-Task-D-b6d98-ys-tasks-in-correct-columns-chromium/error-context.md +180 -0
- package/cloud-runtime/standalone/test-results/kanban-Kanban-Board-Task-D-b6d98-ys-tasks-in-correct-columns-chromium/test-failed-1.png +0 -0
- package/cloud-runtime/standalone/test-results/kanban-Kanban-Board-Task-Display-shows-task-count-per-column-chromium/test-failed-1.png +0 -0
- package/cloud-runtime/standalone/test-results/kanban-Task-Lifecycle-can-advance-task-through-all-stages-chromium/test-failed-1.png +0 -0
- package/cloud-runtime/standalone/tsconfig.json +1 -2
- package/cloud-runtime/standalone/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/cloud-runtime/standalone/.next/server/chunks/[root-of-the-server]__4bce7db7._.js +0 -101
- package/cloud-runtime/standalone/.next/server/chunks/_17e53c87._.js +0 -3
- package/cloud-runtime/standalone/.next/server/chunks/lib_ea45fe73._.js +0 -94
- package/cloud-runtime/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +0 -221
- package/cloud-runtime/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
- package/cloud-runtime/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
- package/cloud-runtime/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/cloud-runtime/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
- package/cloud-runtime/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
- package/cloud-runtime/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
- package/cloud-runtime/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/cloud-runtime/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
- /package/cloud-runtime/standalone/.next/static/{V5ruh627UpcC-LTWDgYk8 → tIFXRWCxPD896ykJ6O--N}/_buildManifest.js +0 -0
- /package/cloud-runtime/standalone/.next/static/{V5ruh627UpcC-LTWDgYk8 → tIFXRWCxPD896ykJ6O--N}/_clientMiddlewareManifest.json +0 -0
- /package/cloud-runtime/standalone/.next/static/{V5ruh627UpcC-LTWDgYk8 → tIFXRWCxPD896ykJ6O--N}/_ssgManifest.js +0 -0
- /package/cloud-runtime/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/index.js +0 -0
- /package/cloud-runtime/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/versions.json +0 -0
|
@@ -0,0 +1,1907 @@
|
|
|
1
|
+
# Prompt Scheduler Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Replace agx-cloud's graph-based automations with a simple prompt+schedule+CLI scheduler, where users create PromptJobs that run on cron schedules via any CLI (claude, codex, gemini, etc.).
|
|
6
|
+
|
|
7
|
+
**Architecture:** The existing graph engine becomes internal plumbing — a PromptJob maps to a single-node execution graph. New `prompt_runs` table tracks every execution. The runner polls for queued runs, shells out to the target CLI, streams output, and checks a cancel flag every 5s. The existing AutomationsBoard UI is replaced with a PromptJob-focused board.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Next.js 16 (app router), SQLite (better-sqlite3), React 19, TailwindCSS 4, cron-parser, existing graph engine internals.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
### New Files
|
|
16
|
+
| File | Responsibility |
|
|
17
|
+
|------|---------------|
|
|
18
|
+
| `src/prompt-scheduler/types.ts` | PromptJob and PromptRun types |
|
|
19
|
+
| `src/prompt-scheduler/cron.ts` | Cron parsing, next-run computation (wraps cron-parser + nl-schedule) |
|
|
20
|
+
| `src/prompt-scheduler/engine.ts` | Core scheduling logic: find due jobs, create runs, compute next tick |
|
|
21
|
+
| `src/prompt-scheduler/runner.ts` | CLI execution: spawn child process, capture output, cancel check |
|
|
22
|
+
| `src/prompt-scheduler/store.ts` | Database operations for prompt_jobs and prompt_runs tables |
|
|
23
|
+
| `db/sqlite/002_prompt_scheduler_schema.sql` | Migration: prompt_jobs + prompt_runs tables |
|
|
24
|
+
| `app/api/prompt-jobs/route.ts` | GET (list) + POST (create) prompt jobs |
|
|
25
|
+
| `app/api/prompt-jobs/[id]/route.ts` | GET (detail) + PATCH (update) + DELETE prompt job |
|
|
26
|
+
| `app/api/prompt-jobs/[id]/runs/route.ts` | GET runs for a job |
|
|
27
|
+
| `app/api/prompt-jobs/[id]/cancel/route.ts` | POST cancel current run |
|
|
28
|
+
| `app/api/prompt-jobs/poll/route.ts` | POST poll for due jobs and execute |
|
|
29
|
+
| `components/PromptJobBoard.tsx` | Main UI: job list, create form, run log, controls |
|
|
30
|
+
| `hooks/usePromptJobs.ts` | Data fetching hook for prompt jobs + runs |
|
|
31
|
+
| `__tests__/prompt-scheduler/cron.test.ts` | Cron parsing tests |
|
|
32
|
+
| `__tests__/prompt-scheduler/engine.test.ts` | Scheduling engine tests |
|
|
33
|
+
| `__tests__/prompt-scheduler/runner.test.ts` | Runner execution tests |
|
|
34
|
+
| `__tests__/prompt-scheduler/store.test.ts` | Store CRUD tests |
|
|
35
|
+
| `__tests__/prompt-scheduler/api.test.ts` | API route integration tests |
|
|
36
|
+
|
|
37
|
+
### Modified Files
|
|
38
|
+
| File | Change |
|
|
39
|
+
|------|--------|
|
|
40
|
+
| `app/automations/page.tsx` | Replace `AutomationsBoard` with `PromptJobBoard` |
|
|
41
|
+
| `db/sqlite/001_agx_board_schema.sql` | Reference only — no changes, new migration file instead |
|
|
42
|
+
|
|
43
|
+
### Preserved (internal plumbing, untouched)
|
|
44
|
+
| File | Why |
|
|
45
|
+
|------|-----|
|
|
46
|
+
| `src/graph/schedule.ts` | Still used internally by graph engine for existing tasks |
|
|
47
|
+
| `src/graph/schedule-runner.ts` | Prompt scheduler calls into this for graph-level execution |
|
|
48
|
+
| `src/graph/nl-schedule.ts` | Reused by `src/prompt-scheduler/cron.ts` |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Task 1: Database Schema — prompt_jobs and prompt_runs tables
|
|
53
|
+
|
|
54
|
+
**Files:**
|
|
55
|
+
- Create: `db/sqlite/002_prompt_scheduler_schema.sql`
|
|
56
|
+
- Create: `src/prompt-scheduler/types.ts`
|
|
57
|
+
|
|
58
|
+
- [ ] **Step 1: Define TypeScript types**
|
|
59
|
+
|
|
60
|
+
Create `src/prompt-scheduler/types.ts`:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
export type PromptJobState = 'active' | 'paused' | 'stopped';
|
|
64
|
+
export type OverlapPolicy = 'skip' | 'queue' | 'allow';
|
|
65
|
+
export type RunStatus = 'queued' | 'running' | 'success' | 'failed' | 'cancelled';
|
|
66
|
+
|
|
67
|
+
export interface PromptJob {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
prompt: string;
|
|
71
|
+
cli: string;
|
|
72
|
+
cronExpr: string;
|
|
73
|
+
cadence: string;
|
|
74
|
+
state: PromptJobState;
|
|
75
|
+
overlapPolicy: OverlapPolicy;
|
|
76
|
+
cancelCheckSec: number;
|
|
77
|
+
nextRunAt: number | null;
|
|
78
|
+
lastRunAt: number | null;
|
|
79
|
+
lastOutcome: RunStatus | null;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
updatedAt: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PromptRun {
|
|
85
|
+
id: string;
|
|
86
|
+
jobId: string;
|
|
87
|
+
status: RunStatus;
|
|
88
|
+
output: string | null;
|
|
89
|
+
error: string | null;
|
|
90
|
+
durationMs: number | null;
|
|
91
|
+
startedAt: string | null;
|
|
92
|
+
finishedAt: string | null;
|
|
93
|
+
cancelledAt: string | null;
|
|
94
|
+
createdAt: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CreatePromptJobInput {
|
|
98
|
+
name: string;
|
|
99
|
+
prompt: string;
|
|
100
|
+
cli: string;
|
|
101
|
+
cadence: string;
|
|
102
|
+
overlapPolicy?: OverlapPolicy;
|
|
103
|
+
cancelCheckSec?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface UpdatePromptJobInput {
|
|
107
|
+
name?: string;
|
|
108
|
+
prompt?: string;
|
|
109
|
+
cli?: string;
|
|
110
|
+
cadence?: string;
|
|
111
|
+
state?: PromptJobState;
|
|
112
|
+
overlapPolicy?: OverlapPolicy;
|
|
113
|
+
cancelCheckSec?: number;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- [ ] **Step 2: Write the SQL migration**
|
|
118
|
+
|
|
119
|
+
Create `db/sqlite/002_prompt_scheduler_schema.sql`:
|
|
120
|
+
|
|
121
|
+
```sql
|
|
122
|
+
-- Prompt Scheduler tables
|
|
123
|
+
CREATE TABLE IF NOT EXISTS prompt_jobs (
|
|
124
|
+
id TEXT PRIMARY KEY,
|
|
125
|
+
name TEXT NOT NULL,
|
|
126
|
+
prompt TEXT NOT NULL,
|
|
127
|
+
cli TEXT NOT NULL DEFAULT 'claude',
|
|
128
|
+
cron_expr TEXT NOT NULL,
|
|
129
|
+
cadence TEXT NOT NULL DEFAULT '',
|
|
130
|
+
state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'paused', 'stopped')),
|
|
131
|
+
overlap_policy TEXT NOT NULL DEFAULT 'skip' CHECK (overlap_policy IN ('skip', 'queue', 'allow')),
|
|
132
|
+
cancel_check_sec INTEGER NOT NULL DEFAULT 5,
|
|
133
|
+
next_run_at INTEGER,
|
|
134
|
+
last_run_at INTEGER,
|
|
135
|
+
last_outcome TEXT CHECK (last_outcome IS NULL OR last_outcome IN ('queued', 'running', 'success', 'failed', 'cancelled')),
|
|
136
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
137
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_prompt_jobs_state ON prompt_jobs(state);
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_prompt_jobs_next_run_at ON prompt_jobs(next_run_at);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS prompt_runs (
|
|
144
|
+
id TEXT PRIMARY KEY,
|
|
145
|
+
job_id TEXT NOT NULL REFERENCES prompt_jobs(id) ON DELETE CASCADE,
|
|
146
|
+
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'running', 'success', 'failed', 'cancelled')),
|
|
147
|
+
output TEXT,
|
|
148
|
+
error TEXT,
|
|
149
|
+
duration_ms INTEGER,
|
|
150
|
+
started_at TEXT,
|
|
151
|
+
finished_at TEXT,
|
|
152
|
+
cancelled_at TEXT,
|
|
153
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_prompt_runs_job_id ON prompt_runs(job_id);
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_prompt_runs_status ON prompt_runs(status);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- [ ] **Step 3: Commit**
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git add src/prompt-scheduler/types.ts db/sqlite/002_prompt_scheduler_schema.sql
|
|
164
|
+
git commit -m "feat: add prompt_jobs and prompt_runs schema + types"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Task 2: Store — CRUD operations for prompt_jobs and prompt_runs
|
|
170
|
+
|
|
171
|
+
**Files:**
|
|
172
|
+
- Create: `src/prompt-scheduler/store.ts`
|
|
173
|
+
- Create: `__tests__/prompt-scheduler/store.test.ts`
|
|
174
|
+
|
|
175
|
+
- [ ] **Step 1: Write failing tests for store operations**
|
|
176
|
+
|
|
177
|
+
Create `__tests__/prompt-scheduler/store.test.ts`:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import Database from 'better-sqlite3';
|
|
181
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
182
|
+
import { PromptJobStore } from '@/src/prompt-scheduler/store';
|
|
183
|
+
import { readFileSync } from 'fs';
|
|
184
|
+
import path from 'path';
|
|
185
|
+
|
|
186
|
+
function createTestDb(): Database.Database {
|
|
187
|
+
const db = new Database(':memory:');
|
|
188
|
+
const boardSchema = readFileSync(
|
|
189
|
+
path.join(__dirname, '../../db/sqlite/001_agx_board_schema.sql'),
|
|
190
|
+
'utf-8'
|
|
191
|
+
);
|
|
192
|
+
const promptSchema = readFileSync(
|
|
193
|
+
path.join(__dirname, '../../db/sqlite/002_prompt_scheduler_schema.sql'),
|
|
194
|
+
'utf-8'
|
|
195
|
+
);
|
|
196
|
+
db.exec(boardSchema);
|
|
197
|
+
db.exec(promptSchema);
|
|
198
|
+
return db;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
describe('PromptJobStore', () => {
|
|
202
|
+
let db: Database.Database;
|
|
203
|
+
let store: PromptJobStore;
|
|
204
|
+
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
db = createTestDb();
|
|
207
|
+
store = new PromptJobStore(db);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('createJob', () => {
|
|
211
|
+
it('creates a job and returns it with computed fields', () => {
|
|
212
|
+
const job = store.createJob({
|
|
213
|
+
name: 'Test Job',
|
|
214
|
+
prompt: 'Summarize my inbox',
|
|
215
|
+
cli: 'claude',
|
|
216
|
+
cronExpr: '0 9 * * *',
|
|
217
|
+
cadence: 'Daily at 9am',
|
|
218
|
+
});
|
|
219
|
+
expect(job.id).toBeTruthy();
|
|
220
|
+
expect(job.name).toBe('Test Job');
|
|
221
|
+
expect(job.prompt).toBe('Summarize my inbox');
|
|
222
|
+
expect(job.cli).toBe('claude');
|
|
223
|
+
expect(job.cronExpr).toBe('0 9 * * *');
|
|
224
|
+
expect(job.state).toBe('active');
|
|
225
|
+
expect(job.nextRunAt).toBeGreaterThan(Date.now() - 60000);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('listJobs', () => {
|
|
230
|
+
it('returns all non-stopped jobs by default', () => {
|
|
231
|
+
store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: 'Hourly' });
|
|
232
|
+
store.createJob({ name: 'B', prompt: 'b', cli: 'codex', cronExpr: '0 9 * * *', cadence: 'Daily' });
|
|
233
|
+
const jobs = store.listJobs();
|
|
234
|
+
expect(jobs).toHaveLength(2);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('filters by state', () => {
|
|
238
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
239
|
+
store.updateJob(job.id, { state: 'paused' });
|
|
240
|
+
expect(store.listJobs({ state: 'active' })).toHaveLength(0);
|
|
241
|
+
expect(store.listJobs({ state: 'paused' })).toHaveLength(1);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('updateJob', () => {
|
|
246
|
+
it('updates fields and bumps updatedAt', () => {
|
|
247
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
248
|
+
const updated = store.updateJob(job.id, { name: 'B', state: 'paused' });
|
|
249
|
+
expect(updated.name).toBe('B');
|
|
250
|
+
expect(updated.state).toBe('paused');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('deleteJob', () => {
|
|
255
|
+
it('deletes job and its runs', () => {
|
|
256
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
257
|
+
store.createRun(job.id);
|
|
258
|
+
store.deleteJob(job.id);
|
|
259
|
+
expect(store.listJobs()).toHaveLength(0);
|
|
260
|
+
expect(store.listRuns(job.id)).toHaveLength(0);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('runs', () => {
|
|
265
|
+
it('creates a run in queued status', () => {
|
|
266
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
267
|
+
const run = store.createRun(job.id);
|
|
268
|
+
expect(run.status).toBe('queued');
|
|
269
|
+
expect(run.jobId).toBe(job.id);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('updates run status and output', () => {
|
|
273
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
274
|
+
const run = store.createRun(job.id);
|
|
275
|
+
const updated = store.updateRun(run.id, {
|
|
276
|
+
status: 'success',
|
|
277
|
+
output: 'Done!',
|
|
278
|
+
durationMs: 1500,
|
|
279
|
+
finishedAt: new Date().toISOString(),
|
|
280
|
+
});
|
|
281
|
+
expect(updated.status).toBe('success');
|
|
282
|
+
expect(updated.output).toBe('Done!');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('lists runs for a job ordered by created_at desc', () => {
|
|
286
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
287
|
+
store.createRun(job.id);
|
|
288
|
+
store.createRun(job.id);
|
|
289
|
+
const runs = store.listRuns(job.id);
|
|
290
|
+
expect(runs).toHaveLength(2);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('getDueJobs', () => {
|
|
295
|
+
it('returns active jobs where nextRunAt <= now', () => {
|
|
296
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
297
|
+
// Force nextRunAt to the past
|
|
298
|
+
db.prepare('UPDATE prompt_jobs SET next_run_at = ? WHERE id = ?').run(Date.now() - 60000, job.id);
|
|
299
|
+
const due = store.getDueJobs();
|
|
300
|
+
expect(due).toHaveLength(1);
|
|
301
|
+
expect(due[0].id).toBe(job.id);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('skips paused jobs', () => {
|
|
305
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
306
|
+
db.prepare('UPDATE prompt_jobs SET next_run_at = ?, state = ? WHERE id = ?').run(Date.now() - 60000, 'paused', job.id);
|
|
307
|
+
expect(store.getDueJobs()).toHaveLength(0);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('hasRunningRun', () => {
|
|
312
|
+
it('returns true if job has a running run', () => {
|
|
313
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
314
|
+
const run = store.createRun(job.id);
|
|
315
|
+
store.updateRun(run.id, { status: 'running', startedAt: new Date().toISOString() });
|
|
316
|
+
expect(store.hasRunningRun(job.id)).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('returns false if no running runs', () => {
|
|
320
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
321
|
+
expect(store.hasRunningRun(job.id)).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('isRunCancelled', () => {
|
|
326
|
+
it('returns false for non-cancelled run', () => {
|
|
327
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
328
|
+
const run = store.createRun(job.id);
|
|
329
|
+
expect(store.isRunCancelled(run.id)).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('returns true for cancelled run', () => {
|
|
333
|
+
const job = store.createJob({ name: 'A', prompt: 'a', cli: 'claude', cronExpr: '0 * * * *', cadence: '' });
|
|
334
|
+
const run = store.createRun(job.id);
|
|
335
|
+
store.updateRun(run.id, { status: 'cancelled', cancelledAt: new Date().toISOString() });
|
|
336
|
+
expect(store.isRunCancelled(run.id)).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npx vitest run __tests__/prompt-scheduler/store.test.ts
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Expected: FAIL — `Cannot find module '@/src/prompt-scheduler/store'`
|
|
349
|
+
|
|
350
|
+
- [ ] **Step 3: Implement the store**
|
|
351
|
+
|
|
352
|
+
Create `src/prompt-scheduler/store.ts`:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import Database from 'better-sqlite3';
|
|
356
|
+
import { randomUUID } from 'crypto';
|
|
357
|
+
import type { PromptJob, PromptRun, RunStatus } from './types';
|
|
358
|
+
import { computeNextTickFromCron } from '../graph/scheduler';
|
|
359
|
+
|
|
360
|
+
interface CreateJobInput {
|
|
361
|
+
name: string;
|
|
362
|
+
prompt: string;
|
|
363
|
+
cli: string;
|
|
364
|
+
cronExpr: string;
|
|
365
|
+
cadence: string;
|
|
366
|
+
overlapPolicy?: string;
|
|
367
|
+
cancelCheckSec?: number;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface ListJobsFilter {
|
|
371
|
+
state?: string;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
interface UpdateRunInput {
|
|
375
|
+
status?: RunStatus;
|
|
376
|
+
output?: string;
|
|
377
|
+
error?: string;
|
|
378
|
+
durationMs?: number;
|
|
379
|
+
startedAt?: string;
|
|
380
|
+
finishedAt?: string;
|
|
381
|
+
cancelledAt?: string;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function rowToJob(row: Record<string, unknown>): PromptJob {
|
|
385
|
+
return {
|
|
386
|
+
id: row.id as string,
|
|
387
|
+
name: row.name as string,
|
|
388
|
+
prompt: row.prompt as string,
|
|
389
|
+
cli: row.cli as string,
|
|
390
|
+
cronExpr: row.cron_expr as string,
|
|
391
|
+
cadence: row.cadence as string,
|
|
392
|
+
state: row.state as PromptJob['state'],
|
|
393
|
+
overlapPolicy: row.overlap_policy as PromptJob['overlapPolicy'],
|
|
394
|
+
cancelCheckSec: row.cancel_check_sec as number,
|
|
395
|
+
nextRunAt: row.next_run_at as number | null,
|
|
396
|
+
lastRunAt: row.last_run_at as number | null,
|
|
397
|
+
lastOutcome: row.last_outcome as RunStatus | null,
|
|
398
|
+
createdAt: row.created_at as string,
|
|
399
|
+
updatedAt: row.updated_at as string,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function rowToRun(row: Record<string, unknown>): PromptRun {
|
|
404
|
+
return {
|
|
405
|
+
id: row.id as string,
|
|
406
|
+
jobId: row.job_id as string,
|
|
407
|
+
status: row.status as RunStatus,
|
|
408
|
+
output: row.output as string | null,
|
|
409
|
+
error: row.error as string | null,
|
|
410
|
+
durationMs: row.duration_ms as number | null,
|
|
411
|
+
startedAt: row.started_at as string | null,
|
|
412
|
+
finishedAt: row.finished_at as string | null,
|
|
413
|
+
cancelledAt: row.cancelled_at as string | null,
|
|
414
|
+
createdAt: row.created_at as string,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export class PromptJobStore {
|
|
419
|
+
constructor(private db: Database.Database) {}
|
|
420
|
+
|
|
421
|
+
createJob(input: CreateJobInput): PromptJob {
|
|
422
|
+
const id = randomUUID();
|
|
423
|
+
const now = new Date().toISOString();
|
|
424
|
+
const nextRunAt = computeNextTickFromCron(input.cronExpr);
|
|
425
|
+
|
|
426
|
+
this.db.prepare(`
|
|
427
|
+
INSERT INTO prompt_jobs (id, name, prompt, cli, cron_expr, cadence, state, overlap_policy, cancel_check_sec, next_run_at, created_at, updated_at)
|
|
428
|
+
VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)
|
|
429
|
+
`).run(
|
|
430
|
+
id,
|
|
431
|
+
input.name,
|
|
432
|
+
input.prompt,
|
|
433
|
+
input.cli,
|
|
434
|
+
input.cronExpr,
|
|
435
|
+
input.cadence,
|
|
436
|
+
input.overlapPolicy ?? 'skip',
|
|
437
|
+
input.cancelCheckSec ?? 5,
|
|
438
|
+
nextRunAt,
|
|
439
|
+
now,
|
|
440
|
+
now,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
return this.getJob(id)!;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
getJob(id: string): PromptJob | null {
|
|
447
|
+
const row = this.db.prepare('SELECT * FROM prompt_jobs WHERE id = ?').get(id) as Record<string, unknown> | undefined;
|
|
448
|
+
return row ? rowToJob(row) : null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
listJobs(filter?: ListJobsFilter): PromptJob[] {
|
|
452
|
+
if (filter?.state) {
|
|
453
|
+
return (this.db.prepare('SELECT * FROM prompt_jobs WHERE state = ? ORDER BY created_at DESC').all(filter.state) as Record<string, unknown>[]).map(rowToJob);
|
|
454
|
+
}
|
|
455
|
+
return (this.db.prepare('SELECT * FROM prompt_jobs ORDER BY created_at DESC').all() as Record<string, unknown>[]).map(rowToJob);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
updateJob(id: string, updates: Record<string, unknown>): PromptJob {
|
|
459
|
+
const sets: string[] = [];
|
|
460
|
+
const values: unknown[] = [];
|
|
461
|
+
|
|
462
|
+
const fieldMap: Record<string, string> = {
|
|
463
|
+
name: 'name',
|
|
464
|
+
prompt: 'prompt',
|
|
465
|
+
cli: 'cli',
|
|
466
|
+
cronExpr: 'cron_expr',
|
|
467
|
+
cadence: 'cadence',
|
|
468
|
+
state: 'state',
|
|
469
|
+
overlapPolicy: 'overlap_policy',
|
|
470
|
+
cancelCheckSec: 'cancel_check_sec',
|
|
471
|
+
nextRunAt: 'next_run_at',
|
|
472
|
+
lastRunAt: 'last_run_at',
|
|
473
|
+
lastOutcome: 'last_outcome',
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
for (const [key, val] of Object.entries(updates)) {
|
|
477
|
+
const col = fieldMap[key];
|
|
478
|
+
if (col) {
|
|
479
|
+
sets.push(`${col} = ?`);
|
|
480
|
+
values.push(val);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (sets.length === 0) return this.getJob(id)!;
|
|
485
|
+
|
|
486
|
+
sets.push("updated_at = datetime('now')");
|
|
487
|
+
values.push(id);
|
|
488
|
+
|
|
489
|
+
this.db.prepare(`UPDATE prompt_jobs SET ${sets.join(', ')} WHERE id = ?`).run(...values);
|
|
490
|
+
return this.getJob(id)!;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
deleteJob(id: string): void {
|
|
494
|
+
this.db.prepare('DELETE FROM prompt_jobs WHERE id = ?').run(id);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
getDueJobs(now: number = Date.now()): PromptJob[] {
|
|
498
|
+
return (this.db.prepare(
|
|
499
|
+
"SELECT * FROM prompt_jobs WHERE state = 'active' AND next_run_at IS NOT NULL AND next_run_at <= ?"
|
|
500
|
+
).all(now) as Record<string, unknown>[]).map(rowToJob);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
createRun(jobId: string): PromptRun {
|
|
504
|
+
const id = randomUUID();
|
|
505
|
+
const now = new Date().toISOString();
|
|
506
|
+
this.db.prepare(`
|
|
507
|
+
INSERT INTO prompt_runs (id, job_id, status, created_at)
|
|
508
|
+
VALUES (?, ?, 'queued', ?)
|
|
509
|
+
`).run(id, jobId, now);
|
|
510
|
+
return this.getRun(id)!;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
getRun(id: string): PromptRun | null {
|
|
514
|
+
const row = this.db.prepare('SELECT * FROM prompt_runs WHERE id = ?').get(id) as Record<string, unknown> | undefined;
|
|
515
|
+
return row ? rowToRun(row) : null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
listRuns(jobId: string, limit: number = 50): PromptRun[] {
|
|
519
|
+
return (this.db.prepare(
|
|
520
|
+
'SELECT * FROM prompt_runs WHERE job_id = ? ORDER BY created_at DESC LIMIT ?'
|
|
521
|
+
).all(jobId, limit) as Record<string, unknown>[]).map(rowToRun);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
updateRun(id: string, updates: UpdateRunInput): PromptRun {
|
|
525
|
+
const sets: string[] = [];
|
|
526
|
+
const values: unknown[] = [];
|
|
527
|
+
|
|
528
|
+
const fieldMap: Record<string, string> = {
|
|
529
|
+
status: 'status',
|
|
530
|
+
output: 'output',
|
|
531
|
+
error: 'error',
|
|
532
|
+
durationMs: 'duration_ms',
|
|
533
|
+
startedAt: 'started_at',
|
|
534
|
+
finishedAt: 'finished_at',
|
|
535
|
+
cancelledAt: 'cancelled_at',
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
for (const [key, val] of Object.entries(updates)) {
|
|
539
|
+
const col = fieldMap[key];
|
|
540
|
+
if (col) {
|
|
541
|
+
sets.push(`${col} = ?`);
|
|
542
|
+
values.push(val);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (sets.length === 0) return this.getRun(id)!;
|
|
547
|
+
values.push(id);
|
|
548
|
+
|
|
549
|
+
this.db.prepare(`UPDATE prompt_runs SET ${sets.join(', ')} WHERE id = ?`).run(...values);
|
|
550
|
+
return this.getRun(id)!;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
hasRunningRun(jobId: string): boolean {
|
|
554
|
+
const row = this.db.prepare(
|
|
555
|
+
"SELECT 1 FROM prompt_runs WHERE job_id = ? AND status IN ('queued', 'running') LIMIT 1"
|
|
556
|
+
).get(jobId);
|
|
557
|
+
return !!row;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
isRunCancelled(runId: string): boolean {
|
|
561
|
+
const row = this.db.prepare(
|
|
562
|
+
"SELECT 1 FROM prompt_runs WHERE id = ? AND status = 'cancelled'"
|
|
563
|
+
).get(runId);
|
|
564
|
+
return !!row;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npx vitest run __tests__/prompt-scheduler/store.test.ts
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Expected: All tests PASS
|
|
576
|
+
|
|
577
|
+
- [ ] **Step 5: Commit**
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
git add src/prompt-scheduler/store.ts __tests__/prompt-scheduler/store.test.ts
|
|
581
|
+
git commit -m "feat: add PromptJobStore with CRUD for jobs and runs"
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Task 3: Cron Module — parsing and next-run computation
|
|
587
|
+
|
|
588
|
+
**Files:**
|
|
589
|
+
- Create: `src/prompt-scheduler/cron.ts`
|
|
590
|
+
- Create: `__tests__/prompt-scheduler/cron.test.ts`
|
|
591
|
+
|
|
592
|
+
- [ ] **Step 1: Write failing tests**
|
|
593
|
+
|
|
594
|
+
Create `__tests__/prompt-scheduler/cron.test.ts`:
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
import { describe, it, expect } from 'vitest';
|
|
598
|
+
import { parseCadence, computeNextRun } from '@/src/prompt-scheduler/cron';
|
|
599
|
+
|
|
600
|
+
describe('parseCadence', () => {
|
|
601
|
+
it('parses natural language to cron', () => {
|
|
602
|
+
const result = parseCadence('daily at 9am');
|
|
603
|
+
expect(result).not.toBeNull();
|
|
604
|
+
expect(result!.cronExpr).toBe('0 9 * * *');
|
|
605
|
+
expect(result!.cadence).toBe('Daily at 9:00 AM');
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('passes through valid cron expressions', () => {
|
|
609
|
+
const result = parseCadence('*/15 * * * *');
|
|
610
|
+
expect(result).not.toBeNull();
|
|
611
|
+
expect(result!.cronExpr).toBe('*/15 * * * *');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('parses "every 2 hours"', () => {
|
|
615
|
+
const result = parseCadence('every 2 hours');
|
|
616
|
+
expect(result).not.toBeNull();
|
|
617
|
+
expect(result!.cronExpr).toBe('0 */2 * * *');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('parses "weekdays at 3pm"', () => {
|
|
621
|
+
const result = parseCadence('weekdays at 3pm');
|
|
622
|
+
expect(result).not.toBeNull();
|
|
623
|
+
expect(result!.cronExpr).toBe('0 15 * * 1-5');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('returns null for unparseable input', () => {
|
|
627
|
+
expect(parseCadence('whenever the moon is full')).toBeNull();
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe('computeNextRun', () => {
|
|
632
|
+
it('returns a future timestamp for a valid cron expression', () => {
|
|
633
|
+
const next = computeNextRun('* * * * *');
|
|
634
|
+
expect(next).toBeGreaterThan(Date.now());
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('returns a future timestamp for daily at 9am', () => {
|
|
638
|
+
const next = computeNextRun('0 9 * * *');
|
|
639
|
+
expect(next).toBeGreaterThan(Date.now() - 86400000);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('returns null for invalid cron', () => {
|
|
643
|
+
expect(computeNextRun('not a cron')).toBeNull();
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npx vitest run __tests__/prompt-scheduler/cron.test.ts
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Expected: FAIL — `Cannot find module '@/src/prompt-scheduler/cron'`
|
|
655
|
+
|
|
656
|
+
- [ ] **Step 3: Implement the cron module**
|
|
657
|
+
|
|
658
|
+
Create `src/prompt-scheduler/cron.ts`:
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
import { toCronExpr } from '../graph/nl-schedule';
|
|
662
|
+
|
|
663
|
+
export interface ParsedCadence {
|
|
664
|
+
cronExpr: string;
|
|
665
|
+
cadence: string;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function parseCadence(input: string): ParsedCadence | null {
|
|
669
|
+
const trimmed = input.trim();
|
|
670
|
+
if (!trimmed) return null;
|
|
671
|
+
|
|
672
|
+
const nlResult = toCronExpr(trimmed);
|
|
673
|
+
if (nlResult) {
|
|
674
|
+
return { cronExpr: nlResult.cronExpr, cadence: nlResult.cadence };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Try parsing as raw cron (5 fields)
|
|
678
|
+
if (isValidCron(trimmed)) {
|
|
679
|
+
return { cronExpr: trimmed, cadence: trimmed };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export function computeNextRun(cronExpr: string, fromMs?: number): number | null {
|
|
686
|
+
try {
|
|
687
|
+
const { parseExpression } = require('cron-parser');
|
|
688
|
+
const interval = parseExpression(cronExpr, {
|
|
689
|
+
currentDate: fromMs ? new Date(fromMs) : new Date(),
|
|
690
|
+
});
|
|
691
|
+
return interval.next().getTime();
|
|
692
|
+
} catch {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function isValidCron(expr: string): boolean {
|
|
698
|
+
const parts = expr.split(/\s+/);
|
|
699
|
+
if (parts.length !== 5) return false;
|
|
700
|
+
try {
|
|
701
|
+
const { parseExpression } = require('cron-parser');
|
|
702
|
+
parseExpression(expr);
|
|
703
|
+
return true;
|
|
704
|
+
} catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
711
|
+
|
|
712
|
+
```bash
|
|
713
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npx vitest run __tests__/prompt-scheduler/cron.test.ts
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Expected: All tests PASS
|
|
717
|
+
|
|
718
|
+
- [ ] **Step 5: Commit**
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
git add src/prompt-scheduler/cron.ts __tests__/prompt-scheduler/cron.test.ts
|
|
722
|
+
git commit -m "feat: add cron parsing module wrapping nl-schedule + cron-parser"
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Task 4: Engine — find due jobs, create runs, advance schedule
|
|
728
|
+
|
|
729
|
+
**Files:**
|
|
730
|
+
- Create: `src/prompt-scheduler/engine.ts`
|
|
731
|
+
- Create: `__tests__/prompt-scheduler/engine.test.ts`
|
|
732
|
+
|
|
733
|
+
- [ ] **Step 1: Write failing tests**
|
|
734
|
+
|
|
735
|
+
Create `__tests__/prompt-scheduler/engine.test.ts`:
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
import Database from 'better-sqlite3';
|
|
739
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
740
|
+
import { PromptJobStore } from '@/src/prompt-scheduler/store';
|
|
741
|
+
import { pollDueJobs } from '@/src/prompt-scheduler/engine';
|
|
742
|
+
import { readFileSync } from 'fs';
|
|
743
|
+
import path from 'path';
|
|
744
|
+
|
|
745
|
+
function createTestDb(): Database.Database {
|
|
746
|
+
const db = new Database(':memory:');
|
|
747
|
+
db.exec(readFileSync(path.join(__dirname, '../../db/sqlite/001_agx_board_schema.sql'), 'utf-8'));
|
|
748
|
+
db.exec(readFileSync(path.join(__dirname, '../../db/sqlite/002_prompt_scheduler_schema.sql'), 'utf-8'));
|
|
749
|
+
return db;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
describe('pollDueJobs', () => {
|
|
753
|
+
let db: Database.Database;
|
|
754
|
+
let store: PromptJobStore;
|
|
755
|
+
|
|
756
|
+
beforeEach(() => {
|
|
757
|
+
db = createTestDb();
|
|
758
|
+
store = new PromptJobStore(db);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('creates runs for due jobs and advances nextRunAt', () => {
|
|
762
|
+
const job = store.createJob({ name: 'A', prompt: 'hello', cli: 'claude', cronExpr: '* * * * *', cadence: '' });
|
|
763
|
+
db.prepare('UPDATE prompt_jobs SET next_run_at = ? WHERE id = ?').run(Date.now() - 60000, job.id);
|
|
764
|
+
|
|
765
|
+
const result = pollDueJobs(store);
|
|
766
|
+
|
|
767
|
+
expect(result.queued).toHaveLength(1);
|
|
768
|
+
expect(result.queued[0].jobId).toBe(job.id);
|
|
769
|
+
expect(result.queued[0].status).toBe('queued');
|
|
770
|
+
|
|
771
|
+
const updated = store.getJob(job.id)!;
|
|
772
|
+
expect(updated.nextRunAt).toBeGreaterThan(Date.now() - 1000);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('skips jobs with overlap_policy=skip when a run is active', () => {
|
|
776
|
+
const job = store.createJob({ name: 'A', prompt: 'hello', cli: 'claude', cronExpr: '* * * * *', cadence: '' });
|
|
777
|
+
db.prepare('UPDATE prompt_jobs SET next_run_at = ? WHERE id = ?').run(Date.now() - 60000, job.id);
|
|
778
|
+
|
|
779
|
+
const run = store.createRun(job.id);
|
|
780
|
+
store.updateRun(run.id, { status: 'running', startedAt: new Date().toISOString() });
|
|
781
|
+
|
|
782
|
+
const result = pollDueJobs(store);
|
|
783
|
+
expect(result.queued).toHaveLength(0);
|
|
784
|
+
expect(result.skipped).toHaveLength(1);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('queues jobs with overlap_policy=allow even when running', () => {
|
|
788
|
+
const job = store.createJob({ name: 'A', prompt: 'hello', cli: 'claude', cronExpr: '* * * * *', cadence: '' });
|
|
789
|
+
store.updateJob(job.id, { overlapPolicy: 'allow' });
|
|
790
|
+
db.prepare('UPDATE prompt_jobs SET next_run_at = ? WHERE id = ?').run(Date.now() - 60000, job.id);
|
|
791
|
+
|
|
792
|
+
const run = store.createRun(job.id);
|
|
793
|
+
store.updateRun(run.id, { status: 'running', startedAt: new Date().toISOString() });
|
|
794
|
+
|
|
795
|
+
const result = pollDueJobs(store);
|
|
796
|
+
expect(result.queued).toHaveLength(1);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('returns empty when no jobs are due', () => {
|
|
800
|
+
store.createJob({ name: 'A', prompt: 'hello', cli: 'claude', cronExpr: '0 9 * * *', cadence: '' });
|
|
801
|
+
const result = pollDueJobs(store);
|
|
802
|
+
expect(result.queued).toHaveLength(0);
|
|
803
|
+
expect(result.skipped).toHaveLength(0);
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
809
|
+
|
|
810
|
+
```bash
|
|
811
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npx vitest run __tests__/prompt-scheduler/engine.test.ts
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
Expected: FAIL — `Cannot find module '@/src/prompt-scheduler/engine'`
|
|
815
|
+
|
|
816
|
+
- [ ] **Step 3: Implement the engine**
|
|
817
|
+
|
|
818
|
+
Create `src/prompt-scheduler/engine.ts`:
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
import type { PromptRun } from './types';
|
|
822
|
+
import type { PromptJobStore } from './store';
|
|
823
|
+
import { computeNextRun } from './cron';
|
|
824
|
+
|
|
825
|
+
export interface PollResult {
|
|
826
|
+
queued: PromptRun[];
|
|
827
|
+
skipped: Array<{ jobId: string; reason: string }>;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function pollDueJobs(store: PromptJobStore, now: number = Date.now()): PollResult {
|
|
831
|
+
const dueJobs = store.getDueJobs(now);
|
|
832
|
+
const queued: PromptRun[] = [];
|
|
833
|
+
const skipped: Array<{ jobId: string; reason: string }> = [];
|
|
834
|
+
|
|
835
|
+
for (const job of dueJobs) {
|
|
836
|
+
if (job.overlapPolicy === 'skip' && store.hasRunningRun(job.id)) {
|
|
837
|
+
skipped.push({ jobId: job.id, reason: 'overlap_skip' });
|
|
838
|
+
// Still advance nextRunAt so we don't re-check this tick
|
|
839
|
+
const nextRunAt = computeNextRun(job.cronExpr, now);
|
|
840
|
+
store.updateJob(job.id, { nextRunAt });
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const run = store.createRun(job.id);
|
|
845
|
+
queued.push(run);
|
|
846
|
+
|
|
847
|
+
const nextRunAt = computeNextRun(job.cronExpr, now);
|
|
848
|
+
store.updateJob(job.id, {
|
|
849
|
+
nextRunAt,
|
|
850
|
+
lastRunAt: now,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return { queued, skipped };
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
859
|
+
|
|
860
|
+
```bash
|
|
861
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npx vitest run __tests__/prompt-scheduler/engine.test.ts
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Expected: All tests PASS
|
|
865
|
+
|
|
866
|
+
- [ ] **Step 5: Commit**
|
|
867
|
+
|
|
868
|
+
```bash
|
|
869
|
+
git add src/prompt-scheduler/engine.ts __tests__/prompt-scheduler/engine.test.ts
|
|
870
|
+
git commit -m "feat: add poll engine — finds due jobs, handles overlap, creates runs"
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Task 5: Runner — CLI execution with cancel checking
|
|
876
|
+
|
|
877
|
+
**Files:**
|
|
878
|
+
- Create: `src/prompt-scheduler/runner.ts`
|
|
879
|
+
- Create: `__tests__/prompt-scheduler/runner.test.ts`
|
|
880
|
+
|
|
881
|
+
- [ ] **Step 1: Write failing tests**
|
|
882
|
+
|
|
883
|
+
Create `__tests__/prompt-scheduler/runner.test.ts`:
|
|
884
|
+
|
|
885
|
+
```typescript
|
|
886
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
887
|
+
import { buildCliCommand, executeRun } from '@/src/prompt-scheduler/runner';
|
|
888
|
+
|
|
889
|
+
describe('buildCliCommand', () => {
|
|
890
|
+
it('builds claude command', () => {
|
|
891
|
+
const { cmd, args } = buildCliCommand('claude', 'summarize inbox');
|
|
892
|
+
expect(cmd).toBe('claude');
|
|
893
|
+
expect(args).toEqual(['-p', 'summarize inbox']);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('builds codex command', () => {
|
|
897
|
+
const { cmd, args } = buildCliCommand('codex', 'fix the bug');
|
|
898
|
+
expect(cmd).toBe('codex');
|
|
899
|
+
expect(args).toEqual(['-p', 'fix the bug']);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('builds gemini command', () => {
|
|
903
|
+
const { cmd, args } = buildCliCommand('gemini', 'review PR');
|
|
904
|
+
expect(cmd).toBe('gemini');
|
|
905
|
+
expect(args).toEqual(['-p', 'review PR']);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('builds custom cli command with {prompt} template', () => {
|
|
909
|
+
const { cmd, args } = buildCliCommand('my-cli run --input {prompt}', 'hello world');
|
|
910
|
+
expect(cmd).toBe('my-cli');
|
|
911
|
+
expect(args).toEqual(['run', '--input', 'hello world']);
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
describe('executeRun', () => {
|
|
916
|
+
it('calls onStart, onComplete callbacks', async () => {
|
|
917
|
+
const onStart = vi.fn();
|
|
918
|
+
const onComplete = vi.fn();
|
|
919
|
+
const isCancelled = vi.fn().mockResolvedValue(false);
|
|
920
|
+
|
|
921
|
+
// Use echo as a simple CLI that exits immediately
|
|
922
|
+
await executeRun({
|
|
923
|
+
cli: 'echo',
|
|
924
|
+
prompt: 'hello',
|
|
925
|
+
cancelCheckSec: 1,
|
|
926
|
+
isCancelled,
|
|
927
|
+
onStart,
|
|
928
|
+
onComplete,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
expect(onStart).toHaveBeenCalledOnce();
|
|
932
|
+
expect(onComplete).toHaveBeenCalledOnce();
|
|
933
|
+
expect(onComplete).toHaveBeenCalledWith(
|
|
934
|
+
expect.objectContaining({
|
|
935
|
+
status: 'success',
|
|
936
|
+
output: expect.stringContaining('hello'),
|
|
937
|
+
})
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it('cancels when isCancelled returns true', async () => {
|
|
942
|
+
const onStart = vi.fn();
|
|
943
|
+
const onComplete = vi.fn();
|
|
944
|
+
// Return true on second call to cancel mid-run
|
|
945
|
+
const isCancelled = vi.fn()
|
|
946
|
+
.mockResolvedValueOnce(false)
|
|
947
|
+
.mockResolvedValue(true);
|
|
948
|
+
|
|
949
|
+
await executeRun({
|
|
950
|
+
cli: 'sleep',
|
|
951
|
+
prompt: '10',
|
|
952
|
+
cancelCheckSec: 0.1,
|
|
953
|
+
isCancelled,
|
|
954
|
+
onStart,
|
|
955
|
+
onComplete,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
expect(onComplete).toHaveBeenCalledWith(
|
|
959
|
+
expect.objectContaining({ status: 'cancelled' })
|
|
960
|
+
);
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npx vitest run __tests__/prompt-scheduler/runner.test.ts
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
Expected: FAIL — `Cannot find module '@/src/prompt-scheduler/runner'`
|
|
972
|
+
|
|
973
|
+
- [ ] **Step 3: Implement the runner**
|
|
974
|
+
|
|
975
|
+
Create `src/prompt-scheduler/runner.ts`:
|
|
976
|
+
|
|
977
|
+
```typescript
|
|
978
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
979
|
+
|
|
980
|
+
const KNOWN_CLIS: Record<string, string[]> = {
|
|
981
|
+
claude: ['-p'],
|
|
982
|
+
codex: ['-p'],
|
|
983
|
+
gemini: ['-p'],
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
export function buildCliCommand(cli: string, prompt: string): { cmd: string; args: string[] } {
|
|
987
|
+
const knownFlags = KNOWN_CLIS[cli];
|
|
988
|
+
if (knownFlags) {
|
|
989
|
+
return { cmd: cli, args: [...knownFlags, prompt] };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Custom CLI with {prompt} template
|
|
993
|
+
if (cli.includes('{prompt}')) {
|
|
994
|
+
const parts = cli.replace('{prompt}', prompt).split(/\s+/);
|
|
995
|
+
return { cmd: parts[0], args: parts.slice(1) };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Custom CLI: append prompt as last arg
|
|
999
|
+
const parts = cli.split(/\s+/);
|
|
1000
|
+
return { cmd: parts[0], args: [...parts.slice(1), prompt] };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export interface RunResult {
|
|
1004
|
+
status: 'success' | 'failed' | 'cancelled';
|
|
1005
|
+
output: string;
|
|
1006
|
+
error: string;
|
|
1007
|
+
durationMs: number;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
export interface ExecuteRunOptions {
|
|
1011
|
+
cli: string;
|
|
1012
|
+
prompt: string;
|
|
1013
|
+
cancelCheckSec: number;
|
|
1014
|
+
isCancelled: () => Promise<boolean>;
|
|
1015
|
+
onStart: () => void;
|
|
1016
|
+
onComplete: (result: RunResult) => void;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
export async function executeRun(opts: ExecuteRunOptions): Promise<void> {
|
|
1020
|
+
const { cli, prompt, cancelCheckSec, isCancelled, onStart, onComplete } = opts;
|
|
1021
|
+
const { cmd, args } = buildCliCommand(cli, prompt);
|
|
1022
|
+
const startTime = Date.now();
|
|
1023
|
+
|
|
1024
|
+
opts.onStart();
|
|
1025
|
+
|
|
1026
|
+
return new Promise<void>((resolve) => {
|
|
1027
|
+
let stdout = '';
|
|
1028
|
+
let stderr = '';
|
|
1029
|
+
let killed = false;
|
|
1030
|
+
|
|
1031
|
+
const child: ChildProcess = spawn(cmd, args, {
|
|
1032
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1033
|
+
shell: false,
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
1037
|
+
stdout += data.toString();
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
1041
|
+
stderr += data.toString();
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const cancelInterval = setInterval(async () => {
|
|
1045
|
+
try {
|
|
1046
|
+
if (await isCancelled()) {
|
|
1047
|
+
killed = true;
|
|
1048
|
+
child.kill('SIGTERM');
|
|
1049
|
+
clearInterval(cancelInterval);
|
|
1050
|
+
}
|
|
1051
|
+
} catch {
|
|
1052
|
+
// ignore cancel check errors
|
|
1053
|
+
}
|
|
1054
|
+
}, cancelCheckSec * 1000);
|
|
1055
|
+
|
|
1056
|
+
child.on('close', (code) => {
|
|
1057
|
+
clearInterval(cancelInterval);
|
|
1058
|
+
const durationMs = Date.now() - startTime;
|
|
1059
|
+
|
|
1060
|
+
if (killed) {
|
|
1061
|
+
onComplete({ status: 'cancelled', output: stdout, error: stderr, durationMs });
|
|
1062
|
+
} else if (code === 0) {
|
|
1063
|
+
onComplete({ status: 'success', output: stdout, error: stderr, durationMs });
|
|
1064
|
+
} else {
|
|
1065
|
+
onComplete({ status: 'failed', output: stdout, error: stderr || `Exit code ${code}`, durationMs });
|
|
1066
|
+
}
|
|
1067
|
+
resolve();
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
child.on('error', (err) => {
|
|
1071
|
+
clearInterval(cancelInterval);
|
|
1072
|
+
const durationMs = Date.now() - startTime;
|
|
1073
|
+
onComplete({ status: 'failed', output: stdout, error: err.message, durationMs });
|
|
1074
|
+
resolve();
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
1081
|
+
|
|
1082
|
+
```bash
|
|
1083
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npx vitest run __tests__/prompt-scheduler/runner.test.ts
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
Expected: All tests PASS
|
|
1087
|
+
|
|
1088
|
+
- [ ] **Step 5: Commit**
|
|
1089
|
+
|
|
1090
|
+
```bash
|
|
1091
|
+
git add src/prompt-scheduler/runner.ts __tests__/prompt-scheduler/runner.test.ts
|
|
1092
|
+
git commit -m "feat: add CLI runner with cancel checking and custom CLI templates"
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
---
|
|
1096
|
+
|
|
1097
|
+
## Task 6: API Routes — CRUD + poll + cancel
|
|
1098
|
+
|
|
1099
|
+
**Files:**
|
|
1100
|
+
- Create: `app/api/prompt-jobs/route.ts`
|
|
1101
|
+
- Create: `app/api/prompt-jobs/[id]/route.ts`
|
|
1102
|
+
- Create: `app/api/prompt-jobs/[id]/runs/route.ts`
|
|
1103
|
+
- Create: `app/api/prompt-jobs/[id]/cancel/route.ts`
|
|
1104
|
+
- Create: `app/api/prompt-jobs/poll/route.ts`
|
|
1105
|
+
|
|
1106
|
+
- [ ] **Step 1: Implement list + create endpoint**
|
|
1107
|
+
|
|
1108
|
+
Create `app/api/prompt-jobs/route.ts`:
|
|
1109
|
+
|
|
1110
|
+
```typescript
|
|
1111
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
1112
|
+
import { getPromptJobStore } from '@/src/prompt-scheduler/get-store';
|
|
1113
|
+
import { parseCadence, computeNextRun } from '@/src/prompt-scheduler/cron';
|
|
1114
|
+
|
|
1115
|
+
export async function GET(req: NextRequest) {
|
|
1116
|
+
const store = getPromptJobStore();
|
|
1117
|
+
const state = req.nextUrl.searchParams.get('state') ?? undefined;
|
|
1118
|
+
const jobs = store.listJobs(state ? { state } : undefined);
|
|
1119
|
+
return NextResponse.json({ count: jobs.length, jobs });
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
export async function POST(req: NextRequest) {
|
|
1123
|
+
const body = await req.json();
|
|
1124
|
+
const { name, prompt, cli, cadence, overlapPolicy, cancelCheckSec } = body;
|
|
1125
|
+
|
|
1126
|
+
if (!name || !prompt || !cadence) {
|
|
1127
|
+
return NextResponse.json({ error: 'name, prompt, and cadence are required' }, { status: 400 });
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const parsed = parseCadence(cadence);
|
|
1131
|
+
if (!parsed) {
|
|
1132
|
+
return NextResponse.json({ error: `Could not parse cadence: "${cadence}"` }, { status: 400 });
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const store = getPromptJobStore();
|
|
1136
|
+
const job = store.createJob({
|
|
1137
|
+
name,
|
|
1138
|
+
prompt,
|
|
1139
|
+
cli: cli ?? 'claude',
|
|
1140
|
+
cronExpr: parsed.cronExpr,
|
|
1141
|
+
cadence: parsed.cadence,
|
|
1142
|
+
overlapPolicy,
|
|
1143
|
+
cancelCheckSec,
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
return NextResponse.json(job, { status: 201 });
|
|
1147
|
+
}
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
- [ ] **Step 2: Implement get, update, delete endpoint**
|
|
1151
|
+
|
|
1152
|
+
Create `app/api/prompt-jobs/[id]/route.ts`:
|
|
1153
|
+
|
|
1154
|
+
```typescript
|
|
1155
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
1156
|
+
import { getPromptJobStore } from '@/src/prompt-scheduler/get-store';
|
|
1157
|
+
import { parseCadence, computeNextRun } from '@/src/prompt-scheduler/cron';
|
|
1158
|
+
|
|
1159
|
+
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
1160
|
+
const { id } = await params;
|
|
1161
|
+
const store = getPromptJobStore();
|
|
1162
|
+
const job = store.getJob(id);
|
|
1163
|
+
if (!job) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
1164
|
+
return NextResponse.json(job);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
1168
|
+
const { id } = await params;
|
|
1169
|
+
const store = getPromptJobStore();
|
|
1170
|
+
const job = store.getJob(id);
|
|
1171
|
+
if (!job) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
1172
|
+
|
|
1173
|
+
const body = await req.json();
|
|
1174
|
+
const updates: Record<string, unknown> = {};
|
|
1175
|
+
|
|
1176
|
+
if (body.name !== undefined) updates.name = body.name;
|
|
1177
|
+
if (body.prompt !== undefined) updates.prompt = body.prompt;
|
|
1178
|
+
if (body.cli !== undefined) updates.cli = body.cli;
|
|
1179
|
+
if (body.state !== undefined) updates.state = body.state;
|
|
1180
|
+
if (body.overlapPolicy !== undefined) updates.overlapPolicy = body.overlapPolicy;
|
|
1181
|
+
if (body.cancelCheckSec !== undefined) updates.cancelCheckSec = body.cancelCheckSec;
|
|
1182
|
+
|
|
1183
|
+
if (body.cadence !== undefined) {
|
|
1184
|
+
const parsed = parseCadence(body.cadence);
|
|
1185
|
+
if (!parsed) return NextResponse.json({ error: `Could not parse cadence: "${body.cadence}"` }, { status: 400 });
|
|
1186
|
+
updates.cronExpr = parsed.cronExpr;
|
|
1187
|
+
updates.cadence = parsed.cadence;
|
|
1188
|
+
updates.nextRunAt = computeNextRun(parsed.cronExpr);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const updated = store.updateJob(id, updates);
|
|
1192
|
+
return NextResponse.json(updated);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
1196
|
+
const { id } = await params;
|
|
1197
|
+
const store = getPromptJobStore();
|
|
1198
|
+
store.deleteJob(id);
|
|
1199
|
+
return NextResponse.json({ ok: true });
|
|
1200
|
+
}
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
- [ ] **Step 3: Implement runs endpoint**
|
|
1204
|
+
|
|
1205
|
+
Create `app/api/prompt-jobs/[id]/runs/route.ts`:
|
|
1206
|
+
|
|
1207
|
+
```typescript
|
|
1208
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
1209
|
+
import { getPromptJobStore } from '@/src/prompt-scheduler/get-store';
|
|
1210
|
+
|
|
1211
|
+
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
1212
|
+
const { id } = await params;
|
|
1213
|
+
const store = getPromptJobStore();
|
|
1214
|
+
const runs = store.listRuns(id);
|
|
1215
|
+
return NextResponse.json({ count: runs.length, runs });
|
|
1216
|
+
}
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
- [ ] **Step 4: Implement cancel endpoint**
|
|
1220
|
+
|
|
1221
|
+
Create `app/api/prompt-jobs/[id]/cancel/route.ts`:
|
|
1222
|
+
|
|
1223
|
+
```typescript
|
|
1224
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
1225
|
+
import { getPromptJobStore } from '@/src/prompt-scheduler/get-store';
|
|
1226
|
+
|
|
1227
|
+
export async function POST(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
1228
|
+
const { id } = await params;
|
|
1229
|
+
const store = getPromptJobStore();
|
|
1230
|
+
|
|
1231
|
+
// Find the active run for this job
|
|
1232
|
+
const runs = store.listRuns(id, 1);
|
|
1233
|
+
const activeRun = runs.find(r => r.status === 'running' || r.status === 'queued');
|
|
1234
|
+
if (!activeRun) {
|
|
1235
|
+
return NextResponse.json({ error: 'No active run to cancel' }, { status: 404 });
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
store.updateRun(activeRun.id, {
|
|
1239
|
+
status: 'cancelled',
|
|
1240
|
+
cancelledAt: new Date().toISOString(),
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
return NextResponse.json({ ok: true, runId: activeRun.id });
|
|
1244
|
+
}
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
- [ ] **Step 5: Implement poll endpoint**
|
|
1248
|
+
|
|
1249
|
+
Create `app/api/prompt-jobs/poll/route.ts`:
|
|
1250
|
+
|
|
1251
|
+
```typescript
|
|
1252
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
1253
|
+
import { getPromptJobStore } from '@/src/prompt-scheduler/get-store';
|
|
1254
|
+
import { pollDueJobs } from '@/src/prompt-scheduler/engine';
|
|
1255
|
+
import { executeRun } from '@/src/prompt-scheduler/runner';
|
|
1256
|
+
|
|
1257
|
+
export async function POST(req: NextRequest) {
|
|
1258
|
+
const store = getPromptJobStore();
|
|
1259
|
+
const body = await req.json().catch(() => ({}));
|
|
1260
|
+
const { jobId } = body as { jobId?: string };
|
|
1261
|
+
|
|
1262
|
+
// If specific job requested, force-queue it
|
|
1263
|
+
if (jobId) {
|
|
1264
|
+
const job = store.getJob(jobId);
|
|
1265
|
+
if (!job) return NextResponse.json({ error: 'Job not found' }, { status: 404 });
|
|
1266
|
+
const run = store.createRun(job.id);
|
|
1267
|
+
|
|
1268
|
+
// Execute async — don't block the response
|
|
1269
|
+
executeRunInBackground(store, job.id, run.id, job.cli, job.prompt, job.cancelCheckSec);
|
|
1270
|
+
|
|
1271
|
+
return NextResponse.json({ queued: [run], skipped: [] });
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Normal poll: find due jobs
|
|
1275
|
+
const result = pollDueJobs(store);
|
|
1276
|
+
|
|
1277
|
+
// Execute all queued runs in background
|
|
1278
|
+
for (const run of result.queued) {
|
|
1279
|
+
const job = store.getJob(run.jobId);
|
|
1280
|
+
if (!job) continue;
|
|
1281
|
+
executeRunInBackground(store, job.id, run.id, job.cli, job.prompt, job.cancelCheckSec);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
return NextResponse.json({
|
|
1285
|
+
queued: result.queued.length,
|
|
1286
|
+
skipped: result.skipped.length,
|
|
1287
|
+
details: result,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function executeRunInBackground(
|
|
1292
|
+
store: ReturnType<typeof getPromptJobStore>,
|
|
1293
|
+
jobId: string,
|
|
1294
|
+
runId: string,
|
|
1295
|
+
cli: string,
|
|
1296
|
+
prompt: string,
|
|
1297
|
+
cancelCheckSec: number,
|
|
1298
|
+
) {
|
|
1299
|
+
executeRun({
|
|
1300
|
+
cli,
|
|
1301
|
+
prompt,
|
|
1302
|
+
cancelCheckSec,
|
|
1303
|
+
isCancelled: async () => store.isRunCancelled(runId),
|
|
1304
|
+
onStart: () => {
|
|
1305
|
+
store.updateRun(runId, { status: 'running', startedAt: new Date().toISOString() });
|
|
1306
|
+
},
|
|
1307
|
+
onComplete: (result) => {
|
|
1308
|
+
store.updateRun(runId, {
|
|
1309
|
+
status: result.status,
|
|
1310
|
+
output: result.output,
|
|
1311
|
+
error: result.error || undefined,
|
|
1312
|
+
durationMs: result.durationMs,
|
|
1313
|
+
finishedAt: new Date().toISOString(),
|
|
1314
|
+
cancelledAt: result.status === 'cancelled' ? new Date().toISOString() : undefined,
|
|
1315
|
+
});
|
|
1316
|
+
store.updateJob(jobId, { lastOutcome: result.status });
|
|
1317
|
+
},
|
|
1318
|
+
}).catch((err) => {
|
|
1319
|
+
store.updateRun(runId, {
|
|
1320
|
+
status: 'failed',
|
|
1321
|
+
error: `Runner error: ${err.message}`,
|
|
1322
|
+
finishedAt: new Date().toISOString(),
|
|
1323
|
+
});
|
|
1324
|
+
store.updateJob(jobId, { lastOutcome: 'failed' });
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
- [ ] **Step 6: Create store accessor**
|
|
1330
|
+
|
|
1331
|
+
Create `src/prompt-scheduler/get-store.ts`:
|
|
1332
|
+
|
|
1333
|
+
```typescript
|
|
1334
|
+
import { PromptJobStore } from './store';
|
|
1335
|
+
import { getSQLiteDb } from '@/lib/sqlite-query-adapter';
|
|
1336
|
+
import { readFileSync } from 'fs';
|
|
1337
|
+
import path from 'path';
|
|
1338
|
+
|
|
1339
|
+
let _store: PromptJobStore | null = null;
|
|
1340
|
+
|
|
1341
|
+
export function getPromptJobStore(): PromptJobStore {
|
|
1342
|
+
if (!_store) {
|
|
1343
|
+
const db = getSQLiteDb();
|
|
1344
|
+
// Run migration if prompt_jobs table doesn't exist
|
|
1345
|
+
const tableExists = db.prepare(
|
|
1346
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='prompt_jobs'"
|
|
1347
|
+
).get();
|
|
1348
|
+
if (!tableExists) {
|
|
1349
|
+
const migration = readFileSync(
|
|
1350
|
+
path.join(process.cwd(), 'db/sqlite/002_prompt_scheduler_schema.sql'),
|
|
1351
|
+
'utf-8'
|
|
1352
|
+
);
|
|
1353
|
+
db.exec(migration);
|
|
1354
|
+
}
|
|
1355
|
+
_store = new PromptJobStore(db);
|
|
1356
|
+
}
|
|
1357
|
+
return _store;
|
|
1358
|
+
}
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
- [ ] **Step 7: Commit**
|
|
1362
|
+
|
|
1363
|
+
```bash
|
|
1364
|
+
git add app/api/prompt-jobs/ src/prompt-scheduler/get-store.ts
|
|
1365
|
+
git commit -m "feat: add API routes for prompt jobs — CRUD, poll, cancel, runs"
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
---
|
|
1369
|
+
|
|
1370
|
+
## Task 7: UI — PromptJobBoard replacing AutomationsBoard
|
|
1371
|
+
|
|
1372
|
+
**Files:**
|
|
1373
|
+
- Create: `components/PromptJobBoard.tsx`
|
|
1374
|
+
- Create: `hooks/usePromptJobs.ts`
|
|
1375
|
+
- Modify: `app/automations/page.tsx`
|
|
1376
|
+
|
|
1377
|
+
- [ ] **Step 1: Create the data hook**
|
|
1378
|
+
|
|
1379
|
+
Create `hooks/usePromptJobs.ts`:
|
|
1380
|
+
|
|
1381
|
+
```typescript
|
|
1382
|
+
'use client';
|
|
1383
|
+
|
|
1384
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
1385
|
+
import type { PromptJob, PromptRun } from '@/src/prompt-scheduler/types';
|
|
1386
|
+
|
|
1387
|
+
export interface UsePromptJobsResult {
|
|
1388
|
+
jobs: PromptJob[];
|
|
1389
|
+
loading: boolean;
|
|
1390
|
+
error: string | null;
|
|
1391
|
+
refresh: () => Promise<void>;
|
|
1392
|
+
createJob: (input: { name: string; prompt: string; cli: string; cadence: string }) => Promise<PromptJob | null>;
|
|
1393
|
+
updateJob: (id: string, updates: Record<string, unknown>) => Promise<boolean>;
|
|
1394
|
+
deleteJob: (id: string) => Promise<boolean>;
|
|
1395
|
+
toggleJob: (job: PromptJob) => Promise<boolean>;
|
|
1396
|
+
runNow: (id: string) => Promise<boolean>;
|
|
1397
|
+
cancelRun: (id: string) => Promise<boolean>;
|
|
1398
|
+
fetchRuns: (jobId: string) => Promise<PromptRun[]>;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
export function usePromptJobs(): UsePromptJobsResult {
|
|
1402
|
+
const [jobs, setJobs] = useState<PromptJob[]>([]);
|
|
1403
|
+
const [loading, setLoading] = useState(true);
|
|
1404
|
+
const [error, setError] = useState<string | null>(null);
|
|
1405
|
+
|
|
1406
|
+
const refresh = useCallback(async () => {
|
|
1407
|
+
try {
|
|
1408
|
+
const res = await fetch('/api/prompt-jobs');
|
|
1409
|
+
const data = await res.json();
|
|
1410
|
+
setJobs(data.jobs);
|
|
1411
|
+
setError(null);
|
|
1412
|
+
} catch (e) {
|
|
1413
|
+
setError(e instanceof Error ? e.message : 'Failed to fetch');
|
|
1414
|
+
} finally {
|
|
1415
|
+
setLoading(false);
|
|
1416
|
+
}
|
|
1417
|
+
}, []);
|
|
1418
|
+
|
|
1419
|
+
useEffect(() => {
|
|
1420
|
+
refresh();
|
|
1421
|
+
const interval = setInterval(refresh, 10_000);
|
|
1422
|
+
return () => clearInterval(interval);
|
|
1423
|
+
}, [refresh]);
|
|
1424
|
+
|
|
1425
|
+
const createJob = async (input: { name: string; prompt: string; cli: string; cadence: string }) => {
|
|
1426
|
+
try {
|
|
1427
|
+
const res = await fetch('/api/prompt-jobs', {
|
|
1428
|
+
method: 'POST',
|
|
1429
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1430
|
+
body: JSON.stringify(input),
|
|
1431
|
+
});
|
|
1432
|
+
if (!res.ok) {
|
|
1433
|
+
const data = await res.json();
|
|
1434
|
+
setError(data.error || 'Failed to create');
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
const job = await res.json();
|
|
1438
|
+
await refresh();
|
|
1439
|
+
return job;
|
|
1440
|
+
} catch {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
const updateJob = async (id: string, updates: Record<string, unknown>) => {
|
|
1446
|
+
try {
|
|
1447
|
+
const res = await fetch(`/api/prompt-jobs/${id}`, {
|
|
1448
|
+
method: 'PATCH',
|
|
1449
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1450
|
+
body: JSON.stringify(updates),
|
|
1451
|
+
});
|
|
1452
|
+
if (res.ok) { await refresh(); return true; }
|
|
1453
|
+
return false;
|
|
1454
|
+
} catch { return false; }
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
const deleteJob = async (id: string) => {
|
|
1458
|
+
try {
|
|
1459
|
+
const res = await fetch(`/api/prompt-jobs/${id}`, { method: 'DELETE' });
|
|
1460
|
+
if (res.ok) { await refresh(); return true; }
|
|
1461
|
+
return false;
|
|
1462
|
+
} catch { return false; }
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
const toggleJob = async (job: PromptJob) => {
|
|
1466
|
+
const newState = job.state === 'active' ? 'paused' : 'active';
|
|
1467
|
+
return updateJob(job.id, { state: newState });
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
const runNow = async (id: string) => {
|
|
1471
|
+
try {
|
|
1472
|
+
const res = await fetch('/api/prompt-jobs/poll', {
|
|
1473
|
+
method: 'POST',
|
|
1474
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1475
|
+
body: JSON.stringify({ jobId: id }),
|
|
1476
|
+
});
|
|
1477
|
+
if (res.ok) { setTimeout(refresh, 1000); return true; }
|
|
1478
|
+
return false;
|
|
1479
|
+
} catch { return false; }
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
const cancelRun = async (id: string) => {
|
|
1483
|
+
try {
|
|
1484
|
+
const res = await fetch(`/api/prompt-jobs/${id}/cancel`, { method: 'POST' });
|
|
1485
|
+
if (res.ok) { await refresh(); return true; }
|
|
1486
|
+
return false;
|
|
1487
|
+
} catch { return false; }
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
const fetchRuns = async (jobId: string): Promise<PromptRun[]> => {
|
|
1491
|
+
try {
|
|
1492
|
+
const res = await fetch(`/api/prompt-jobs/${jobId}/runs`);
|
|
1493
|
+
const data = await res.json();
|
|
1494
|
+
return data.runs;
|
|
1495
|
+
} catch { return []; }
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
return { jobs, loading, error, refresh, createJob, updateJob, deleteJob, toggleJob, runNow, cancelRun, fetchRuns };
|
|
1499
|
+
}
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
- [ ] **Step 2: Create the board component**
|
|
1503
|
+
|
|
1504
|
+
Create `components/PromptJobBoard.tsx`:
|
|
1505
|
+
|
|
1506
|
+
```tsx
|
|
1507
|
+
'use client';
|
|
1508
|
+
|
|
1509
|
+
import { useState } from 'react';
|
|
1510
|
+
import { usePromptJobs } from '@/hooks/usePromptJobs';
|
|
1511
|
+
import type { PromptJob, PromptRun, PromptJobState } from '@/src/prompt-scheduler/types';
|
|
1512
|
+
import { Play, Pause, Trash2, Plus, X, Clock, Terminal, RefreshCw } from 'lucide-react';
|
|
1513
|
+
|
|
1514
|
+
type FilterState = 'all' | PromptJobState;
|
|
1515
|
+
|
|
1516
|
+
export default function PromptJobBoard() {
|
|
1517
|
+
const { jobs, loading, error, createJob, deleteJob, toggleJob, runNow, cancelRun, fetchRuns, refresh } = usePromptJobs();
|
|
1518
|
+
const [filter, setFilter] = useState<FilterState>('all');
|
|
1519
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
1520
|
+
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
1521
|
+
const [runs, setRuns] = useState<PromptRun[]>([]);
|
|
1522
|
+
const [busy, setBusy] = useState<Set<string>>(new Set());
|
|
1523
|
+
|
|
1524
|
+
const filtered = filter === 'all' ? jobs : jobs.filter(j => j.state === filter);
|
|
1525
|
+
const counts = {
|
|
1526
|
+
all: jobs.length,
|
|
1527
|
+
active: jobs.filter(j => j.state === 'active').length,
|
|
1528
|
+
paused: jobs.filter(j => j.state === 'paused').length,
|
|
1529
|
+
stopped: jobs.filter(j => j.state === 'stopped').length,
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
const selectJob = async (id: string) => {
|
|
1533
|
+
setSelectedJobId(id);
|
|
1534
|
+
const r = await fetchRuns(id);
|
|
1535
|
+
setRuns(r);
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
const withBusy = async (id: string, fn: () => Promise<unknown>) => {
|
|
1539
|
+
setBusy(prev => new Set(prev).add(id));
|
|
1540
|
+
try { await fn(); } finally {
|
|
1541
|
+
setBusy(prev => { const next = new Set(prev); next.delete(id); return next; });
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
if (loading) return <div className="p-8 text-[var(--muted-foreground)]">Loading...</div>;
|
|
1546
|
+
|
|
1547
|
+
return (
|
|
1548
|
+
<div className="flex h-full">
|
|
1549
|
+
{/* Main panel */}
|
|
1550
|
+
<div className="flex-1 p-6 overflow-auto">
|
|
1551
|
+
<div className="flex items-center justify-between mb-6">
|
|
1552
|
+
<h1 className="text-xl font-semibold text-[var(--foreground)]">Prompt Scheduler</h1>
|
|
1553
|
+
<div className="flex gap-2">
|
|
1554
|
+
<button onClick={refresh} className="p-2 rounded hover:bg-[var(--card-bg)]">
|
|
1555
|
+
<RefreshCw size={16} className="text-[var(--muted-foreground)]" />
|
|
1556
|
+
</button>
|
|
1557
|
+
<button onClick={() => setShowCreate(true)} className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-[var(--primary)] text-white text-sm">
|
|
1558
|
+
<Plus size={14} /> New Job
|
|
1559
|
+
</button>
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1562
|
+
|
|
1563
|
+
{error && <div className="mb-4 p-3 rounded bg-red-500/10 text-red-400 text-sm">{error}</div>}
|
|
1564
|
+
|
|
1565
|
+
{/* Filter pills */}
|
|
1566
|
+
<div className="flex gap-2 mb-4">
|
|
1567
|
+
{(['all', 'active', 'paused', 'stopped'] as FilterState[]).map(f => (
|
|
1568
|
+
<button
|
|
1569
|
+
key={f}
|
|
1570
|
+
onClick={() => setFilter(f)}
|
|
1571
|
+
className={`px-3 py-1 rounded-full text-xs font-medium ${filter === f ? 'bg-[var(--primary)] text-white' : 'bg-[var(--card-bg)] text-[var(--muted-foreground)]'}`}
|
|
1572
|
+
>
|
|
1573
|
+
{f} ({counts[f]})
|
|
1574
|
+
</button>
|
|
1575
|
+
))}
|
|
1576
|
+
</div>
|
|
1577
|
+
|
|
1578
|
+
{/* Job cards */}
|
|
1579
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
|
1580
|
+
{filtered.map(job => (
|
|
1581
|
+
<div
|
|
1582
|
+
key={job.id}
|
|
1583
|
+
onClick={() => selectJob(job.id)}
|
|
1584
|
+
className={`p-4 rounded-lg border cursor-pointer transition-colors ${selectedJobId === job.id ? 'border-[var(--primary)] bg-[var(--primary)]/5' : 'border-[var(--card-border)] bg-[var(--card-bg)] hover:border-[var(--muted-foreground)]/30'}`}
|
|
1585
|
+
>
|
|
1586
|
+
<div className="flex items-start justify-between mb-2">
|
|
1587
|
+
<div>
|
|
1588
|
+
<h3 className="font-medium text-sm text-[var(--foreground)]">{job.name}</h3>
|
|
1589
|
+
<p className="text-xs text-[var(--muted-foreground)] mt-0.5">{job.cadence || job.cronExpr}</p>
|
|
1590
|
+
</div>
|
|
1591
|
+
<div className="flex items-center gap-1">
|
|
1592
|
+
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
|
1593
|
+
job.state === 'active' ? 'bg-green-500/10 text-green-400' :
|
|
1594
|
+
job.state === 'paused' ? 'bg-yellow-500/10 text-yellow-400' :
|
|
1595
|
+
'bg-gray-500/10 text-gray-400'
|
|
1596
|
+
}`}>
|
|
1597
|
+
{job.state}
|
|
1598
|
+
</span>
|
|
1599
|
+
</div>
|
|
1600
|
+
</div>
|
|
1601
|
+
|
|
1602
|
+
<p className="text-xs text-[var(--muted-foreground)] truncate mb-3">{job.prompt}</p>
|
|
1603
|
+
|
|
1604
|
+
<div className="flex items-center justify-between">
|
|
1605
|
+
<div className="flex items-center gap-2 text-xs text-[var(--muted-foreground)]">
|
|
1606
|
+
<Terminal size={12} />
|
|
1607
|
+
<span>{job.cli}</span>
|
|
1608
|
+
{job.nextRunAt && (
|
|
1609
|
+
<>
|
|
1610
|
+
<Clock size={12} className="ml-2" />
|
|
1611
|
+
<span>{formatNextRun(job.nextRunAt)}</span>
|
|
1612
|
+
</>
|
|
1613
|
+
)}
|
|
1614
|
+
</div>
|
|
1615
|
+
<div className="flex gap-1">
|
|
1616
|
+
<button
|
|
1617
|
+
onClick={(e) => { e.stopPropagation(); withBusy(job.id, () => runNow(job.id)); }}
|
|
1618
|
+
disabled={busy.has(job.id)}
|
|
1619
|
+
className="p-1.5 rounded hover:bg-[var(--background)]"
|
|
1620
|
+
title="Run now"
|
|
1621
|
+
>
|
|
1622
|
+
<Play size={14} className="text-green-400" />
|
|
1623
|
+
</button>
|
|
1624
|
+
<button
|
|
1625
|
+
onClick={(e) => { e.stopPropagation(); withBusy(job.id, () => toggleJob(job)); }}
|
|
1626
|
+
disabled={busy.has(job.id)}
|
|
1627
|
+
className="p-1.5 rounded hover:bg-[var(--background)]"
|
|
1628
|
+
title={job.state === 'active' ? 'Pause' : 'Resume'}
|
|
1629
|
+
>
|
|
1630
|
+
<Pause size={14} className="text-yellow-400" />
|
|
1631
|
+
</button>
|
|
1632
|
+
<button
|
|
1633
|
+
onClick={(e) => { e.stopPropagation(); if (confirm('Delete this job?')) withBusy(job.id, () => deleteJob(job.id)); }}
|
|
1634
|
+
disabled={busy.has(job.id)}
|
|
1635
|
+
className="p-1.5 rounded hover:bg-[var(--background)]"
|
|
1636
|
+
title="Delete"
|
|
1637
|
+
>
|
|
1638
|
+
<Trash2 size={14} className="text-red-400" />
|
|
1639
|
+
</button>
|
|
1640
|
+
</div>
|
|
1641
|
+
</div>
|
|
1642
|
+
</div>
|
|
1643
|
+
))}
|
|
1644
|
+
</div>
|
|
1645
|
+
|
|
1646
|
+
{filtered.length === 0 && (
|
|
1647
|
+
<div className="text-center py-12 text-[var(--muted-foreground)]">
|
|
1648
|
+
{jobs.length === 0 ? 'No jobs yet. Create one to get started.' : 'No jobs match this filter.'}
|
|
1649
|
+
</div>
|
|
1650
|
+
)}
|
|
1651
|
+
</div>
|
|
1652
|
+
|
|
1653
|
+
{/* Inspector panel */}
|
|
1654
|
+
{selectedJobId && (
|
|
1655
|
+
<InspectorPanel
|
|
1656
|
+
job={jobs.find(j => j.id === selectedJobId)!}
|
|
1657
|
+
runs={runs}
|
|
1658
|
+
onClose={() => setSelectedJobId(null)}
|
|
1659
|
+
onCancel={() => withBusy(selectedJobId, () => cancelRun(selectedJobId))}
|
|
1660
|
+
/>
|
|
1661
|
+
)}
|
|
1662
|
+
|
|
1663
|
+
{/* Create modal */}
|
|
1664
|
+
{showCreate && (
|
|
1665
|
+
<CreateJobModal
|
|
1666
|
+
onClose={() => setShowCreate(false)}
|
|
1667
|
+
onCreate={async (input) => {
|
|
1668
|
+
const job = await createJob(input);
|
|
1669
|
+
if (job) setShowCreate(false);
|
|
1670
|
+
}}
|
|
1671
|
+
/>
|
|
1672
|
+
)}
|
|
1673
|
+
</div>
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function InspectorPanel({ job, runs, onClose, onCancel }: {
|
|
1678
|
+
job: PromptJob;
|
|
1679
|
+
runs: PromptRun[];
|
|
1680
|
+
onClose: () => void;
|
|
1681
|
+
onCancel: () => void;
|
|
1682
|
+
}) {
|
|
1683
|
+
const activeRun = runs.find(r => r.status === 'running' || r.status === 'queued');
|
|
1684
|
+
|
|
1685
|
+
return (
|
|
1686
|
+
<div className="w-80 border-l border-[var(--card-border)] bg-[var(--card-bg)] p-4 overflow-auto">
|
|
1687
|
+
<div className="flex items-center justify-between mb-4">
|
|
1688
|
+
<h2 className="font-medium text-sm text-[var(--foreground)]">{job.name}</h2>
|
|
1689
|
+
<button onClick={onClose} className="p-1 rounded hover:bg-[var(--background)]">
|
|
1690
|
+
<X size={14} className="text-[var(--muted-foreground)]" />
|
|
1691
|
+
</button>
|
|
1692
|
+
</div>
|
|
1693
|
+
|
|
1694
|
+
<div className="space-y-3 text-xs mb-4">
|
|
1695
|
+
<div><span className="text-[var(--muted-foreground)]">CLI:</span> <span className="text-[var(--foreground)]">{job.cli}</span></div>
|
|
1696
|
+
<div><span className="text-[var(--muted-foreground)]">Schedule:</span> <span className="text-[var(--foreground)]">{job.cadence || job.cronExpr}</span></div>
|
|
1697
|
+
<div><span className="text-[var(--muted-foreground)]">Overlap:</span> <span className="text-[var(--foreground)]">{job.overlapPolicy}</span></div>
|
|
1698
|
+
<div><span className="text-[var(--muted-foreground)]">Last outcome:</span> <span className="text-[var(--foreground)]">{job.lastOutcome ?? 'never run'}</span></div>
|
|
1699
|
+
</div>
|
|
1700
|
+
|
|
1701
|
+
<div className="mb-3">
|
|
1702
|
+
<h3 className="text-xs font-medium text-[var(--muted-foreground)] mb-1">Prompt</h3>
|
|
1703
|
+
<pre className="text-xs bg-[var(--background)] p-2 rounded whitespace-pre-wrap text-[var(--foreground)]">{job.prompt}</pre>
|
|
1704
|
+
</div>
|
|
1705
|
+
|
|
1706
|
+
{activeRun && (
|
|
1707
|
+
<button onClick={onCancel} className="w-full mb-4 px-3 py-1.5 rounded bg-red-500/10 text-red-400 text-xs font-medium hover:bg-red-500/20">
|
|
1708
|
+
Cancel Running
|
|
1709
|
+
</button>
|
|
1710
|
+
)}
|
|
1711
|
+
|
|
1712
|
+
<h3 className="text-xs font-medium text-[var(--muted-foreground)] mb-2">Recent Runs</h3>
|
|
1713
|
+
<div className="space-y-2">
|
|
1714
|
+
{runs.slice(0, 10).map(run => (
|
|
1715
|
+
<div key={run.id} className="p-2 rounded bg-[var(--background)] text-xs">
|
|
1716
|
+
<div className="flex justify-between mb-1">
|
|
1717
|
+
<span className={`font-medium ${
|
|
1718
|
+
run.status === 'success' ? 'text-green-400' :
|
|
1719
|
+
run.status === 'failed' ? 'text-red-400' :
|
|
1720
|
+
run.status === 'cancelled' ? 'text-yellow-400' :
|
|
1721
|
+
run.status === 'running' ? 'text-blue-400' :
|
|
1722
|
+
'text-[var(--muted-foreground)]'
|
|
1723
|
+
}`}>{run.status}</span>
|
|
1724
|
+
{run.durationMs != null && <span className="text-[var(--muted-foreground)]">{(run.durationMs / 1000).toFixed(1)}s</span>}
|
|
1725
|
+
</div>
|
|
1726
|
+
{run.output && <pre className="text-[var(--muted-foreground)] whitespace-pre-wrap truncate max-h-16 overflow-hidden">{run.output.slice(0, 200)}</pre>}
|
|
1727
|
+
{run.error && <pre className="text-red-400 whitespace-pre-wrap truncate max-h-16 overflow-hidden">{run.error.slice(0, 200)}</pre>}
|
|
1728
|
+
</div>
|
|
1729
|
+
))}
|
|
1730
|
+
{runs.length === 0 && <div className="text-[var(--muted-foreground)]">No runs yet</div>}
|
|
1731
|
+
</div>
|
|
1732
|
+
</div>
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function CreateJobModal({ onClose, onCreate }: {
|
|
1737
|
+
onClose: () => void;
|
|
1738
|
+
onCreate: (input: { name: string; prompt: string; cli: string; cadence: string }) => Promise<void>;
|
|
1739
|
+
}) {
|
|
1740
|
+
const [name, setName] = useState('');
|
|
1741
|
+
const [prompt, setPrompt] = useState('');
|
|
1742
|
+
const [cli, setCli] = useState('claude');
|
|
1743
|
+
const [cadence, setCadence] = useState('');
|
|
1744
|
+
const [submitting, setSubmitting] = useState(false);
|
|
1745
|
+
|
|
1746
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
1747
|
+
e.preventDefault();
|
|
1748
|
+
setSubmitting(true);
|
|
1749
|
+
await onCreate({ name, prompt, cli, cadence });
|
|
1750
|
+
setSubmitting(false);
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
return (
|
|
1754
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
|
|
1755
|
+
<form
|
|
1756
|
+
onClick={e => e.stopPropagation()}
|
|
1757
|
+
onSubmit={handleSubmit}
|
|
1758
|
+
className="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-lg p-6 w-[480px] max-h-[80vh] overflow-auto"
|
|
1759
|
+
>
|
|
1760
|
+
<h2 className="text-lg font-semibold text-[var(--foreground)] mb-4">New Prompt Job</h2>
|
|
1761
|
+
|
|
1762
|
+
<label className="block mb-3">
|
|
1763
|
+
<span className="text-xs text-[var(--muted-foreground)]">Name</span>
|
|
1764
|
+
<input value={name} onChange={e => setName(e.target.value)} required
|
|
1765
|
+
className="mt-1 w-full px-3 py-2 rounded bg-[var(--background)] border border-[var(--card-border)] text-sm text-[var(--foreground)]"
|
|
1766
|
+
placeholder="Daily inbox summary" />
|
|
1767
|
+
</label>
|
|
1768
|
+
|
|
1769
|
+
<label className="block mb-3">
|
|
1770
|
+
<span className="text-xs text-[var(--muted-foreground)]">Prompt</span>
|
|
1771
|
+
<textarea value={prompt} onChange={e => setPrompt(e.target.value)} required rows={4}
|
|
1772
|
+
className="mt-1 w-full px-3 py-2 rounded bg-[var(--background)] border border-[var(--card-border)] text-sm text-[var(--foreground)]"
|
|
1773
|
+
placeholder="Summarize my unread emails from the last 24 hours" />
|
|
1774
|
+
</label>
|
|
1775
|
+
|
|
1776
|
+
<label className="block mb-3">
|
|
1777
|
+
<span className="text-xs text-[var(--muted-foreground)]">CLI</span>
|
|
1778
|
+
<select value={cli} onChange={e => setCli(e.target.value)}
|
|
1779
|
+
className="mt-1 w-full px-3 py-2 rounded bg-[var(--background)] border border-[var(--card-border)] text-sm text-[var(--foreground)]">
|
|
1780
|
+
<option value="claude">claude</option>
|
|
1781
|
+
<option value="codex">codex</option>
|
|
1782
|
+
<option value="gemini">gemini</option>
|
|
1783
|
+
</select>
|
|
1784
|
+
</label>
|
|
1785
|
+
|
|
1786
|
+
<label className="block mb-4">
|
|
1787
|
+
<span className="text-xs text-[var(--muted-foreground)]">Schedule</span>
|
|
1788
|
+
<input value={cadence} onChange={e => setCadence(e.target.value)} required
|
|
1789
|
+
className="mt-1 w-full px-3 py-2 rounded bg-[var(--background)] border border-[var(--card-border)] text-sm text-[var(--foreground)]"
|
|
1790
|
+
placeholder="daily at 9am, every 2 hours, 0 */6 * * *" />
|
|
1791
|
+
</label>
|
|
1792
|
+
|
|
1793
|
+
<div className="flex justify-end gap-2">
|
|
1794
|
+
<button type="button" onClick={onClose} className="px-4 py-2 rounded text-sm text-[var(--muted-foreground)] hover:bg-[var(--background)]">
|
|
1795
|
+
Cancel
|
|
1796
|
+
</button>
|
|
1797
|
+
<button type="submit" disabled={submitting} className="px-4 py-2 rounded bg-[var(--primary)] text-white text-sm disabled:opacity-50">
|
|
1798
|
+
{submitting ? 'Creating...' : 'Create'}
|
|
1799
|
+
</button>
|
|
1800
|
+
</div>
|
|
1801
|
+
</form>
|
|
1802
|
+
</div>
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function formatNextRun(epochMs: number): string {
|
|
1807
|
+
const diff = epochMs - Date.now();
|
|
1808
|
+
if (diff < 0) return 'overdue';
|
|
1809
|
+
if (diff < 60_000) return 'in <1m';
|
|
1810
|
+
if (diff < 3_600_000) return `in ${Math.round(diff / 60_000)}m`;
|
|
1811
|
+
if (diff < 86_400_000) return `in ${Math.round(diff / 3_600_000)}h`;
|
|
1812
|
+
return `in ${Math.round(diff / 86_400_000)}d`;
|
|
1813
|
+
}
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
- [ ] **Step 3: Update the automations page to use new board**
|
|
1817
|
+
|
|
1818
|
+
Modify `app/automations/page.tsx` — replace the import:
|
|
1819
|
+
|
|
1820
|
+
```typescript
|
|
1821
|
+
// Replace:
|
|
1822
|
+
import AutomationsBoard from '@/components/AutomationsBoard';
|
|
1823
|
+
// With:
|
|
1824
|
+
import PromptJobBoard from '@/components/PromptJobBoard';
|
|
1825
|
+
```
|
|
1826
|
+
|
|
1827
|
+
And replace the JSX:
|
|
1828
|
+
|
|
1829
|
+
```tsx
|
|
1830
|
+
// Replace:
|
|
1831
|
+
<AutomationsBoard />
|
|
1832
|
+
// With:
|
|
1833
|
+
<PromptJobBoard />
|
|
1834
|
+
```
|
|
1835
|
+
|
|
1836
|
+
- [ ] **Step 4: Commit**
|
|
1837
|
+
|
|
1838
|
+
```bash
|
|
1839
|
+
git add components/PromptJobBoard.tsx hooks/usePromptJobs.ts app/automations/page.tsx
|
|
1840
|
+
git commit -m "feat: add PromptJobBoard UI replacing AutomationsBoard"
|
|
1841
|
+
```
|
|
1842
|
+
|
|
1843
|
+
---
|
|
1844
|
+
|
|
1845
|
+
## Task 8: Integration — wire up migration and smoke test
|
|
1846
|
+
|
|
1847
|
+
**Files:**
|
|
1848
|
+
- Modify: `app/automations/page.tsx` (verify)
|
|
1849
|
+
|
|
1850
|
+
- [ ] **Step 1: Run the schema migration manually to verify**
|
|
1851
|
+
|
|
1852
|
+
```bash
|
|
1853
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && sqlite3 agx-board.db < db/sqlite/002_prompt_scheduler_schema.sql
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
Expected: No errors. Tables `prompt_jobs` and `prompt_runs` created.
|
|
1857
|
+
|
|
1858
|
+
- [ ] **Step 2: Start the dev server**
|
|
1859
|
+
|
|
1860
|
+
```bash
|
|
1861
|
+
cd /Users/mendrika/Projects/Agents/agx-cloud && npm run dev
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
- [ ] **Step 3: Smoke test via curl**
|
|
1865
|
+
|
|
1866
|
+
Create a job:
|
|
1867
|
+
```bash
|
|
1868
|
+
curl -X POST http://localhost:3000/api/prompt-jobs \
|
|
1869
|
+
-H 'Content-Type: application/json' \
|
|
1870
|
+
-d '{"name":"Test Job","prompt":"Say hello","cli":"echo","cadence":"every hour"}'
|
|
1871
|
+
```
|
|
1872
|
+
Expected: 201 with JSON containing the created job.
|
|
1873
|
+
|
|
1874
|
+
List jobs:
|
|
1875
|
+
```bash
|
|
1876
|
+
curl http://localhost:3000/api/prompt-jobs
|
|
1877
|
+
```
|
|
1878
|
+
Expected: 200 with `{ count: 1, jobs: [...] }`.
|
|
1879
|
+
|
|
1880
|
+
Poll (run now):
|
|
1881
|
+
```bash
|
|
1882
|
+
curl -X POST http://localhost:3000/api/prompt-jobs/poll \
|
|
1883
|
+
-H 'Content-Type: application/json' \
|
|
1884
|
+
-d '{"jobId":"<id from create>"}'
|
|
1885
|
+
```
|
|
1886
|
+
Expected: 200 with queued run.
|
|
1887
|
+
|
|
1888
|
+
Check runs:
|
|
1889
|
+
```bash
|
|
1890
|
+
curl http://localhost:3000/api/prompt-jobs/<id>/runs
|
|
1891
|
+
```
|
|
1892
|
+
Expected: 200 with run entries showing status progression.
|
|
1893
|
+
|
|
1894
|
+
- [ ] **Step 4: Open browser and verify UI**
|
|
1895
|
+
|
|
1896
|
+
Navigate to `http://localhost:3000/automations`:
|
|
1897
|
+
- Verify the PromptJobBoard renders
|
|
1898
|
+
- Verify the test job appears
|
|
1899
|
+
- Click "New Job" and create a job via the form
|
|
1900
|
+
- Click "Run now" on a job and verify run appears in inspector
|
|
1901
|
+
|
|
1902
|
+
- [ ] **Step 5: Commit**
|
|
1903
|
+
|
|
1904
|
+
```bash
|
|
1905
|
+
git add -A
|
|
1906
|
+
git commit -m "feat: prompt scheduler integration — migration, smoke test passing"
|
|
1907
|
+
```
|