@voybio/ace-swarm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/LICENSE +186 -0
  3. package/README.md +229 -0
  4. package/assets/.agents/ACE/ACE-Init/AGENTS.md +210 -0
  5. package/assets/.agents/ACE/ACE-Init/instructions.md +118 -0
  6. package/assets/.agents/ACE/ACE_coders/AGENTS.md +154 -0
  7. package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +216 -0
  8. package/assets/.agents/ACE/AGENT_REGISTRY.md +70 -0
  9. package/assets/.agents/ACE/AGENT_REGISTRY_7.md +9 -0
  10. package/assets/.agents/ACE/DIRECTIVE_KERNEL.md +234 -0
  11. package/assets/.agents/ACE/UI/AGENTS.md +115 -0
  12. package/assets/.agents/ACE/UI/instructions.md +178 -0
  13. package/assets/.agents/ACE/VOS/ACE_VOS_MISSING_INFO_MATRIX.md +42 -0
  14. package/assets/.agents/ACE/VOS/AGENTS.md +72 -0
  15. package/assets/.agents/ACE/VOS/instructions.md +211 -0
  16. package/assets/.agents/ACE/agent-astgrep/AGENTS.md +123 -0
  17. package/assets/.agents/ACE/agent-astgrep/instructions.md +91 -0
  18. package/assets/.agents/ACE/agent-builder/AGENTS.md +172 -0
  19. package/assets/.agents/ACE/agent-builder/instructions.md +137 -0
  20. package/assets/.agents/ACE/agent-docs/AGENTS.md +159 -0
  21. package/assets/.agents/ACE/agent-docs/instructions.md +133 -0
  22. package/assets/.agents/ACE/agent-eval/AGENTS.md +46 -0
  23. package/assets/.agents/ACE/agent-eval/instructions.md +56 -0
  24. package/assets/.agents/ACE/agent-memory/AGENTS.md +49 -0
  25. package/assets/.agents/ACE/agent-memory/instructions.md +50 -0
  26. package/assets/.agents/ACE/agent-observability/AGENTS.md +46 -0
  27. package/assets/.agents/ACE/agent-observability/instructions.md +50 -0
  28. package/assets/.agents/ACE/agent-ops/AGENTS.md +201 -0
  29. package/assets/.agents/ACE/agent-ops/instructions.md +136 -0
  30. package/assets/.agents/ACE/agent-qa/AGENTS.md +189 -0
  31. package/assets/.agents/ACE/agent-qa/instructions.md +121 -0
  32. package/assets/.agents/ACE/agent-release/AGENTS.md +48 -0
  33. package/assets/.agents/ACE/agent-release/instructions.md +49 -0
  34. package/assets/.agents/ACE/agent-research/AGENTS.md +160 -0
  35. package/assets/.agents/ACE/agent-research/instructions.md +118 -0
  36. package/assets/.agents/ACE/agent-security/AGENTS.md +48 -0
  37. package/assets/.agents/ACE/agent-security/instructions.md +50 -0
  38. package/assets/.agents/ACE/agent-skeptic/AGENTS.md +178 -0
  39. package/assets/.agents/ACE/agent-skeptic/instructions.md +196 -0
  40. package/assets/.agents/ACE/agent-spec/AGENTS.md +169 -0
  41. package/assets/.agents/ACE/agent-spec/instructions.md +116 -0
  42. package/assets/.agents/ACE/orchestrator/AGENTS.md +365 -0
  43. package/assets/.agents/ACE/orchestrator/instructions.md +231 -0
  44. package/assets/.agents/skills/ace-orchestrator/SKILL.md +63 -0
  45. package/assets/.agents/skills/ace-orchestrator/references/engineering-bootstrap-playbook.md +360 -0
  46. package/assets/.agents/skills/astgrep-index/SKILL.md +58 -0
  47. package/assets/.agents/skills/codemunch/SKILL.md +65 -0
  48. package/assets/.agents/skills/codemunch/references/ast-driven-protocol.md +543 -0
  49. package/assets/.agents/skills/codesnipe/SKILL.md +64 -0
  50. package/assets/.agents/skills/codesnipe/references/dual-codebase-playbook.md +671 -0
  51. package/assets/.agents/skills/eval-harness/SKILL.md +203 -0
  52. package/assets/.agents/skills/handoff-lint/SKILL.md +164 -0
  53. package/assets/.agents/skills/incident-commander/SKILL.md +174 -0
  54. package/assets/.agents/skills/landing-review-watcher/SKILL.md +68 -0
  55. package/assets/.agents/skills/memory-curator/SKILL.md +179 -0
  56. package/assets/.agents/skills/problem-triage/SKILL.md +57 -0
  57. package/assets/.agents/skills/problem-triage/agents/openai.yaml +3 -0
  58. package/assets/.agents/skills/release-sentry/SKILL.md +189 -0
  59. package/assets/.agents/skills/risk-quant/SKILL.md +190 -0
  60. package/assets/.agents/skills/schema-forge/SKILL.md +174 -0
  61. package/assets/.agents/skills/skill-auditor/SKILL.md +52 -0
  62. package/assets/.agents/skills/state-auditor/SKILL.md +182 -0
  63. package/assets/.github/hooks/ace-copilot.json +68 -0
  64. package/assets/agent-state/ACE_WORKFLOW.md +131 -0
  65. package/assets/agent-state/ARTIFACT_MANIFEST.json +5 -0
  66. package/assets/agent-state/AST_GREP_COMMANDS.md +121 -0
  67. package/assets/agent-state/AST_GREP_INDEX.json +13 -0
  68. package/assets/agent-state/AST_GREP_INDEX.md +15 -0
  69. package/assets/agent-state/DECISIONS.md +7 -0
  70. package/assets/agent-state/EVIDENCE_LOG.md +7 -0
  71. package/assets/agent-state/HANDOFF.json +24 -0
  72. package/assets/agent-state/INTERFACE_REGISTRY.md +75 -0
  73. package/assets/agent-state/MODULES/gates/gate-autonomy.json +7 -0
  74. package/assets/agent-state/MODULES/gates/gate-completeness.json +7 -0
  75. package/assets/agent-state/MODULES/gates/gate-correctness.json +7 -0
  76. package/assets/agent-state/MODULES/gates/gate-evaluation.json +7 -0
  77. package/assets/agent-state/MODULES/gates/gate-operability.json +7 -0
  78. package/assets/agent-state/MODULES/gates/gate-security.json +7 -0
  79. package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +7 -0
  80. package/assets/agent-state/MODULES/registry.json +41 -0
  81. package/assets/agent-state/MODULES/roles/capability-astgrep.json +49 -0
  82. package/assets/agent-state/MODULES/roles/capability-build.json +39 -0
  83. package/assets/agent-state/MODULES/roles/capability-docs.json +38 -0
  84. package/assets/agent-state/MODULES/roles/capability-eval.json +20 -0
  85. package/assets/agent-state/MODULES/roles/capability-memory.json +20 -0
  86. package/assets/agent-state/MODULES/roles/capability-observability.json +20 -0
  87. package/assets/agent-state/MODULES/roles/capability-ops.json +45 -0
  88. package/assets/agent-state/MODULES/roles/capability-qa.json +40 -0
  89. package/assets/agent-state/MODULES/roles/capability-release.json +21 -0
  90. package/assets/agent-state/MODULES/roles/capability-research.json +44 -0
  91. package/assets/agent-state/MODULES/roles/capability-security.json +21 -0
  92. package/assets/agent-state/MODULES/roles/capability-skeptic.json +48 -0
  93. package/assets/agent-state/MODULES/roles/capability-spec.json +42 -0
  94. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +289 -0
  95. package/assets/agent-state/MODULES/schemas/ARTIFACT_MANIFEST.schema.json +185 -0
  96. package/assets/agent-state/MODULES/schemas/HANDOFF.agent-state.schema.json +124 -0
  97. package/assets/agent-state/MODULES/schemas/HANDOFF.schema.json +55 -0
  98. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +290 -0
  99. package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +144 -0
  100. package/assets/agent-state/MODULES/schemas/STATUS_EVENT.schema.json +84 -0
  101. package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +138 -0
  102. package/assets/agent-state/MODULES/schemas/TRACKER_SNAPSHOT.schema.json +134 -0
  103. package/assets/agent-state/MODULES/schemas/VERICIFY_BRIDGE_SNAPSHOT.schema.json +157 -0
  104. package/assets/agent-state/MODULES/schemas/VERICIFY_PROCESS_POST_LOG.schema.json +93 -0
  105. package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +133 -0
  106. package/assets/agent-state/PROVENANCE_LOG.md +28 -0
  107. package/assets/agent-state/QUALITY_GATES.md +15 -0
  108. package/assets/agent-state/RISKS.md +8 -0
  109. package/assets/agent-state/SCOPE.md +20 -0
  110. package/assets/agent-state/SKILL_CATALOG.md +48 -0
  111. package/assets/agent-state/STATUS.md +8 -0
  112. package/assets/agent-state/STATUS_EVENTS.ndjson +1 -0
  113. package/assets/agent-state/TASK.md +18 -0
  114. package/assets/agent-state/TEAL_CONFIG.md +117 -0
  115. package/assets/agent-state/handoff-registry.json +5 -0
  116. package/assets/agent-state/index-fingerprints.json +7 -0
  117. package/assets/agent-state/index.json +32 -0
  118. package/assets/agent-state/run-ledger.json +5 -0
  119. package/assets/agent-state/runtime-executor-sessions.json +5 -0
  120. package/assets/agent-state/runtime-tool-specs.json +5 -0
  121. package/assets/agent-state/runtime-workspaces.json +5 -0
  122. package/assets/agent-state/todo-state.json +7 -0
  123. package/assets/agent-state/tracker-snapshot.json +7 -0
  124. package/assets/agent-state/vericify/ace-bridge.json +60 -0
  125. package/assets/agent-state/vericify/process-posts.json +5 -0
  126. package/assets/instructions/ACE.instructions.md +187 -0
  127. package/assets/instructions/ACE_Coder.instructions.md +146 -0
  128. package/assets/instructions/ACE_UI.instructions.md +178 -0
  129. package/assets/instructions/ACE_VOS.instructions.md +211 -0
  130. package/assets/scripts/ace-hook-dispatch.mjs +538 -0
  131. package/assets/scripts/bootstrap-workspace.sh +27 -0
  132. package/assets/scripts/copilot-hook-dispatch.mjs +3 -0
  133. package/assets/scripts/eval-harness.sh +68 -0
  134. package/assets/scripts/render-mcp-configs.sh +396 -0
  135. package/assets/tasks/README.md +48 -0
  136. package/assets/tasks/SWARM_HANDOFF.example.json +53 -0
  137. package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +55 -0
  138. package/assets/tasks/SWARM_HANDOFF.example_vos_to_ui.json +55 -0
  139. package/assets/tasks/SWARM_HANDOFF.template.json +52 -0
  140. package/assets/tasks/cli_work_split.md +22 -0
  141. package/assets/tasks/lessons.md +17 -0
  142. package/assets/tasks/role_tasks.md +206 -0
  143. package/assets/tasks/todo.md +23 -0
  144. package/dist/ace-autonomy.d.ts +137 -0
  145. package/dist/ace-autonomy.js +472 -0
  146. package/dist/ace-context.d.ts +29 -0
  147. package/dist/ace-context.js +240 -0
  148. package/dist/ace-internal-tools.d.ts +8 -0
  149. package/dist/ace-internal-tools.js +76 -0
  150. package/dist/ace-server-instructions.d.ts +12 -0
  151. package/dist/ace-server-instructions.js +324 -0
  152. package/dist/agent-runtime/role-adapters.d.ts +29 -0
  153. package/dist/agent-runtime/role-adapters.js +573 -0
  154. package/dist/astgrep-index.d.ts +24 -0
  155. package/dist/astgrep-index.js +476 -0
  156. package/dist/cli.d.ts +3 -0
  157. package/dist/cli.js +591 -0
  158. package/dist/git-ops.d.ts +53 -0
  159. package/dist/git-ops.js +238 -0
  160. package/dist/handoff-registry.d.ts +71 -0
  161. package/dist/handoff-registry.js +422 -0
  162. package/dist/helpers.d.ts +126 -0
  163. package/dist/helpers.js +1687 -0
  164. package/dist/index-store.d.ts +51 -0
  165. package/dist/index-store.js +328 -0
  166. package/dist/index.d.ts +3 -0
  167. package/dist/index.js +7 -0
  168. package/dist/internal-tool-runtime.d.ts +21 -0
  169. package/dist/internal-tool-runtime.js +136 -0
  170. package/dist/job-scheduler.d.ts +175 -0
  171. package/dist/job-scheduler.js +1217 -0
  172. package/dist/kanban.d.ts +27 -0
  173. package/dist/kanban.js +339 -0
  174. package/dist/local-model-runtime.d.ts +40 -0
  175. package/dist/local-model-runtime.js +174 -0
  176. package/dist/model-bridge.d.ts +54 -0
  177. package/dist/model-bridge.js +587 -0
  178. package/dist/orchestrator-supervisor.d.ts +100 -0
  179. package/dist/orchestrator-supervisor.js +399 -0
  180. package/dist/problem-triage.d.ts +23 -0
  181. package/dist/problem-triage.js +448 -0
  182. package/dist/prompts.d.ts +7 -0
  183. package/dist/prompts.js +628 -0
  184. package/dist/public-surface.d.ts +30 -0
  185. package/dist/public-surface.js +316 -0
  186. package/dist/resources.d.ts +7 -0
  187. package/dist/resources.js +545 -0
  188. package/dist/run-ledger.d.ts +36 -0
  189. package/dist/run-ledger.js +257 -0
  190. package/dist/runtime-command.d.ts +18 -0
  191. package/dist/runtime-command.js +76 -0
  192. package/dist/runtime-executor.d.ts +104 -0
  193. package/dist/runtime-executor.js +985 -0
  194. package/dist/runtime-profile.d.ts +116 -0
  195. package/dist/runtime-profile.js +532 -0
  196. package/dist/runtime-tool-specs.d.ts +68 -0
  197. package/dist/runtime-tool-specs.js +527 -0
  198. package/dist/safe-edit.d.ts +52 -0
  199. package/dist/safe-edit.js +255 -0
  200. package/dist/schemas.d.ts +44 -0
  201. package/dist/schemas.js +830 -0
  202. package/dist/semantic-cache.d.ts +147 -0
  203. package/dist/semantic-cache.js +552 -0
  204. package/dist/semantic-hash.d.ts +83 -0
  205. package/dist/semantic-hash.js +346 -0
  206. package/dist/server.d.ts +10 -0
  207. package/dist/server.js +46 -0
  208. package/dist/shared.d.ts +136 -0
  209. package/dist/shared.js +269 -0
  210. package/dist/skill-auditor.d.ts +26 -0
  211. package/dist/skill-auditor.js +184 -0
  212. package/dist/skill-catalog.d.ts +60 -0
  213. package/dist/skill-catalog.js +305 -0
  214. package/dist/status-events.d.ts +40 -0
  215. package/dist/status-events.js +269 -0
  216. package/dist/store/ace-packed-store.d.ts +69 -0
  217. package/dist/store/ace-packed-store.js +434 -0
  218. package/dist/store/bootstrap-store.d.ts +46 -0
  219. package/dist/store/bootstrap-store.js +242 -0
  220. package/dist/store/catalog-builder.d.ts +21 -0
  221. package/dist/store/catalog-builder.js +68 -0
  222. package/dist/store/importer.d.ts +19 -0
  223. package/dist/store/importer.js +157 -0
  224. package/dist/store/knowledge-bake.d.ts +59 -0
  225. package/dist/store/knowledge-bake.js +339 -0
  226. package/dist/store/materializers/hook-context-materializer.d.ts +25 -0
  227. package/dist/store/materializers/hook-context-materializer.js +100 -0
  228. package/dist/store/materializers/host-file-materializer.d.ts +37 -0
  229. package/dist/store/materializers/host-file-materializer.js +271 -0
  230. package/dist/store/materializers/todo-syncer.d.ts +30 -0
  231. package/dist/store/materializers/todo-syncer.js +140 -0
  232. package/dist/store/materializers/vericify-projector.d.ts +38 -0
  233. package/dist/store/materializers/vericify-projector.js +239 -0
  234. package/dist/store/repositories/discovery-repository.d.ts +24 -0
  235. package/dist/store/repositories/discovery-repository.js +58 -0
  236. package/dist/store/repositories/handoff-repository.d.ts +31 -0
  237. package/dist/store/repositories/handoff-repository.js +67 -0
  238. package/dist/store/repositories/ledger-repository.d.ts +26 -0
  239. package/dist/store/repositories/ledger-repository.js +49 -0
  240. package/dist/store/repositories/runtime-kv-repository.d.ts +16 -0
  241. package/dist/store/repositories/runtime-kv-repository.js +36 -0
  242. package/dist/store/repositories/scheduler-repository.d.ts +50 -0
  243. package/dist/store/repositories/scheduler-repository.js +123 -0
  244. package/dist/store/repositories/session-repository.d.ts +33 -0
  245. package/dist/store/repositories/session-repository.js +82 -0
  246. package/dist/store/repositories/todo-repository.d.ts +31 -0
  247. package/dist/store/repositories/todo-repository.js +77 -0
  248. package/dist/store/repositories/tracker-repository.d.ts +25 -0
  249. package/dist/store/repositories/tracker-repository.js +43 -0
  250. package/dist/store/repositories/vericify-repository.d.ts +32 -0
  251. package/dist/store/repositories/vericify-repository.js +58 -0
  252. package/dist/store/skills-install.d.ts +28 -0
  253. package/dist/store/skills-install.js +86 -0
  254. package/dist/store/state-reader.d.ts +49 -0
  255. package/dist/store/state-reader.js +111 -0
  256. package/dist/store/store-artifacts.d.ts +12 -0
  257. package/dist/store/store-artifacts.js +138 -0
  258. package/dist/store/store-snapshot.d.ts +19 -0
  259. package/dist/store/store-snapshot.js +140 -0
  260. package/dist/store/topology-bake.d.ts +15 -0
  261. package/dist/store/topology-bake.js +215 -0
  262. package/dist/store/types.d.ts +155 -0
  263. package/dist/store/types.js +35 -0
  264. package/dist/store/workspace-snapshot.d.ts +26 -0
  265. package/dist/store/workspace-snapshot.js +107 -0
  266. package/dist/store/write-queue.d.ts +7 -0
  267. package/dist/store/write-queue.js +26 -0
  268. package/dist/todo-state.d.ts +41 -0
  269. package/dist/todo-state.js +399 -0
  270. package/dist/tools-agent.d.ts +7 -0
  271. package/dist/tools-agent.js +1542 -0
  272. package/dist/tools-discovery.d.ts +6 -0
  273. package/dist/tools-discovery.js +178 -0
  274. package/dist/tools-drift.d.ts +13 -0
  275. package/dist/tools-drift.js +357 -0
  276. package/dist/tools-files.d.ts +6 -0
  277. package/dist/tools-files.js +679 -0
  278. package/dist/tools-framework.d.ts +7 -0
  279. package/dist/tools-framework.js +1414 -0
  280. package/dist/tools-git.d.ts +6 -0
  281. package/dist/tools-git.js +183 -0
  282. package/dist/tools-handoff.d.ts +32 -0
  283. package/dist/tools-handoff.js +489 -0
  284. package/dist/tools-lifecycle.d.ts +6 -0
  285. package/dist/tools-lifecycle.js +205 -0
  286. package/dist/tools-memory.d.ts +6 -0
  287. package/dist/tools-memory.js +260 -0
  288. package/dist/tools-scheduler.d.ts +6 -0
  289. package/dist/tools-scheduler.js +228 -0
  290. package/dist/tools-skills.d.ts +3 -0
  291. package/dist/tools-skills.js +104 -0
  292. package/dist/tools-todo.d.ts +6 -0
  293. package/dist/tools-todo.js +154 -0
  294. package/dist/tools.d.ts +9 -0
  295. package/dist/tools.js +33 -0
  296. package/dist/tracker-adapters.d.ts +74 -0
  297. package/dist/tracker-adapters.js +776 -0
  298. package/dist/tracker-sync.d.ts +10 -0
  299. package/dist/tracker-sync.js +84 -0
  300. package/dist/tui/agent-runner.d.ts +137 -0
  301. package/dist/tui/agent-runner.js +466 -0
  302. package/dist/tui/agent-worker.d.ts +10 -0
  303. package/dist/tui/agent-worker.js +347 -0
  304. package/dist/tui/chat.d.ts +84 -0
  305. package/dist/tui/chat.js +368 -0
  306. package/dist/tui/commands.d.ts +57 -0
  307. package/dist/tui/commands.js +432 -0
  308. package/dist/tui/dashboard.d.ts +24 -0
  309. package/dist/tui/dashboard.js +110 -0
  310. package/dist/tui/index.d.ts +114 -0
  311. package/dist/tui/index.js +1059 -0
  312. package/dist/tui/input.d.ts +49 -0
  313. package/dist/tui/input.js +336 -0
  314. package/dist/tui/layout.d.ts +116 -0
  315. package/dist/tui/layout.js +367 -0
  316. package/dist/tui/ollama.d.ts +116 -0
  317. package/dist/tui/ollama.js +192 -0
  318. package/dist/tui/openai-compatible.d.ts +63 -0
  319. package/dist/tui/openai-compatible.js +370 -0
  320. package/dist/tui/provider-discovery.d.ts +59 -0
  321. package/dist/tui/provider-discovery.js +530 -0
  322. package/dist/tui/renderer.d.ts +166 -0
  323. package/dist/tui/renderer.js +304 -0
  324. package/dist/tui/tabs.d.ts +70 -0
  325. package/dist/tui/tabs.js +208 -0
  326. package/dist/tui/telemetry.d.ts +56 -0
  327. package/dist/tui/telemetry.js +106 -0
  328. package/dist/vericify-bridge.d.ts +146 -0
  329. package/dist/vericify-bridge.js +571 -0
  330. package/dist/vericify-context.d.ts +10 -0
  331. package/dist/vericify-context.js +72 -0
  332. package/dist/workspace-manager.d.ts +107 -0
  333. package/dist/workspace-manager.js +636 -0
  334. package/package.json +83 -0
@@ -0,0 +1,1217 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readHandoffRegistry } from "./handoff-registry.js";
3
+ import { safeRead, safeWrite, withFileLock, wsPath } from "./helpers.js";
4
+ import { isReadError } from "./shared.js";
5
+ import { appendStatusEventSafe, readStatusEvents } from "./status-events.js";
6
+ import { appendRunLedgerEntrySafe } from "./run-ledger.js";
7
+ import { readTodoState } from "./todo-state.js";
8
+ // ── Buffered transition-event infrastructure ────────────────────────
9
+ // Events are collected during the scheduler lock and flushed after
10
+ // the lock is released. Emission is failure-tolerant: a broken
11
+ // STATUS_EVENTS file never blocks a state transition.
12
+ const SCHEDULER_SOURCE_MODULE = "capability-scheduler";
13
+ function newEventBuffer() {
14
+ return { status: [], ledger: [] };
15
+ }
16
+ async function flushTransitionEvents(buf) {
17
+ for (const ev of buf.status) {
18
+ try {
19
+ await appendStatusEventSafe({
20
+ source_module: SCHEDULER_SOURCE_MODULE,
21
+ event_type: ev.event_type,
22
+ status: ev.status,
23
+ summary: ev.summary,
24
+ payload: ev.payload,
25
+ });
26
+ }
27
+ catch {
28
+ // Transition events must never break scheduler operations
29
+ }
30
+ }
31
+ for (const le of buf.ledger) {
32
+ try {
33
+ await appendRunLedgerEntrySafe(le);
34
+ }
35
+ catch {
36
+ // Ledger entries must never break scheduler operations
37
+ }
38
+ }
39
+ }
40
+ export const JOB_QUEUE_REL = "agent-state/job-queue.json";
41
+ export const JOB_LOCK_TABLE_REL = "agent-state/job-locks.json";
42
+ export const SCHEDULER_LEASE_REL = "agent-state/scheduler-lease.json";
43
+ export const SCHEDULER_LOCK_REL = "agent-state/job-scheduler.lock";
44
+ export const PRIORITY_BANDS = ["P0", "P1", "P2", "P3"];
45
+ export const JOB_STATUS = [
46
+ "pending",
47
+ "accepted",
48
+ "blocked",
49
+ "ready",
50
+ "running",
51
+ "done",
52
+ "failed",
53
+ "failed_terminal",
54
+ "canceled",
55
+ "unknown_recovery",
56
+ ];
57
+ export const JOB_LOCK_STATUS = [
58
+ "active",
59
+ "completed",
60
+ "released",
61
+ "failed",
62
+ ];
63
+ const PRIORITY_WEIGHT = {
64
+ P0: 0,
65
+ P1: 1,
66
+ P2: 2,
67
+ P3: 3,
68
+ };
69
+ const TERMINAL_STATUSES = new Set(["done", "failed_terminal", "canceled"]);
70
+ const BLOCKED_REASONS = new Set(["blocked", "failed"]);
71
+ function nowIso(now = new Date()) {
72
+ return now.toISOString();
73
+ }
74
+ function parseDate(input) {
75
+ if (!input)
76
+ return undefined;
77
+ const ms = Date.parse(input);
78
+ if (!Number.isFinite(ms))
79
+ return undefined;
80
+ return new Date(ms);
81
+ }
82
+ function toPriority(input) {
83
+ if (!input)
84
+ return "P2";
85
+ return PRIORITY_BANDS.includes(input)
86
+ ? input
87
+ : "P2";
88
+ }
89
+ function matchesExpected(actual, expected) {
90
+ const candidates = expected
91
+ .split("|")
92
+ .map((row) => row.trim())
93
+ .filter((row) => row.length > 0);
94
+ if (candidates.length === 0)
95
+ return actual === expected;
96
+ return candidates.includes(actual);
97
+ }
98
+ function defaultQueueFile() {
99
+ return {
100
+ version: 1,
101
+ updated_at: nowIso(),
102
+ jobs: [],
103
+ };
104
+ }
105
+ function defaultJobLockFile() {
106
+ return {
107
+ version: 1,
108
+ updated_at: nowIso(),
109
+ locks: [],
110
+ };
111
+ }
112
+ function defaultLease(owner = "scheduler") {
113
+ const now = new Date();
114
+ const expires = new Date(now.getTime() + 30_000);
115
+ return {
116
+ version: 1,
117
+ lease_id: randomUUID(),
118
+ owner,
119
+ acquired_at: nowIso(now),
120
+ heartbeat_at: nowIso(now),
121
+ expires_at: nowIso(expires),
122
+ };
123
+ }
124
+ function parseQueueFile(raw) {
125
+ try {
126
+ const parsed = JSON.parse(raw);
127
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
128
+ return undefined;
129
+ const candidate = parsed;
130
+ if (candidate.version !== 1 || !Array.isArray(candidate.jobs))
131
+ return undefined;
132
+ const jobs = [];
133
+ for (const row of candidate.jobs) {
134
+ if (!row || typeof row !== "object")
135
+ continue;
136
+ const job = row;
137
+ if (typeof job.job_id !== "string" || typeof job.kind !== "string")
138
+ continue;
139
+ const status = JOB_STATUS.includes(job.status)
140
+ ? job.status
141
+ : "pending";
142
+ const priority = toPriority(job.priority);
143
+ const deps = Array.isArray(job.dependencies)
144
+ ? job.dependencies
145
+ .filter((dep) => {
146
+ if (!dep || typeof dep !== "object")
147
+ return false;
148
+ const typed = dep;
149
+ return ((typed.kind === "gate" ||
150
+ typed.kind === "handoff" ||
151
+ typed.kind === "todo" ||
152
+ typed.kind === "time") &&
153
+ typeof typed.ref === "string");
154
+ })
155
+ .map((dep) => ({ kind: dep.kind, ref: dep.ref, expected: dep.expected }))
156
+ : [];
157
+ jobs.push({
158
+ job_id: job.job_id,
159
+ kind: job.kind,
160
+ payload: job.payload && typeof job.payload === "object" ? job.payload : {},
161
+ status,
162
+ priority,
163
+ dependencies: deps,
164
+ resource_requirements: Array.isArray(job.resource_requirements)
165
+ ? job.resource_requirements.filter((value) => typeof value === "string" && value.trim().length > 0)
166
+ : ["agent-state/job-scheduler.default"],
167
+ attempt: typeof job.attempt === "number" ? job.attempt : 0,
168
+ max_attempts: typeof job.max_attempts === "number" ? job.max_attempts : 3,
169
+ backoff_ms: typeof job.backoff_ms === "number" ? job.backoff_ms : 5_000,
170
+ next_attempt_at: typeof job.next_attempt_at === "string" ? job.next_attempt_at : undefined,
171
+ created_at: typeof job.created_at === "string" ? job.created_at : nowIso(),
172
+ accepted_at: typeof job.accepted_at === "string" ? job.accepted_at : undefined,
173
+ started_at: typeof job.started_at === "string" ? job.started_at : undefined,
174
+ completed_at: typeof job.completed_at === "string" ? job.completed_at : undefined,
175
+ evidence_ref: typeof job.evidence_ref === "string" ? job.evidence_ref : undefined,
176
+ error: typeof job.error === "string" ? job.error : undefined,
177
+ reason_code: typeof job.reason_code === "string" ? job.reason_code : undefined,
178
+ });
179
+ }
180
+ return {
181
+ version: 1,
182
+ updated_at: typeof candidate.updated_at === "string" ? candidate.updated_at : nowIso(),
183
+ jobs,
184
+ };
185
+ }
186
+ catch {
187
+ return undefined;
188
+ }
189
+ }
190
+ function parseJobLockFile(raw) {
191
+ try {
192
+ const parsed = JSON.parse(raw);
193
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
194
+ return undefined;
195
+ const candidate = parsed;
196
+ if (candidate.version !== 1 || !Array.isArray(candidate.locks))
197
+ return undefined;
198
+ const locks = [];
199
+ for (const row of candidate.locks) {
200
+ if (!row || typeof row !== "object")
201
+ continue;
202
+ const record = row;
203
+ if (typeof record.lock_id !== "string" ||
204
+ typeof record.job_id !== "string" ||
205
+ typeof record.resource !== "string") {
206
+ continue;
207
+ }
208
+ const status = JOB_LOCK_STATUS.includes(record.status)
209
+ ? record.status
210
+ : "active";
211
+ locks.push({
212
+ lock_id: record.lock_id,
213
+ job_id: record.job_id,
214
+ phase: typeof record.phase === "string" ? record.phase : "dispatch",
215
+ resource: record.resource,
216
+ status,
217
+ priority: toPriority(record.priority),
218
+ lease_id: typeof record.lease_id === "string" ? record.lease_id : undefined,
219
+ reason_code: typeof record.reason_code === "string" ? record.reason_code : undefined,
220
+ acquired_at: typeof record.acquired_at === "string" ? record.acquired_at : undefined,
221
+ heartbeat_at: typeof record.heartbeat_at === "string" ? record.heartbeat_at : undefined,
222
+ released_at: typeof record.released_at === "string" ? record.released_at : undefined,
223
+ created_at: typeof record.created_at === "string" ? record.created_at : nowIso(),
224
+ });
225
+ }
226
+ return {
227
+ version: 1,
228
+ updated_at: typeof candidate.updated_at === "string" ? candidate.updated_at : nowIso(),
229
+ locks,
230
+ };
231
+ }
232
+ catch {
233
+ return undefined;
234
+ }
235
+ }
236
+ function parseLease(raw) {
237
+ try {
238
+ const parsed = JSON.parse(raw);
239
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
240
+ return undefined;
241
+ const candidate = parsed;
242
+ if (candidate.version !== 1 ||
243
+ typeof candidate.lease_id !== "string" ||
244
+ typeof candidate.owner !== "string" ||
245
+ typeof candidate.acquired_at !== "string" ||
246
+ typeof candidate.heartbeat_at !== "string" ||
247
+ typeof candidate.expires_at !== "string") {
248
+ return undefined;
249
+ }
250
+ return {
251
+ version: 1,
252
+ lease_id: candidate.lease_id,
253
+ owner: candidate.owner,
254
+ acquired_at: candidate.acquired_at,
255
+ heartbeat_at: candidate.heartbeat_at,
256
+ expires_at: candidate.expires_at,
257
+ };
258
+ }
259
+ catch {
260
+ return undefined;
261
+ }
262
+ }
263
+ export function getJobQueuePath() {
264
+ return wsPath(JOB_QUEUE_REL);
265
+ }
266
+ export function getJobLockTablePath() {
267
+ return wsPath(JOB_LOCK_TABLE_REL);
268
+ }
269
+ export function getSchedulerLeasePath() {
270
+ return wsPath(SCHEDULER_LEASE_REL);
271
+ }
272
+ export function readJobQueue() {
273
+ const raw = safeRead(JOB_QUEUE_REL);
274
+ if (isReadError(raw))
275
+ return defaultQueueFile();
276
+ return parseQueueFile(raw) ?? defaultQueueFile();
277
+ }
278
+ export function readJobLockTable() {
279
+ const raw = safeRead(JOB_LOCK_TABLE_REL);
280
+ if (isReadError(raw))
281
+ return defaultJobLockFile();
282
+ return parseJobLockFile(raw) ?? defaultJobLockFile();
283
+ }
284
+ export function readSchedulerLease() {
285
+ const raw = safeRead(SCHEDULER_LEASE_REL);
286
+ if (isReadError(raw))
287
+ return undefined;
288
+ return parseLease(raw);
289
+ }
290
+ function writeQueue(queue) {
291
+ queue.updated_at = nowIso();
292
+ return safeWrite(JOB_QUEUE_REL, JSON.stringify(queue, null, 2));
293
+ }
294
+ function writeJobLocks(table) {
295
+ table.updated_at = nowIso();
296
+ return safeWrite(JOB_LOCK_TABLE_REL, JSON.stringify(table, null, 2));
297
+ }
298
+ function writeLease(lease) {
299
+ return safeWrite(SCHEDULER_LEASE_REL, JSON.stringify(lease, null, 2));
300
+ }
301
+ function compareJobs(a, b) {
302
+ const priorityCmp = PRIORITY_WEIGHT[a.priority] - PRIORITY_WEIGHT[b.priority];
303
+ if (priorityCmp !== 0)
304
+ return priorityCmp;
305
+ const createdCmp = Date.parse(a.created_at) - Date.parse(b.created_at);
306
+ if (createdCmp !== 0)
307
+ return createdCmp;
308
+ return a.job_id.localeCompare(b.job_id);
309
+ }
310
+ function parseGateResults(payload) {
311
+ const rows = Array.isArray(payload?.gate_results)
312
+ ? payload.gate_results
313
+ : [];
314
+ return rows
315
+ .filter((row) => {
316
+ if (!row || typeof row !== "object" || Array.isArray(row))
317
+ return false;
318
+ const candidate = row;
319
+ return typeof candidate.id === "string" && typeof candidate.status === "string";
320
+ })
321
+ .map((row) => ({
322
+ id: row.id,
323
+ status: row.status,
324
+ detail: typeof row.detail === "string" ? row.detail : undefined,
325
+ }));
326
+ }
327
+ function resolveGateDependencyStatus(dependency, statusEvents) {
328
+ const gateEvents = statusEvents
329
+ .filter((event) => event.event_type === "GATES_EXECUTED")
330
+ .reverse();
331
+ for (const event of gateEvents) {
332
+ const gateResults = parseGateResults(event.payload);
333
+ if (dependency.ref) {
334
+ const match = gateResults.find((row) => row.id === dependency.ref);
335
+ if (match) {
336
+ return { status: match.status, detail: match.detail };
337
+ }
338
+ }
339
+ const gatesRun = Array.isArray(event.payload?.gates_run)
340
+ ? event.payload.gates_run.filter((value) => typeof value === "string")
341
+ : [];
342
+ if (!dependency.ref) {
343
+ return { status: event.status };
344
+ }
345
+ if (gatesRun.includes(dependency.ref)) {
346
+ return { status: event.status };
347
+ }
348
+ }
349
+ return undefined;
350
+ }
351
+ function evaluateDependencyBlocks(job, now, context) {
352
+ const blocks = [];
353
+ const { todoState, handoffState, statusEvents } = context;
354
+ if (job.next_attempt_at) {
355
+ const nextAttempt = parseDate(job.next_attempt_at);
356
+ if (nextAttempt && nextAttempt.getTime() > now.getTime()) {
357
+ blocks.push({
358
+ dependency: { kind: "time", ref: job.next_attempt_at, expected: "retry" },
359
+ reason_code: "retry_backoff",
360
+ detail: `Retry blocked until ${job.next_attempt_at}`,
361
+ });
362
+ }
363
+ }
364
+ for (const dependency of job.dependencies) {
365
+ if (dependency.kind === "time") {
366
+ const marker = dependency.expected ?? dependency.ref;
367
+ const readyAt = parseDate(marker);
368
+ if (!readyAt) {
369
+ blocks.push({
370
+ dependency,
371
+ reason_code: "invalid_time",
372
+ detail: `Invalid time dependency: ${marker}`,
373
+ });
374
+ continue;
375
+ }
376
+ if (readyAt.getTime() > now.getTime()) {
377
+ blocks.push({
378
+ dependency,
379
+ reason_code: "time_not_reached",
380
+ detail: `Waiting until ${readyAt.toISOString()}`,
381
+ });
382
+ }
383
+ continue;
384
+ }
385
+ if (dependency.kind === "todo") {
386
+ const expected = (dependency.expected ?? "done");
387
+ const row = todoState.nodes[dependency.ref];
388
+ if (!row) {
389
+ blocks.push({
390
+ dependency,
391
+ reason_code: "todo_missing",
392
+ detail: `TODO dependency not found: ${dependency.ref}`,
393
+ });
394
+ continue;
395
+ }
396
+ if (!matchesExpected(row.status, expected)) {
397
+ blocks.push({
398
+ dependency,
399
+ reason_code: "todo_unmet",
400
+ detail: `TODO ${dependency.ref} is ${row.status}, expected ${expected}`,
401
+ });
402
+ }
403
+ continue;
404
+ }
405
+ if (dependency.kind === "handoff") {
406
+ const expected = dependency.expected ?? "completed";
407
+ const row = handoffState.handoffs[dependency.ref];
408
+ if (!row) {
409
+ blocks.push({
410
+ dependency,
411
+ reason_code: "handoff_missing",
412
+ detail: `Handoff dependency not found: ${dependency.ref}`,
413
+ });
414
+ continue;
415
+ }
416
+ if (!matchesExpected(row.status ?? "open", expected)) {
417
+ blocks.push({
418
+ dependency,
419
+ reason_code: "handoff_unmet",
420
+ detail: `Handoff ${dependency.ref} is ${row.status ?? "open"}, expected ${expected}`,
421
+ });
422
+ }
423
+ continue;
424
+ }
425
+ if (dependency.kind === "gate") {
426
+ const expected = dependency.expected ?? "pass";
427
+ const relevant = resolveGateDependencyStatus(dependency, statusEvents);
428
+ if (!relevant) {
429
+ blocks.push({
430
+ dependency,
431
+ reason_code: "gate_unknown",
432
+ detail: `No gate evidence found for ${dependency.ref || "<any>"}`,
433
+ });
434
+ continue;
435
+ }
436
+ if (!matchesExpected(relevant.status, expected)) {
437
+ blocks.push({
438
+ dependency,
439
+ reason_code: "gate_unmet",
440
+ detail: `Gate ${dependency.ref || "<any>"} latest status=${relevant.status}, expected=${expected}${relevant.detail ? ` (${relevant.detail})` : ""}`,
441
+ });
442
+ }
443
+ }
444
+ }
445
+ return blocks;
446
+ }
447
+ function normalizeResources(input) {
448
+ const rows = (input ?? []).map((row) => row.trim()).filter((row) => row.length > 0);
449
+ if (rows.length === 0)
450
+ return ["agent-state/job-scheduler.default"];
451
+ return Array.from(new Set(rows));
452
+ }
453
+ function computeBackoffMs(job) {
454
+ const base = Math.max(1000, Math.floor(job.backoff_ms || 1000));
455
+ const exponent = Math.max(0, job.attempt - 1);
456
+ const factor = Math.min(32, 2 ** exponent);
457
+ return base * factor;
458
+ }
459
+ function isJobLockActive(record) {
460
+ return record.status === "active";
461
+ }
462
+ function buildOccupancy(locks) {
463
+ const occupied = new Set();
464
+ for (const lock of locks) {
465
+ if (!isJobLockActive(lock))
466
+ continue;
467
+ occupied.add(lock.resource);
468
+ }
469
+ return occupied;
470
+ }
471
+ function deriveOwner(input) {
472
+ const trimmed = input?.trim();
473
+ if (trimmed && trimmed.length > 0)
474
+ return trimmed;
475
+ return "capability-ops";
476
+ }
477
+ function defaultLeaseTtlSeconds() {
478
+ return 1800;
479
+ }
480
+ function hasActiveLockForJob(locks, jobId) {
481
+ return locks.some((lock) => lock.job_id === jobId && isJobLockActive(lock));
482
+ }
483
+ function acquireLease(owner, ttlSeconds, now) {
484
+ const existing = readSchedulerLease();
485
+ const expiry = new Date(now.getTime() + ttlSeconds * 1000);
486
+ if (!existing) {
487
+ const lease = {
488
+ version: 1,
489
+ lease_id: randomUUID(),
490
+ owner,
491
+ acquired_at: nowIso(now),
492
+ heartbeat_at: nowIso(now),
493
+ expires_at: nowIso(expiry),
494
+ };
495
+ writeLease(lease);
496
+ return { acquired: true, lease, recovery_mode: false };
497
+ }
498
+ const existingExpiry = parseDate(existing.expires_at);
499
+ const expired = !existingExpiry || existingExpiry.getTime() <= now.getTime();
500
+ if (expired || existing.owner === owner) {
501
+ const lease = {
502
+ version: 1,
503
+ lease_id: expired ? randomUUID() : existing.lease_id,
504
+ owner,
505
+ acquired_at: expired ? nowIso(now) : existing.acquired_at,
506
+ heartbeat_at: nowIso(now),
507
+ expires_at: nowIso(expiry),
508
+ };
509
+ writeLease(lease);
510
+ return { acquired: true, lease, recovery_mode: expired };
511
+ }
512
+ return { acquired: false, lease: existing, recovery_mode: false };
513
+ }
514
+ function hasActiveLeaseForOwner(lease, owner, now) {
515
+ if (!lease)
516
+ return false;
517
+ if (lease.owner !== owner)
518
+ return false;
519
+ const expires = parseDate(lease.expires_at);
520
+ if (!expires)
521
+ return false;
522
+ return expires.getTime() > now.getTime();
523
+ }
524
+ function ensureLeaseForOwner(owner, now, ttlSeconds = defaultLeaseTtlSeconds()) {
525
+ const leaseResult = acquireLease(owner, ttlSeconds, now);
526
+ if (!leaseResult.acquired) {
527
+ return leaseResult;
528
+ }
529
+ const refreshedLease = {
530
+ ...leaseResult.lease,
531
+ owner,
532
+ heartbeat_at: nowIso(now),
533
+ expires_at: nowIso(new Date(now.getTime() + ttlSeconds * 1000)),
534
+ };
535
+ writeLease(refreshedLease);
536
+ return {
537
+ ...leaseResult,
538
+ lease: refreshedLease,
539
+ };
540
+ }
541
+ function releaseLocksForRecoveredJob(table, jobId, reasonCode, now) {
542
+ for (const lock of table.locks) {
543
+ if (lock.job_id !== jobId)
544
+ continue;
545
+ if (lock.status !== "active")
546
+ continue;
547
+ lock.status = "released";
548
+ lock.reason_code = reasonCode;
549
+ lock.released_at = nowIso(now);
550
+ lock.heartbeat_at = nowIso(now);
551
+ }
552
+ }
553
+ function heartbeatRunningLocks(table, leaseId, now) {
554
+ for (const lock of table.locks) {
555
+ if (lock.status !== "active")
556
+ continue;
557
+ lock.lease_id = leaseId;
558
+ lock.heartbeat_at = nowIso(now);
559
+ }
560
+ }
561
+ function startJobNow(job, table, lease, now, buf, sourceTool) {
562
+ const acquiredAt = nowIso(now);
563
+ const resources = normalizeResources(job.resource_requirements);
564
+ for (const lock of table.locks) {
565
+ if (lock.job_id !== job.job_id)
566
+ continue;
567
+ if (lock.status === "active") {
568
+ lock.heartbeat_at = acquiredAt;
569
+ lock.lease_id = lease.lease_id;
570
+ }
571
+ }
572
+ const existingResources = new Set(table.locks
573
+ .filter((row) => row.job_id === job.job_id && row.status === "active")
574
+ .map((row) => row.resource));
575
+ for (const resource of resources) {
576
+ if (existingResources.has(resource))
577
+ continue;
578
+ table.locks.push({
579
+ lock_id: `LOCK-${randomUUID().slice(0, 12)}`,
580
+ job_id: job.job_id,
581
+ phase: "dispatch",
582
+ resource,
583
+ status: "active",
584
+ priority: job.priority,
585
+ lease_id: lease.lease_id,
586
+ acquired_at: acquiredAt,
587
+ heartbeat_at: acquiredAt,
588
+ created_at: acquiredAt,
589
+ });
590
+ }
591
+ job.status = "running";
592
+ job.started_at = acquiredAt;
593
+ job.error = undefined;
594
+ job.reason_code = undefined;
595
+ buf.status.push({
596
+ event_type: "SCHEDULER_JOB_STARTED",
597
+ status: "in_progress",
598
+ summary: `Started job ${job.job_id}`,
599
+ payload: {
600
+ job_id: job.job_id,
601
+ acquired_at: acquiredAt,
602
+ resources,
603
+ source_tool: sourceTool,
604
+ },
605
+ });
606
+ buf.ledger.push({
607
+ tool: sourceTool,
608
+ category: "major_update",
609
+ message: `Started scheduler job ${job.job_id}`,
610
+ artifacts: ["agent-state/job-queue.json", "agent-state/job-locks.json"],
611
+ metadata: {
612
+ job_id: job.job_id,
613
+ acquired_at: acquiredAt,
614
+ resources,
615
+ },
616
+ });
617
+ return { acquired_at: acquiredAt, resources };
618
+ }
619
+ function reconcileRecoveryState(queue, table, now, recoveryMode, buf) {
620
+ let recovered = 0;
621
+ for (const job of queue.jobs) {
622
+ if (job.status === "unknown_recovery") {
623
+ releaseLocksForRecoveredJob(table, job.job_id, "recovery_hold", now);
624
+ continue;
625
+ }
626
+ if (job.status !== "running")
627
+ continue;
628
+ const runningLocks = table.locks.filter((lock) => lock.job_id === job.job_id && lock.status === "active");
629
+ const missingLockState = runningLocks.length === 0;
630
+ if (!missingLockState && !recoveryMode) {
631
+ continue;
632
+ }
633
+ const previousStatus = job.status;
634
+ const reasonCode = missingLockState ? "recovery_missing_lock" : "lease_recovered";
635
+ job.status = "unknown_recovery";
636
+ job.reason_code = reasonCode;
637
+ job.error = missingLockState
638
+ ? "Running job lost its resource lock state and requires manual recovery."
639
+ : `Running job lease was recovered by another owner at ${nowIso(now)}`;
640
+ job.next_attempt_at = undefined;
641
+ releaseLocksForRecoveredJob(table, job.job_id, "crash_recovery", now);
642
+ recovered += 1;
643
+ buf.status.push({
644
+ event_type: "SCHEDULER_RECOVERY",
645
+ status: "blocked",
646
+ summary: `Job ${job.job_id} moved to unknown_recovery (${reasonCode})`,
647
+ payload: {
648
+ job_id: job.job_id,
649
+ from_status: previousStatus,
650
+ reason_code: reasonCode,
651
+ error: job.error,
652
+ recovery_mode: recoveryMode,
653
+ },
654
+ });
655
+ }
656
+ return recovered;
657
+ }
658
+ export async function enqueueJob(input) {
659
+ const buf = newEventBuffer();
660
+ const result = await withFileLock(SCHEDULER_LOCK_REL, () => {
661
+ if (!input.kind || input.kind.trim().length === 0) {
662
+ const queue = readJobQueue();
663
+ return {
664
+ ok: false,
665
+ path: getJobQueuePath(),
666
+ queue,
667
+ nack: {
668
+ reason_code: "invalid_kind",
669
+ reason: "`kind` is required.",
670
+ },
671
+ };
672
+ }
673
+ const queue = readJobQueue();
674
+ const now = new Date();
675
+ const created = nowIso(now);
676
+ const jobId = input.job_id && input.job_id.trim().length > 0
677
+ ? input.job_id.trim()
678
+ : `JOB-${created.replace(/[-:.TZ]/g, "").slice(0, 14)}-${randomUUID().slice(0, 8)}`;
679
+ if (queue.jobs.some((job) => job.job_id === jobId)) {
680
+ return {
681
+ ok: false,
682
+ path: getJobQueuePath(),
683
+ queue,
684
+ nack: {
685
+ reason_code: "duplicate_job_id",
686
+ reason: `job_id already exists: ${jobId}`,
687
+ },
688
+ };
689
+ }
690
+ const dependencies = [
691
+ ...(Array.isArray(input.dependencies) ? input.dependencies : []),
692
+ ];
693
+ if (input.run_after && input.run_after.trim().length > 0) {
694
+ dependencies.push({
695
+ kind: "time",
696
+ ref: input.run_after,
697
+ });
698
+ }
699
+ const job = {
700
+ job_id: jobId,
701
+ kind: input.kind.trim(),
702
+ payload: input.payload ?? {},
703
+ status: "accepted",
704
+ priority: toPriority(input.priority),
705
+ dependencies,
706
+ resource_requirements: normalizeResources(input.resource_requirements),
707
+ attempt: 0,
708
+ max_attempts: typeof input.max_attempts === "number" && Number.isFinite(input.max_attempts)
709
+ ? Math.max(1, Math.floor(input.max_attempts))
710
+ : 3,
711
+ backoff_ms: typeof input.backoff_ms === "number" && Number.isFinite(input.backoff_ms)
712
+ ? Math.max(1000, Math.floor(input.backoff_ms))
713
+ : 5000,
714
+ created_at: created,
715
+ accepted_at: created,
716
+ };
717
+ queue.jobs.push(job);
718
+ const path = writeQueue(queue);
719
+ buf.status.push({
720
+ event_type: "SCHEDULER_JOB_ENQUEUED",
721
+ status: "started",
722
+ summary: `Enqueued ${job.job_id} (${job.kind}, ${job.priority})`,
723
+ payload: {
724
+ job_id: job.job_id,
725
+ kind: job.kind,
726
+ priority: job.priority,
727
+ dependency_count: job.dependencies.length,
728
+ resource_requirements: job.resource_requirements,
729
+ },
730
+ });
731
+ buf.ledger.push({
732
+ tool: "enqueue_job",
733
+ category: "major_update",
734
+ message: `Enqueued scheduler job ${job.job_id} (${job.kind})`,
735
+ artifacts: ["agent-state/job-queue.json"],
736
+ metadata: {
737
+ job_id: job.job_id,
738
+ priority: job.priority,
739
+ dependencies: job.dependencies.length,
740
+ },
741
+ });
742
+ return {
743
+ ok: true,
744
+ path,
745
+ queue,
746
+ ack: {
747
+ job_id: job.job_id,
748
+ status: job.status,
749
+ },
750
+ };
751
+ });
752
+ await flushTransitionEvents(buf);
753
+ return result;
754
+ }
755
+ export async function acknowledgeJob(input) {
756
+ const buf = newEventBuffer();
757
+ const result = await withFileLock(SCHEDULER_LOCK_REL, () => {
758
+ const queue = readJobQueue();
759
+ const job = queue.jobs.find((row) => row.job_id === input.job_id);
760
+ if (!job) {
761
+ return {
762
+ ok: false,
763
+ path: getJobQueuePath(),
764
+ queue,
765
+ error: `Unknown job_id: ${input.job_id}`,
766
+ };
767
+ }
768
+ const from = job.status;
769
+ if (input.action === "accept") {
770
+ if (job.status === "pending" || job.status === "blocked") {
771
+ job.status = "accepted";
772
+ job.reason_code = undefined;
773
+ job.error = undefined;
774
+ job.accepted_at = nowIso();
775
+ }
776
+ }
777
+ else if (input.action === "reject" || input.action === "cancel") {
778
+ job.status = "canceled";
779
+ job.reason_code = input.action;
780
+ job.error = input.reason;
781
+ job.completed_at = nowIso();
782
+ }
783
+ else if (input.action === "resume") {
784
+ if (BLOCKED_REASONS.has(job.status) ||
785
+ job.status === "canceled" ||
786
+ job.status === "unknown_recovery") {
787
+ const wasUnknownRecovery = job.status === "unknown_recovery";
788
+ job.status = "accepted";
789
+ job.reason_code = undefined;
790
+ job.error = undefined;
791
+ job.next_attempt_at = undefined;
792
+ if (wasUnknownRecovery) {
793
+ job.started_at = undefined;
794
+ }
795
+ }
796
+ }
797
+ const path = writeQueue(queue);
798
+ buf.status.push({
799
+ event_type: "SCHEDULER_JOB_ACK",
800
+ status: job.status === "canceled" ? "done" : "pass",
801
+ summary: `Job ${input.job_id} ${input.action} (${from} -> ${job.status})`,
802
+ payload: {
803
+ job_id: input.job_id,
804
+ action: input.action,
805
+ from_status: from,
806
+ to_status: job.status,
807
+ reason: input.reason ?? "",
808
+ },
809
+ });
810
+ buf.ledger.push({
811
+ tool: "ack_job",
812
+ category: "info",
813
+ message: `Scheduler job ${input.job_id}: ${from} -> ${job.status} via ${input.action}`,
814
+ artifacts: ["agent-state/job-queue.json"],
815
+ metadata: {
816
+ job_id: input.job_id,
817
+ action: input.action,
818
+ from: from,
819
+ to: job.status,
820
+ },
821
+ });
822
+ return {
823
+ ok: true,
824
+ path,
825
+ queue,
826
+ from,
827
+ to: job.status,
828
+ };
829
+ });
830
+ await flushTransitionEvents(buf);
831
+ return result;
832
+ }
833
+ export async function dispatchJobs(input = {}) {
834
+ const buf = newEventBuffer();
835
+ const result = await withFileLock(SCHEDULER_LOCK_REL, () => {
836
+ const owner = deriveOwner(input.owner);
837
+ const now = parseDate(input.now_iso) ?? new Date();
838
+ const table = readJobLockTable();
839
+ const leaseTtl = typeof input.lease_ttl_seconds === "number" && input.lease_ttl_seconds > 0
840
+ ? Math.floor(input.lease_ttl_seconds)
841
+ : defaultLeaseTtlSeconds();
842
+ const leaseResult = acquireLease(owner, leaseTtl, now);
843
+ if (!leaseResult.acquired) {
844
+ const queue = readJobQueue();
845
+ return {
846
+ ok: false,
847
+ owner,
848
+ lease_acquired: false,
849
+ lease: leaseResult.lease,
850
+ queue_path: getJobQueuePath(),
851
+ lock_path: getJobLockTablePath(),
852
+ queue,
853
+ locks: table,
854
+ summary: {
855
+ total_jobs: queue.jobs.length,
856
+ blocked_jobs: queue.jobs.filter((job) => job.status === "blocked").length,
857
+ ready_jobs: queue.jobs.filter((job) => job.status === "ready").length,
858
+ running_jobs: queue.jobs.filter((job) => job.status === "running").length,
859
+ started_jobs: 0,
860
+ recovered_jobs: 0,
861
+ },
862
+ };
863
+ }
864
+ const queue = readJobQueue();
865
+ const dependencyContext = {
866
+ todoState: readTodoState(),
867
+ handoffState: readHandoffRegistry(),
868
+ statusEvents: readStatusEvents(500),
869
+ };
870
+ const recoveredJobs = reconcileRecoveryState(queue, table, now, leaseResult.recovery_mode, buf);
871
+ heartbeatRunningLocks(table, leaseResult.lease.lease_id, now);
872
+ for (const job of queue.jobs) {
873
+ if (TERMINAL_STATUSES.has(job.status))
874
+ continue;
875
+ if (job.status === "running" || job.status === "unknown_recovery")
876
+ continue;
877
+ if (job.status === "pending") {
878
+ job.status = "accepted";
879
+ job.accepted_at = job.accepted_at ?? nowIso(now);
880
+ }
881
+ const blocks = evaluateDependencyBlocks(job, now, dependencyContext);
882
+ if (blocks.length > 0) {
883
+ job.status = "blocked";
884
+ job.reason_code = blocks[0]?.reason_code;
885
+ job.error = blocks[0]?.detail;
886
+ }
887
+ else {
888
+ job.status = "ready";
889
+ job.reason_code = undefined;
890
+ job.error = undefined;
891
+ }
892
+ }
893
+ const occupancy = buildOccupancy(table.locks);
894
+ const readyJobs = queue.jobs
895
+ .filter((job) => job.status === "ready")
896
+ .sort(compareJobs);
897
+ let startedJobs = 0;
898
+ for (const job of readyJobs) {
899
+ const resources = normalizeResources(job.resource_requirements);
900
+ const free = resources.every((resource) => !occupancy.has(resource));
901
+ if (!free) {
902
+ continue;
903
+ }
904
+ for (const resource of resources) {
905
+ occupancy.add(resource);
906
+ }
907
+ startJobNow(job, table, leaseResult.lease, now, buf, "dispatch_jobs");
908
+ startedJobs += 1;
909
+ }
910
+ const refreshedLease = {
911
+ ...leaseResult.lease,
912
+ owner,
913
+ heartbeat_at: nowIso(now),
914
+ expires_at: nowIso(new Date(now.getTime() + leaseTtl * 1000)),
915
+ };
916
+ writeLease(refreshedLease);
917
+ const finalQueuePath = writeQueue(queue);
918
+ const finalLockPath = writeJobLocks(table);
919
+ const summary = {
920
+ total_jobs: queue.jobs.length,
921
+ blocked_jobs: queue.jobs.filter((job) => job.status === "blocked").length,
922
+ ready_jobs: queue.jobs.filter((job) => job.status === "ready").length,
923
+ running_jobs: queue.jobs.filter((job) => job.status === "running").length,
924
+ started_jobs: startedJobs,
925
+ recovered_jobs: recoveredJobs,
926
+ };
927
+ buf.status.push({
928
+ event_type: "SCHEDULER_TICK",
929
+ status: "pass",
930
+ summary: `Dispatch started=${summary.started_jobs} ready=${summary.ready_jobs} running=${summary.running_jobs} recovered=${recoveredJobs}`,
931
+ payload: {
932
+ owner,
933
+ lease_acquired: true,
934
+ lease_id: refreshedLease.lease_id,
935
+ recovery_mode: leaseResult.recovery_mode,
936
+ ...summary,
937
+ },
938
+ });
939
+ buf.ledger.push({
940
+ tool: "dispatch_jobs",
941
+ category: summary.started_jobs > 0 || summary.recovered_jobs > 0 ? "major_update" : "info",
942
+ message: `Scheduler dispatch complete (started=${summary.started_jobs}, running=${summary.running_jobs})`,
943
+ artifacts: ["agent-state/job-queue.json", "agent-state/job-locks.json"],
944
+ metadata: {
945
+ owner,
946
+ lease_id: refreshedLease.lease_id,
947
+ lease_acquired: true,
948
+ recovery_mode: leaseResult.recovery_mode,
949
+ summary,
950
+ },
951
+ });
952
+ return {
953
+ ok: true,
954
+ owner,
955
+ lease_acquired: true,
956
+ lease: refreshedLease,
957
+ queue_path: finalQueuePath,
958
+ lock_path: finalLockPath,
959
+ queue,
960
+ locks: table,
961
+ summary,
962
+ };
963
+ });
964
+ await flushTransitionEvents(buf);
965
+ return result;
966
+ }
967
+ export async function dispatchJobNow(input) {
968
+ const buf = newEventBuffer();
969
+ const result = await withFileLock(SCHEDULER_LOCK_REL, () => {
970
+ const owner = deriveOwner(input.owner);
971
+ const now = parseDate(input.now_iso) ?? new Date();
972
+ const leaseResult = ensureLeaseForOwner(owner, now);
973
+ if (!leaseResult.acquired) {
974
+ return {
975
+ ok: false,
976
+ queue_path: getJobQueuePath(),
977
+ lock_path: getJobLockTablePath(),
978
+ queue: readJobQueue(),
979
+ locks: readJobLockTable(),
980
+ error: `Owner ${owner} does not hold an active scheduler lease.`,
981
+ };
982
+ }
983
+ const queue = readJobQueue();
984
+ const table = readJobLockTable();
985
+ const job = queue.jobs.find((row) => row.job_id === input.job_id);
986
+ if (!job) {
987
+ return {
988
+ ok: false,
989
+ queue_path: getJobQueuePath(),
990
+ lock_path: getJobLockTablePath(),
991
+ queue,
992
+ locks: table,
993
+ error: `Unknown job_id: ${input.job_id}`,
994
+ };
995
+ }
996
+ if (job.status === "unknown_recovery") {
997
+ return {
998
+ ok: false,
999
+ queue_path: getJobQueuePath(),
1000
+ lock_path: getJobLockTablePath(),
1001
+ queue,
1002
+ locks: table,
1003
+ error: `Job ${input.job_id} is parked in unknown_recovery; resume it before dispatch.`,
1004
+ };
1005
+ }
1006
+ if (TERMINAL_STATUSES.has(job.status)) {
1007
+ return {
1008
+ ok: false,
1009
+ queue_path: getJobQueuePath(),
1010
+ lock_path: getJobLockTablePath(),
1011
+ queue,
1012
+ locks: table,
1013
+ error: `Job ${input.job_id} is ${job.status} and cannot be started.`,
1014
+ };
1015
+ }
1016
+ if (job.status === "running") {
1017
+ const runningResources = table.locks
1018
+ .filter((row) => row.job_id === input.job_id && row.status === "active")
1019
+ .map((row) => row.resource);
1020
+ const acquiredAt = table.locks.find((row) => row.job_id === input.job_id && row.status === "active")
1021
+ ?.acquired_at ??
1022
+ job.started_at ??
1023
+ nowIso(now);
1024
+ heartbeatRunningLocks(table, leaseResult.lease.lease_id, now);
1025
+ const queuePath = writeQueue(queue);
1026
+ const lockPath = writeJobLocks(table);
1027
+ return {
1028
+ ok: true,
1029
+ queue_path: queuePath,
1030
+ lock_path: lockPath,
1031
+ queue,
1032
+ locks: table,
1033
+ started_lock: {
1034
+ acquired_at: acquiredAt,
1035
+ resources: runningResources,
1036
+ },
1037
+ };
1038
+ }
1039
+ const dependencyContext = {
1040
+ todoState: readTodoState(),
1041
+ handoffState: readHandoffRegistry(),
1042
+ statusEvents: readStatusEvents(500),
1043
+ };
1044
+ const blocks = evaluateDependencyBlocks(job, now, dependencyContext);
1045
+ if (blocks.length > 0) {
1046
+ return {
1047
+ ok: false,
1048
+ queue_path: getJobQueuePath(),
1049
+ lock_path: getJobLockTablePath(),
1050
+ queue,
1051
+ locks: table,
1052
+ error: blocks[0]?.detail ?? `Job ${input.job_id} is blocked.`,
1053
+ };
1054
+ }
1055
+ const occupancy = buildOccupancy(table.locks);
1056
+ const resources = normalizeResources(job.resource_requirements);
1057
+ const busy = resources.find((resource) => table.locks.some((row) => row.job_id !== input.job_id && row.resource === resource && row.status === "active"));
1058
+ if (busy) {
1059
+ return {
1060
+ ok: false,
1061
+ queue_path: getJobQueuePath(),
1062
+ lock_path: getJobLockTablePath(),
1063
+ queue,
1064
+ locks: table,
1065
+ error: `Resource ${busy} is already held by another running job.`,
1066
+ };
1067
+ }
1068
+ for (const resource of resources) {
1069
+ occupancy.add(resource);
1070
+ }
1071
+ const startedLock = startJobNow(job, table, leaseResult.lease, now, buf, "dispatch_job_now");
1072
+ const queuePath = writeQueue(queue);
1073
+ const lockPath = writeJobLocks(table);
1074
+ return {
1075
+ ok: true,
1076
+ queue_path: queuePath,
1077
+ lock_path: lockPath,
1078
+ queue,
1079
+ locks: table,
1080
+ started_lock: startedLock,
1081
+ };
1082
+ });
1083
+ await flushTransitionEvents(buf);
1084
+ return result;
1085
+ }
1086
+ export async function completeJob(input) {
1087
+ const buf = newEventBuffer();
1088
+ const result = await withFileLock(SCHEDULER_LOCK_REL, () => {
1089
+ const owner = deriveOwner(input.owner);
1090
+ const now = parseDate(input.now_iso) ?? new Date();
1091
+ const leaseResult = ensureLeaseForOwner(owner, now);
1092
+ if (!leaseResult.acquired) {
1093
+ return {
1094
+ ok: false,
1095
+ queue_path: getJobQueuePath(),
1096
+ lock_path: getJobLockTablePath(),
1097
+ queue: readJobQueue(),
1098
+ locks: readJobLockTable(),
1099
+ error: `Owner ${owner} does not hold an active scheduler lease.`,
1100
+ };
1101
+ }
1102
+ const queue = readJobQueue();
1103
+ const table = readJobLockTable();
1104
+ const job = queue.jobs.find((row) => row.job_id === input.job_id);
1105
+ if (!job) {
1106
+ return {
1107
+ ok: false,
1108
+ queue_path: getJobQueuePath(),
1109
+ lock_path: getJobLockTablePath(),
1110
+ queue,
1111
+ locks: table,
1112
+ error: `Unknown job_id: ${input.job_id}`,
1113
+ };
1114
+ }
1115
+ if (job.status !== "running") {
1116
+ return {
1117
+ ok: false,
1118
+ queue_path: getJobQueuePath(),
1119
+ lock_path: getJobLockTablePath(),
1120
+ queue,
1121
+ locks: table,
1122
+ error: `Job ${input.job_id} is ${job.status}; only running jobs can be completed.`,
1123
+ };
1124
+ }
1125
+ const runningLocks = table.locks.filter((lock) => lock.job_id === input.job_id && lock.status === "active");
1126
+ if (runningLocks.length === 0) {
1127
+ return {
1128
+ ok: false,
1129
+ queue_path: getJobQueuePath(),
1130
+ lock_path: getJobLockTablePath(),
1131
+ queue,
1132
+ locks: table,
1133
+ error: `Job ${input.job_id} has no active resource locks.`,
1134
+ };
1135
+ }
1136
+ const success = input.success !== false;
1137
+ for (const lock of table.locks) {
1138
+ if (lock.job_id !== input.job_id)
1139
+ continue;
1140
+ if (lock.status === "active") {
1141
+ lock.status = success ? "completed" : "failed";
1142
+ lock.reason_code = success ? undefined : "execution_failed";
1143
+ lock.released_at = nowIso(now);
1144
+ lock.heartbeat_at = nowIso(now);
1145
+ }
1146
+ }
1147
+ if (success) {
1148
+ job.status = "done";
1149
+ job.completed_at = nowIso(now);
1150
+ job.evidence_ref = input.evidence_ref ?? job.evidence_ref;
1151
+ job.error = undefined;
1152
+ job.reason_code = undefined;
1153
+ job.next_attempt_at = undefined;
1154
+ }
1155
+ else {
1156
+ job.attempt += 1;
1157
+ job.error = input.error ?? "Job execution failed";
1158
+ job.evidence_ref = input.evidence_ref ?? job.evidence_ref;
1159
+ if (job.attempt >= job.max_attempts) {
1160
+ job.status = "failed_terminal";
1161
+ job.completed_at = nowIso(now);
1162
+ job.reason_code = "retry_exhausted";
1163
+ job.next_attempt_at = undefined;
1164
+ }
1165
+ else {
1166
+ const retryAt = new Date(now.getTime() + computeBackoffMs(job));
1167
+ job.status = "blocked";
1168
+ job.reason_code = "retry_scheduled";
1169
+ job.next_attempt_at = nowIso(retryAt);
1170
+ job.started_at = undefined;
1171
+ }
1172
+ }
1173
+ const queuePath = writeQueue(queue);
1174
+ const lockPath = writeJobLocks(table);
1175
+ const finalStatus = job.status;
1176
+ const isSuccess = finalStatus === "done";
1177
+ buf.status.push({
1178
+ event_type: "SCHEDULER_JOB_COMPLETED",
1179
+ status: isSuccess ? "done" : finalStatus === "failed_terminal" ? "fail" : "blocked",
1180
+ summary: isSuccess
1181
+ ? `Completed job ${input.job_id}`
1182
+ : `Job ${input.job_id} ended as ${finalStatus}`,
1183
+ payload: {
1184
+ job_id: input.job_id,
1185
+ final_status: finalStatus,
1186
+ retry_scheduled_for: job.next_attempt_at ?? null,
1187
+ evidence_ref: input.evidence_ref ?? "",
1188
+ error: input.error ?? "",
1189
+ },
1190
+ });
1191
+ buf.ledger.push({
1192
+ tool: "complete_job",
1193
+ category: isSuccess ? "major_update" : "regression",
1194
+ message: isSuccess
1195
+ ? `Completed scheduler job ${input.job_id}`
1196
+ : `Scheduler job ${input.job_id} ended with ${finalStatus}`,
1197
+ artifacts: ["agent-state/job-queue.json", "agent-state/job-locks.json"],
1198
+ metadata: {
1199
+ job_id: input.job_id,
1200
+ final_status: finalStatus,
1201
+ retry_scheduled_for: job.next_attempt_at,
1202
+ },
1203
+ });
1204
+ return {
1205
+ ok: true,
1206
+ queue_path: queuePath,
1207
+ lock_path: lockPath,
1208
+ queue,
1209
+ locks: table,
1210
+ status: job.status,
1211
+ retry_scheduled_for: job.next_attempt_at,
1212
+ };
1213
+ });
1214
+ await flushTransitionEvents(buf);
1215
+ return result;
1216
+ }
1217
+ //# sourceMappingURL=job-scheduler.js.map