agentic-orchestrator 0.1.28 → 0.2.1

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 (836) hide show
  1. package/.claude/settings.local.json +46 -1
  2. package/.cortexrc +28 -0
  3. package/.github/agents/copilot-instructions.md +29 -0
  4. package/.github/copilot-instructions.md +93 -0
  5. package/.vscode/settings.json +13 -0
  6. package/.vscode/tms.code-snippets +223 -0
  7. package/AGENTS.md +72 -1
  8. package/Agentic-Orchestrator.iml +12 -11
  9. package/CLAUDE.md +72 -1
  10. package/CONSTITUTION.md +504 -0
  11. package/FUTURE-ENHANCEMENTS.md +85 -0
  12. package/NEXT-TASKS.md +25 -0
  13. package/PROMPTS.md +161 -0
  14. package/README.md +126 -29
  15. package/agentic/orchestrator/agents.yaml +4 -3
  16. package/agentic/orchestrator/defaults/policy.defaults.yaml +39 -3
  17. package/agentic/orchestrator/gates.yaml +15 -3
  18. package/agentic/orchestrator/policy.yaml +47 -3
  19. package/agentic/orchestrator/prompts/builder.system.md +69 -20
  20. package/agentic/orchestrator/prompts/planner-intake.system.md +149 -0
  21. package/agentic/orchestrator/prompts/planner.system.md +113 -40
  22. package/agentic/orchestrator/prompts/qa.system.md +73 -18
  23. package/agentic/orchestrator/prompts/reconciler.system.md +119 -0
  24. package/agentic/orchestrator/schemas/agents.schema.json +89 -1
  25. package/agentic/orchestrator/schemas/execution-control.schema.json +242 -0
  26. package/agentic/orchestrator/schemas/index.schema.json +234 -0
  27. package/agentic/orchestrator/schemas/intake.review.schema.json +82 -0
  28. package/agentic/orchestrator/schemas/organizer-ordering-artifact.schema.json +75 -0
  29. package/agentic/orchestrator/schemas/plan.schema.json +44 -0
  30. package/agentic/orchestrator/schemas/policy.schema.json +238 -9
  31. package/agentic/orchestrator/schemas/policy.user.schema.json +129 -1
  32. package/agentic/orchestrator/schemas/spec.manifest.bootstrap.schema.json +101 -0
  33. package/agentic/orchestrator/schemas/spec.manifest.verified.schema.json +80 -0
  34. package/agentic/orchestrator/schemas/state.schema.json +298 -3
  35. package/agentic/orchestrator/tools/catalog.json +145 -15
  36. package/agentic/orchestrator/tools/schemas/input/doctor.run.input.schema.json +18 -0
  37. package/agentic/orchestrator/tools/schemas/input/evidence.latest.input.schema.json +4 -0
  38. package/agentic/orchestrator/tools/schemas/input/evidence.verify_chain.input.schema.json +13 -0
  39. package/agentic/orchestrator/tools/schemas/input/feature.intake_submit.input.schema.json +11 -0
  40. package/agentic/orchestrator/tools/schemas/input/feature.question_answer.input.schema.json +15 -0
  41. package/agentic/orchestrator/tools/schemas/input/feature.question_create.input.schema.json +21 -0
  42. package/agentic/orchestrator/tools/schemas/input/feature.question_list.input.schema.json +13 -0
  43. package/agentic/orchestrator/tools/schemas/input/feature.ready_to_merge.input.schema.json +5 -0
  44. package/agentic/orchestrator/tools/schemas/input/feature.send_message.input.schema.json +1 -1
  45. package/agentic/orchestrator/tools/schemas/input/replay.timeline_get.input.schema.json +32 -0
  46. package/agentic/orchestrator/tools/schemas/input/repo.conflict_abort.input.schema.json +16 -0
  47. package/agentic/orchestrator/tools/schemas/input/repo.conflict_files.input.schema.json +16 -0
  48. package/agentic/orchestrator/tools/schemas/input/repo.reconcile_mainline.input.schema.json +37 -0
  49. package/agentic/orchestrator/tools/schemas/input/repo.resolve_conflict.input.schema.json +40 -0
  50. package/agentic/orchestrator/tools/schemas/input/runtime.execution_request_list.input.schema.json +7 -0
  51. package/agentic/orchestrator/tools/schemas/input/runtime.execution_request_submit.input.schema.json +25 -0
  52. package/agentic/orchestrator/tools/schemas/output/doctor.run.output.schema.json +34 -0
  53. package/agentic/orchestrator/tools/schemas/output/evidence.verify_chain.output.schema.json +23 -0
  54. package/agentic/orchestrator/tools/schemas/output/feature.get_context.output.schema.json +62 -2
  55. package/agentic/orchestrator/tools/schemas/output/feature.intake_submit.output.schema.json +24 -0
  56. package/agentic/orchestrator/tools/schemas/output/feature.question_answer.output.schema.json +21 -0
  57. package/agentic/orchestrator/tools/schemas/output/feature.question_create.output.schema.json +12 -0
  58. package/agentic/orchestrator/tools/schemas/output/feature.question_list.output.schema.json +14 -0
  59. package/agentic/orchestrator/tools/schemas/output/feature.ready_to_merge.output.schema.json +31 -0
  60. package/agentic/orchestrator/tools/schemas/output/feature.send_message.output.schema.json +8 -18
  61. package/agentic/orchestrator/tools/schemas/output/replay.timeline_get.output.schema.json +64 -0
  62. package/agentic/orchestrator/tools/schemas/output/repo.conflict_abort.output.schema.json +16 -0
  63. package/agentic/orchestrator/tools/schemas/output/repo.conflict_files.output.schema.json +22 -0
  64. package/agentic/orchestrator/tools/schemas/output/repo.reconcile_mainline.output.schema.json +61 -0
  65. package/agentic/orchestrator/tools/schemas/output/repo.resolve_conflict.output.schema.json +19 -0
  66. package/agentic/orchestrator/tools/schemas/output/report.dashboard.output.schema.json +26 -0
  67. package/agentic/orchestrator/tools/schemas/output/runtime.execution_request_list.output.schema.json +17 -0
  68. package/agentic/orchestrator/tools/schemas/output/runtime.execution_request_submit.output.schema.json +24 -0
  69. package/agentic/orchestrator/tools.md +13 -0
  70. package/apps/control-plane/scripts/validate-mcp-contracts.ts +1 -1
  71. package/apps/control-plane/src/application/kernel-tool-wiring.ts +140 -2
  72. package/apps/control-plane/src/application/services/activity-monitor-service.ts +44 -1
  73. package/apps/control-plane/src/application/services/bootstrap-manifest-generator-service.ts +251 -0
  74. package/apps/control-plane/src/application/services/checkpoint-service.ts +87 -27
  75. package/apps/control-plane/src/application/services/collision-override-service.ts +906 -0
  76. package/apps/control-plane/src/application/services/collision-queue-service.ts +129 -38
  77. package/apps/control-plane/src/application/services/cost-tracking-service.ts +94 -0
  78. package/apps/control-plane/src/application/services/execution-control-service.ts +599 -0
  79. package/apps/control-plane/src/application/services/feature-deletion-service.ts +37 -1
  80. package/apps/control-plane/src/application/services/feature-lifecycle-service.ts +182 -4
  81. package/apps/control-plane/src/application/services/feature-send-message-service.ts +17 -8
  82. package/apps/control-plane/src/application/services/feature-state-service.ts +191 -6
  83. package/apps/control-plane/src/application/services/gate-service.ts +121 -2
  84. package/apps/control-plane/src/application/services/git-reconciliation-service.ts +1591 -0
  85. package/apps/control-plane/src/application/services/intake-service.ts +1468 -0
  86. package/apps/control-plane/src/application/services/merge-service.ts +308 -17
  87. package/apps/control-plane/src/application/services/notifier-service.ts +3 -1
  88. package/apps/control-plane/src/application/services/performance-analytics-service.ts +75 -0
  89. package/apps/control-plane/src/application/services/plan-service.ts +336 -20
  90. package/apps/control-plane/src/application/services/question-service.ts +693 -0
  91. package/apps/control-plane/src/application/services/reactions-service.ts +73 -17
  92. package/apps/control-plane/src/application/services/replay-timeline-service.ts +295 -0
  93. package/apps/control-plane/src/application/services/reporting-service.ts +194 -10
  94. package/apps/control-plane/src/application/services/run-lease-service.ts +121 -5
  95. package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +95 -8
  96. package/apps/control-plane/src/application/tools/tool-metadata.ts +7 -0
  97. package/apps/control-plane/src/application/usage-types.ts +138 -0
  98. package/apps/control-plane/src/cli/add-command-handler.ts +162 -0
  99. package/apps/control-plane/src/cli/answer-command-handler.ts +113 -0
  100. package/apps/control-plane/src/cli/attach-command-handler.ts +12 -3
  101. package/apps/control-plane/src/cli/cli-argument-parser.ts +133 -11
  102. package/apps/control-plane/src/cli/collision-command-handler.ts +113 -0
  103. package/apps/control-plane/src/cli/command-catalog.ts +479 -0
  104. package/apps/control-plane/src/cli/complete-command-handler.ts +23 -0
  105. package/apps/control-plane/src/cli/completion-command-handler.ts +25 -0
  106. package/apps/control-plane/src/cli/completion-resolver.ts +319 -0
  107. package/apps/control-plane/src/cli/completion-shell-renderer.ts +58 -0
  108. package/apps/control-plane/src/cli/dashboard-command-handler.ts +111 -1
  109. package/apps/control-plane/src/cli/dashboard-runtime-runner.ts +1036 -0
  110. package/apps/control-plane/src/cli/dashboard-runtime.ts +31 -0
  111. package/apps/control-plane/src/cli/help-command-handler.ts +17 -185
  112. package/apps/control-plane/src/cli/init-command-handler.ts +51 -6
  113. package/apps/control-plane/src/cli/merge-command-handler.ts +200 -0
  114. package/apps/control-plane/src/cli/questions-command-handler.ts +70 -0
  115. package/apps/control-plane/src/cli/replay-command-handler.ts +98 -0
  116. package/apps/control-plane/src/cli/resume-command-handler.ts +231 -16
  117. package/apps/control-plane/src/cli/retry-command-handler.ts +229 -17
  118. package/apps/control-plane/src/cli/retry-resume-decision.ts +75 -0
  119. package/apps/control-plane/src/cli/rollback-command-handler.ts +4 -2
  120. package/apps/control-plane/src/cli/run-command-handler.ts +35 -1
  121. package/apps/control-plane/src/cli/spec-ingestion-service.ts +45 -55
  122. package/apps/control-plane/src/cli/spec-preparation.ts +114 -0
  123. package/apps/control-plane/src/cli/spec-utils.ts +90 -11
  124. package/apps/control-plane/src/cli/status-command-handler.ts +122 -0
  125. package/apps/control-plane/src/cli/types.ts +41 -3
  126. package/apps/control-plane/src/core/collisions.ts +150 -31
  127. package/apps/control-plane/src/core/constants.ts +18 -1
  128. package/apps/control-plane/src/core/error-codes.ts +39 -0
  129. package/apps/control-plane/src/core/execution-control.ts +56 -0
  130. package/apps/control-plane/src/core/feature-resume-phase.ts +118 -0
  131. package/apps/control-plane/src/core/gate-freshness.ts +359 -0
  132. package/apps/control-plane/src/core/gate-log-extractor.ts +97 -0
  133. package/apps/control-plane/src/core/gates.ts +90 -1
  134. package/apps/control-plane/src/core/intake-artifacts.ts +295 -0
  135. package/apps/control-plane/src/core/kernel-types.ts +11 -0
  136. package/apps/control-plane/src/core/kernel.ts +604 -16
  137. package/apps/control-plane/src/core/mainline-conflict.ts +22 -0
  138. package/apps/control-plane/src/core/merge-repair.ts +149 -0
  139. package/apps/control-plane/src/core/path-layout.ts +46 -2
  140. package/apps/control-plane/src/core/path-rules.ts +11 -3
  141. package/apps/control-plane/src/core/plan-submit-recovery.ts +130 -0
  142. package/apps/control-plane/src/core/questions.ts +49 -0
  143. package/apps/control-plane/src/core/runtime-sessions.ts +4 -0
  144. package/apps/control-plane/src/core/schemas.ts +40 -1
  145. package/apps/control-plane/src/core/tool-caller.ts +25 -1
  146. package/apps/control-plane/src/core/utils/index-normalizer.ts +25 -4
  147. package/apps/control-plane/src/core/worktree-diff.ts +66 -0
  148. package/apps/control-plane/src/index.ts +29 -1
  149. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +300 -6
  150. package/apps/control-plane/src/mcp/kernel-tool-executor.ts +17 -0
  151. package/apps/control-plane/src/mcp/tool-runtime.ts +63 -4
  152. package/apps/control-plane/src/providers/api-worker-provider.ts +62 -15
  153. package/apps/control-plane/src/providers/cli-worker-provider.ts +1037 -61
  154. package/apps/control-plane/src/providers/output-parsers/generic-output-parser.ts +99 -1
  155. package/apps/control-plane/src/providers/output-parsers/types.ts +2 -0
  156. package/apps/control-plane/src/providers/provider-defaults.ts +116 -7
  157. package/apps/control-plane/src/providers/providers.ts +225 -21
  158. package/apps/control-plane/src/providers/worker-provider-factory.ts +26 -2
  159. package/apps/control-plane/src/supervisor/artifact-stager.ts +52 -0
  160. package/apps/control-plane/src/supervisor/build-wave-executor.ts +477 -166
  161. package/apps/control-plane/src/supervisor/execution-enrollment-service.ts +408 -0
  162. package/apps/control-plane/src/supervisor/organizer-enrollment-scheduler.ts +117 -0
  163. package/apps/control-plane/src/supervisor/organizer-sidecar-service.ts +394 -0
  164. package/apps/control-plane/src/supervisor/plan-conformance-scorer.ts +2 -5
  165. package/apps/control-plane/src/supervisor/planner-phase.ts +85 -0
  166. package/apps/control-plane/src/supervisor/planning-wave-executor.ts +993 -64
  167. package/apps/control-plane/src/supervisor/prompt-bundle-loader.ts +20 -1
  168. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +384 -177
  169. package/apps/control-plane/src/supervisor/run-coordinator.ts +723 -20
  170. package/apps/control-plane/src/supervisor/runtime.ts +485 -9
  171. package/apps/control-plane/src/supervisor/session-orchestrator.ts +220 -1
  172. package/apps/control-plane/src/supervisor/types.ts +152 -1
  173. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +1030 -92
  174. package/apps/control-plane/test/activity-monitor.spec.ts +76 -0
  175. package/apps/control-plane/test/add-command-handler.spec.ts +189 -0
  176. package/apps/control-plane/test/application/services/feature-state-service.spec.ts +208 -0
  177. package/apps/control-plane/test/artifact-stager.spec.ts +93 -0
  178. package/apps/control-plane/test/batch-operations.spec.ts +58 -0
  179. package/apps/control-plane/test/bootstrap-edge-cases.spec.ts +50 -2
  180. package/apps/control-plane/test/bootstrap-manifest-generator-service.spec.ts +99 -0
  181. package/apps/control-plane/test/bootstrap.spec.ts +177 -4
  182. package/apps/control-plane/test/checkpoint-service.spec.ts +977 -29
  183. package/apps/control-plane/test/cli-argument-parser.spec.ts +119 -0
  184. package/apps/control-plane/test/cli-helpers.spec.ts +1202 -12
  185. package/apps/control-plane/test/cli.unit.spec.ts +797 -16
  186. package/apps/control-plane/test/collision-command-handler.spec.ts +182 -0
  187. package/apps/control-plane/test/collision-override-service.spec.ts +878 -0
  188. package/apps/control-plane/test/collision-queue.spec.ts +430 -2
  189. package/apps/control-plane/test/collisions.spec.ts +209 -1
  190. package/apps/control-plane/test/core-utils.spec.ts +61 -0
  191. package/apps/control-plane/test/cost-tracking.spec.ts +224 -0
  192. package/apps/control-plane/test/dashboard-api.integration.spec.ts +185 -5
  193. package/apps/control-plane/test/dashboard-client.spec.ts +948 -0
  194. package/apps/control-plane/test/dashboard-command.spec.ts +138 -6
  195. package/apps/control-plane/test/dashboard-runtime-runner.spec.ts +1550 -0
  196. package/apps/control-plane/test/dashboard-runtime.spec.ts +138 -0
  197. package/apps/control-plane/test/dashboard-ui-utils.spec.ts +56 -12
  198. package/apps/control-plane/test/dependency-scheduler.spec.ts +7 -1
  199. package/apps/control-plane/test/env-file.spec.ts +76 -0
  200. package/apps/control-plane/test/execution-control-service.spec.ts +535 -0
  201. package/apps/control-plane/test/execution-enrollment-service.spec.ts +648 -0
  202. package/apps/control-plane/test/feature-lifecycle.spec.ts +126 -0
  203. package/apps/control-plane/test/feature-resume-phase.spec.ts +164 -0
  204. package/apps/control-plane/test/feature-send-message-service.spec.ts +161 -0
  205. package/apps/control-plane/test/feature-state-service.spec.ts +295 -0
  206. package/apps/control-plane/test/fs.spec.ts +80 -0
  207. package/apps/control-plane/test/gate-freshness.spec.ts +590 -0
  208. package/apps/control-plane/test/gate-log-extractor.spec.ts +170 -0
  209. package/apps/control-plane/test/gates.spec.ts +108 -0
  210. package/apps/control-plane/test/git-reconciliation-service.spec.ts +2307 -0
  211. package/apps/control-plane/test/helpers.ts +65 -0
  212. package/apps/control-plane/test/incremental-gates.spec.ts +271 -0
  213. package/apps/control-plane/test/index-normalizer.spec.ts +98 -0
  214. package/apps/control-plane/test/init-wizard.spec.ts +17 -0
  215. package/apps/control-plane/test/intake-artifacts.spec.ts +203 -0
  216. package/apps/control-plane/test/intake-service.spec.ts +3176 -0
  217. package/apps/control-plane/test/kernel-collision-replay.spec.ts +3 -2
  218. package/apps/control-plane/test/kernel-tool-executor.spec.ts +77 -0
  219. package/apps/control-plane/test/kernel-tool-wiring.spec.ts +279 -0
  220. package/apps/control-plane/test/kernel.branches.spec.ts +15 -2
  221. package/apps/control-plane/test/kernel.coverage.spec.ts +7 -3
  222. package/apps/control-plane/test/kernel.coverage2.spec.ts +731 -2
  223. package/apps/control-plane/test/kernel.spec.ts +464 -2
  224. package/apps/control-plane/test/mainline-conflict.spec.ts +66 -0
  225. package/apps/control-plane/test/mcp-helpers.spec.ts +79 -0
  226. package/apps/control-plane/test/mcp.spec.ts +177 -13
  227. package/apps/control-plane/test/merge-command-handler.spec.ts +531 -0
  228. package/apps/control-plane/test/merge-service.spec.ts +570 -4
  229. package/apps/control-plane/test/notifier-service.spec.ts +26 -0
  230. package/apps/control-plane/test/organizer-enrollment-scheduler.spec.ts +340 -0
  231. package/apps/control-plane/test/organizer-ordering-artifact.spec.ts +95 -0
  232. package/apps/control-plane/test/organizer-sidecar-service.spec.ts +468 -0
  233. package/apps/control-plane/test/output-loop-detector.spec.ts +6 -0
  234. package/apps/control-plane/test/path-layout.spec.ts +70 -0
  235. package/apps/control-plane/test/performance-analytics.spec.ts +124 -0
  236. package/apps/control-plane/test/plan-conformance-scorer.spec.ts +53 -0
  237. package/apps/control-plane/test/plan-service.spec.ts +686 -4
  238. package/apps/control-plane/test/planning-wave-executor.spec.ts +3272 -86
  239. package/apps/control-plane/test/policy-loader-service.spec.ts +5 -0
  240. package/apps/control-plane/test/prompt-overlay.spec.ts +65 -0
  241. package/apps/control-plane/test/provider-command-runner-epipe.spec.ts +64 -0
  242. package/apps/control-plane/test/providers/api-worker-provider.spec.ts +129 -0
  243. package/apps/control-plane/test/providers/cli-worker-provider.spec.ts +148 -0
  244. package/apps/control-plane/test/providers/usage-types.spec.ts +98 -0
  245. package/apps/control-plane/test/providers.spec.ts +293 -16
  246. package/apps/control-plane/test/question-command-handlers.spec.ts +156 -0
  247. package/apps/control-plane/test/question-service.spec.ts +1119 -0
  248. package/apps/control-plane/test/reactions.spec.ts +114 -0
  249. package/apps/control-plane/test/replay-command-handler.spec.ts +144 -0
  250. package/apps/control-plane/test/replay-timeline-service.spec.ts +459 -0
  251. package/apps/control-plane/test/response.spec.ts +31 -0
  252. package/apps/control-plane/test/resume-command.spec.ts +757 -9
  253. package/apps/control-plane/test/retry-resume-decision.spec.ts +133 -0
  254. package/apps/control-plane/test/rollback-command-handler.spec.ts +334 -0
  255. package/apps/control-plane/test/rollback-command.spec.ts +120 -0
  256. package/apps/control-plane/test/run-coordinator.spec.ts +3062 -404
  257. package/apps/control-plane/test/schemas/state.schema.spec.ts +71 -0
  258. package/apps/control-plane/test/service-retry-paths.spec.ts +112 -0
  259. package/apps/control-plane/test/services.spec.ts +472 -2
  260. package/apps/control-plane/test/session-management.spec.ts +346 -1
  261. package/apps/control-plane/test/spec-ingestion.spec.ts +102 -28
  262. package/apps/control-plane/test/spec-preparation.spec.ts +182 -0
  263. package/apps/control-plane/test/supervisor-collaborators.spec.ts +191 -3
  264. package/apps/control-plane/test/supervisor.calltool.spec.ts +198 -0
  265. package/apps/control-plane/test/supervisor.spec.ts +95 -16
  266. package/apps/control-plane/test/supervisor.unit.spec.ts +385 -18
  267. package/apps/control-plane/test/tool-runtime.spec.ts +122 -0
  268. package/apps/control-plane/test/worker-decision-loop.spec.ts +3479 -476
  269. package/apps/control-plane/test/worker-execution-policy.spec.ts +1416 -6
  270. package/apps/control-plane/test/worker-provider-adapters.spec.ts +1894 -37
  271. package/apps/control-plane/test/worker-provider-factory.spec.ts +81 -0
  272. package/apps/control-plane/test/worktree-watchdog-service.spec.ts +125 -0
  273. package/apps/control-plane/vitest.config.ts +5 -0
  274. package/config/agentic/orchestrator/agents.yaml +22 -1
  275. package/config/agentic/orchestrator/gates.yaml +24 -7
  276. package/config/agentic/orchestrator/policy.yaml +23 -1
  277. package/config/agentic/orchestrator/prompts/builder.system.md +69 -20
  278. package/config/agentic/orchestrator/prompts/organizer.system.md +85 -0
  279. package/config/agentic/orchestrator/prompts/overrides/builder.claude.md +28 -0
  280. package/config/agentic/orchestrator/prompts/overrides/builder.codex.md +28 -0
  281. package/config/agentic/orchestrator/prompts/overrides/planner.claude.md +20 -0
  282. package/config/agentic/orchestrator/prompts/overrides/planner.codex.md +20 -0
  283. package/config/agentic/orchestrator/prompts/planner-intake.system.md +149 -0
  284. package/config/agentic/orchestrator/prompts/planner.system.md +113 -40
  285. package/config/agentic/orchestrator/prompts/qa.system.md +75 -18
  286. package/config/agentic/orchestrator/prompts/reconciler.system.md +119 -0
  287. package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +26 -2
  288. package/dist/apps/control-plane/application/kernel-tool-wiring.js +40 -2
  289. package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -1
  290. package/dist/apps/control-plane/application/services/activity-monitor-service.js +37 -1
  291. package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -1
  292. package/dist/apps/control-plane/application/services/bootstrap-manifest-generator-service.d.ts +4 -0
  293. package/dist/apps/control-plane/application/services/bootstrap-manifest-generator-service.js +188 -0
  294. package/dist/apps/control-plane/application/services/bootstrap-manifest-generator-service.js.map +1 -0
  295. package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +5 -0
  296. package/dist/apps/control-plane/application/services/checkpoint-service.js +69 -24
  297. package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -1
  298. package/dist/apps/control-plane/application/services/collision-override-service.d.ts +139 -0
  299. package/dist/apps/control-plane/application/services/collision-override-service.js +568 -0
  300. package/dist/apps/control-plane/application/services/collision-override-service.js.map +1 -0
  301. package/dist/apps/control-plane/application/services/collision-queue-service.d.ts +15 -0
  302. package/dist/apps/control-plane/application/services/collision-queue-service.js +92 -33
  303. package/dist/apps/control-plane/application/services/collision-queue-service.js.map +1 -1
  304. package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +11 -0
  305. package/dist/apps/control-plane/application/services/cost-tracking-service.js +75 -0
  306. package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -1
  307. package/dist/apps/control-plane/application/services/execution-control-service.d.ts +75 -0
  308. package/dist/apps/control-plane/application/services/execution-control-service.js +421 -0
  309. package/dist/apps/control-plane/application/services/execution-control-service.js.map +1 -0
  310. package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +1 -0
  311. package/dist/apps/control-plane/application/services/feature-deletion-service.js +23 -1
  312. package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
  313. package/dist/apps/control-plane/application/services/feature-lifecycle-service.d.ts +24 -1
  314. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js +132 -3
  315. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js.map +1 -1
  316. package/dist/apps/control-plane/application/services/feature-send-message-service.js +16 -8
  317. package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -1
  318. package/dist/apps/control-plane/application/services/feature-state-service.d.ts +36 -0
  319. package/dist/apps/control-plane/application/services/feature-state-service.js +163 -6
  320. package/dist/apps/control-plane/application/services/feature-state-service.js.map +1 -1
  321. package/dist/apps/control-plane/application/services/gate-service.d.ts +2 -1
  322. package/dist/apps/control-plane/application/services/gate-service.js +95 -5
  323. package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
  324. package/dist/apps/control-plane/application/services/git-reconciliation-service.d.ts +92 -0
  325. package/dist/apps/control-plane/application/services/git-reconciliation-service.js +1097 -0
  326. package/dist/apps/control-plane/application/services/git-reconciliation-service.js.map +1 -0
  327. package/dist/apps/control-plane/application/services/intake-service.d.ts +63 -0
  328. package/dist/apps/control-plane/application/services/intake-service.js +1050 -0
  329. package/dist/apps/control-plane/application/services/intake-service.js.map +1 -0
  330. package/dist/apps/control-plane/application/services/merge-service.d.ts +5 -1
  331. package/dist/apps/control-plane/application/services/merge-service.js +233 -18
  332. package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
  333. package/dist/apps/control-plane/application/services/notifier-service.d.ts +1 -1
  334. package/dist/apps/control-plane/application/services/notifier-service.js +1 -0
  335. package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -1
  336. package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +11 -0
  337. package/dist/apps/control-plane/application/services/performance-analytics-service.js +59 -0
  338. package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -1
  339. package/dist/apps/control-plane/application/services/plan-service.d.ts +5 -0
  340. package/dist/apps/control-plane/application/services/plan-service.js +254 -15
  341. package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
  342. package/dist/apps/control-plane/application/services/question-service.d.ts +72 -0
  343. package/dist/apps/control-plane/application/services/question-service.js +507 -0
  344. package/dist/apps/control-plane/application/services/question-service.js.map +1 -0
  345. package/dist/apps/control-plane/application/services/reactions-service.d.ts +2 -0
  346. package/dist/apps/control-plane/application/services/reactions-service.js +60 -17
  347. package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -1
  348. package/dist/apps/control-plane/application/services/replay-timeline-service.d.ts +39 -0
  349. package/dist/apps/control-plane/application/services/replay-timeline-service.js +205 -0
  350. package/dist/apps/control-plane/application/services/replay-timeline-service.js.map +1 -0
  351. package/dist/apps/control-plane/application/services/reporting-service.d.ts +59 -0
  352. package/dist/apps/control-plane/application/services/reporting-service.js +121 -9
  353. package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
  354. package/dist/apps/control-plane/application/services/run-lease-service.d.ts +20 -0
  355. package/dist/apps/control-plane/application/services/run-lease-service.js +81 -4
  356. package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
  357. package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +10 -0
  358. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +65 -8
  359. package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -1
  360. package/dist/apps/control-plane/application/tools/tool-metadata.js +7 -0
  361. package/dist/apps/control-plane/application/tools/tool-metadata.js.map +1 -1
  362. package/dist/apps/control-plane/application/usage-types.d.ts +65 -0
  363. package/dist/apps/control-plane/application/usage-types.js +75 -0
  364. package/dist/apps/control-plane/application/usage-types.js.map +1 -0
  365. package/dist/apps/control-plane/cli/add-command-handler.d.ts +18 -0
  366. package/dist/apps/control-plane/cli/add-command-handler.js +110 -0
  367. package/dist/apps/control-plane/cli/add-command-handler.js.map +1 -0
  368. package/dist/apps/control-plane/cli/answer-command-handler.d.ts +8 -0
  369. package/dist/apps/control-plane/cli/answer-command-handler.js +96 -0
  370. package/dist/apps/control-plane/cli/answer-command-handler.js.map +1 -0
  371. package/dist/apps/control-plane/cli/attach-command-handler.js +8 -3
  372. package/dist/apps/control-plane/cli/attach-command-handler.js.map +1 -1
  373. package/dist/apps/control-plane/cli/cli-argument-parser.js +131 -11
  374. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  375. package/dist/apps/control-plane/cli/collision-command-handler.d.ts +8 -0
  376. package/dist/apps/control-plane/cli/collision-command-handler.js +90 -0
  377. package/dist/apps/control-plane/cli/collision-command-handler.js.map +1 -0
  378. package/dist/apps/control-plane/cli/command-catalog.d.ts +21 -0
  379. package/dist/apps/control-plane/cli/command-catalog.js +416 -0
  380. package/dist/apps/control-plane/cli/command-catalog.js.map +1 -0
  381. package/dist/apps/control-plane/cli/complete-command-handler.d.ts +15 -0
  382. package/dist/apps/control-plane/cli/complete-command-handler.js +26 -0
  383. package/dist/apps/control-plane/cli/complete-command-handler.js.map +1 -0
  384. package/dist/apps/control-plane/cli/completion-command-handler.d.ts +8 -0
  385. package/dist/apps/control-plane/cli/completion-command-handler.js +20 -0
  386. package/dist/apps/control-plane/cli/completion-command-handler.js.map +1 -0
  387. package/dist/apps/control-plane/cli/completion-resolver.d.ts +1 -0
  388. package/dist/apps/control-plane/cli/completion-resolver.js +250 -0
  389. package/dist/apps/control-plane/cli/completion-resolver.js.map +1 -0
  390. package/dist/apps/control-plane/cli/completion-shell-renderer.d.ts +3 -0
  391. package/dist/apps/control-plane/cli/completion-shell-renderer.js +53 -0
  392. package/dist/apps/control-plane/cli/completion-shell-renderer.js.map +1 -0
  393. package/dist/apps/control-plane/cli/dashboard-command-handler.d.ts +1 -0
  394. package/dist/apps/control-plane/cli/dashboard-command-handler.js +84 -1
  395. package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -1
  396. package/dist/apps/control-plane/cli/dashboard-runtime-runner.d.ts +81 -0
  397. package/dist/apps/control-plane/cli/dashboard-runtime-runner.js +724 -0
  398. package/dist/apps/control-plane/cli/dashboard-runtime-runner.js.map +1 -0
  399. package/dist/apps/control-plane/cli/dashboard-runtime.d.ts +1 -0
  400. package/dist/apps/control-plane/cli/dashboard-runtime.js +26 -0
  401. package/dist/apps/control-plane/cli/dashboard-runtime.js.map +1 -0
  402. package/dist/apps/control-plane/cli/help-command-handler.js +13 -172
  403. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  404. package/dist/apps/control-plane/cli/init-command-handler.js +51 -6
  405. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  406. package/dist/apps/control-plane/cli/merge-command-handler.d.ts +8 -0
  407. package/dist/apps/control-plane/cli/merge-command-handler.js +139 -0
  408. package/dist/apps/control-plane/cli/merge-command-handler.js.map +1 -0
  409. package/dist/apps/control-plane/cli/questions-command-handler.d.ts +8 -0
  410. package/dist/apps/control-plane/cli/questions-command-handler.js +59 -0
  411. package/dist/apps/control-plane/cli/questions-command-handler.js.map +1 -0
  412. package/dist/apps/control-plane/cli/replay-command-handler.d.ts +15 -0
  413. package/dist/apps/control-plane/cli/replay-command-handler.js +55 -0
  414. package/dist/apps/control-plane/cli/replay-command-handler.js.map +1 -0
  415. package/dist/apps/control-plane/cli/resume-command-handler.d.ts +2 -0
  416. package/dist/apps/control-plane/cli/resume-command-handler.js +180 -17
  417. package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
  418. package/dist/apps/control-plane/cli/retry-command-handler.js +202 -16
  419. package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -1
  420. package/dist/apps/control-plane/cli/retry-resume-decision.d.ts +26 -0
  421. package/dist/apps/control-plane/cli/retry-resume-decision.js +61 -0
  422. package/dist/apps/control-plane/cli/retry-resume-decision.js.map +1 -0
  423. package/dist/apps/control-plane/cli/rollback-command-handler.js +3 -2
  424. package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -1
  425. package/dist/apps/control-plane/cli/run-command-handler.js +26 -2
  426. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  427. package/dist/apps/control-plane/cli/spec-ingestion-service.d.ts +2 -0
  428. package/dist/apps/control-plane/cli/spec-ingestion-service.js +37 -48
  429. package/dist/apps/control-plane/cli/spec-ingestion-service.js.map +1 -1
  430. package/dist/apps/control-plane/cli/spec-preparation.d.ts +14 -0
  431. package/dist/apps/control-plane/cli/spec-preparation.js +81 -0
  432. package/dist/apps/control-plane/cli/spec-preparation.js.map +1 -0
  433. package/dist/apps/control-plane/cli/spec-utils.d.ts +4 -0
  434. package/dist/apps/control-plane/cli/spec-utils.js +70 -11
  435. package/dist/apps/control-plane/cli/spec-utils.js.map +1 -1
  436. package/dist/apps/control-plane/cli/status-command-handler.js +69 -0
  437. package/dist/apps/control-plane/cli/status-command-handler.js.map +1 -1
  438. package/dist/apps/control-plane/cli/types.d.ts +41 -4
  439. package/dist/apps/control-plane/cli/types.js +9 -1
  440. package/dist/apps/control-plane/cli/types.js.map +1 -1
  441. package/dist/apps/control-plane/core/collisions.d.ts +37 -19
  442. package/dist/apps/control-plane/core/collisions.js +87 -12
  443. package/dist/apps/control-plane/core/collisions.js.map +1 -1
  444. package/dist/apps/control-plane/core/constants.d.ts +17 -1
  445. package/dist/apps/control-plane/core/constants.js +18 -1
  446. package/dist/apps/control-plane/core/constants.js.map +1 -1
  447. package/dist/apps/control-plane/core/error-codes.d.ts +39 -0
  448. package/dist/apps/control-plane/core/error-codes.js +39 -0
  449. package/dist/apps/control-plane/core/error-codes.js.map +1 -1
  450. package/dist/apps/control-plane/core/execution-control.d.ts +45 -0
  451. package/dist/apps/control-plane/core/execution-control.js +2 -0
  452. package/dist/apps/control-plane/core/execution-control.js.map +1 -0
  453. package/dist/apps/control-plane/core/feature-resume-phase.d.ts +3 -0
  454. package/dist/apps/control-plane/core/feature-resume-phase.js +88 -0
  455. package/dist/apps/control-plane/core/feature-resume-phase.js.map +1 -0
  456. package/dist/apps/control-plane/core/gate-freshness.d.ts +48 -0
  457. package/dist/apps/control-plane/core/gate-freshness.js +267 -0
  458. package/dist/apps/control-plane/core/gate-freshness.js.map +1 -0
  459. package/dist/apps/control-plane/core/gate-log-extractor.d.ts +22 -0
  460. package/dist/apps/control-plane/core/gate-log-extractor.js +66 -0
  461. package/dist/apps/control-plane/core/gate-log-extractor.js.map +1 -0
  462. package/dist/apps/control-plane/core/gates.d.ts +11 -2
  463. package/dist/apps/control-plane/core/gates.js +67 -3
  464. package/dist/apps/control-plane/core/gates.js.map +1 -1
  465. package/dist/apps/control-plane/core/intake-artifacts.d.ts +109 -0
  466. package/dist/apps/control-plane/core/intake-artifacts.js +143 -0
  467. package/dist/apps/control-plane/core/intake-artifacts.js.map +1 -0
  468. package/dist/apps/control-plane/core/kernel-types.d.ts +8 -0
  469. package/dist/apps/control-plane/core/kernel.d.ts +256 -8
  470. package/dist/apps/control-plane/core/kernel.js +400 -14
  471. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  472. package/dist/apps/control-plane/core/mainline-conflict.d.ts +7 -0
  473. package/dist/apps/control-plane/core/mainline-conflict.js +20 -0
  474. package/dist/apps/control-plane/core/mainline-conflict.js.map +1 -0
  475. package/dist/apps/control-plane/core/merge-repair.d.ts +35 -0
  476. package/dist/apps/control-plane/core/merge-repair.js +99 -0
  477. package/dist/apps/control-plane/core/merge-repair.js.map +1 -0
  478. package/dist/apps/control-plane/core/path-layout.d.ts +10 -0
  479. package/dist/apps/control-plane/core/path-layout.js +32 -2
  480. package/dist/apps/control-plane/core/path-layout.js.map +1 -1
  481. package/dist/apps/control-plane/core/path-rules.js +9 -3
  482. package/dist/apps/control-plane/core/path-rules.js.map +1 -1
  483. package/dist/apps/control-plane/core/plan-submit-recovery.d.ts +22 -0
  484. package/dist/apps/control-plane/core/plan-submit-recovery.js +78 -0
  485. package/dist/apps/control-plane/core/plan-submit-recovery.js.map +1 -0
  486. package/dist/apps/control-plane/core/questions.d.ts +40 -0
  487. package/dist/apps/control-plane/core/questions.js +2 -0
  488. package/dist/apps/control-plane/core/questions.js.map +1 -0
  489. package/dist/apps/control-plane/core/runtime-sessions.d.ts +4 -0
  490. package/dist/apps/control-plane/core/schemas.d.ts +2 -0
  491. package/dist/apps/control-plane/core/schemas.js +31 -1
  492. package/dist/apps/control-plane/core/schemas.js.map +1 -1
  493. package/dist/apps/control-plane/core/tool-caller.d.ts +18 -1
  494. package/dist/apps/control-plane/core/utils/index-normalizer.js +17 -4
  495. package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -1
  496. package/dist/apps/control-plane/core/worktree-diff.d.ts +4 -0
  497. package/dist/apps/control-plane/core/worktree-diff.js +52 -0
  498. package/dist/apps/control-plane/core/worktree-diff.js.map +1 -0
  499. package/dist/apps/control-plane/index.d.ts +10 -2
  500. package/dist/apps/control-plane/index.js +9 -2
  501. package/dist/apps/control-plane/index.js.map +1 -1
  502. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +236 -6
  503. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  504. package/dist/apps/control-plane/mcp/kernel-tool-executor.js +16 -0
  505. package/dist/apps/control-plane/mcp/kernel-tool-executor.js.map +1 -1
  506. package/dist/apps/control-plane/mcp/tool-runtime.d.ts +5 -0
  507. package/dist/apps/control-plane/mcp/tool-runtime.js +40 -5
  508. package/dist/apps/control-plane/mcp/tool-runtime.js.map +1 -1
  509. package/dist/apps/control-plane/providers/api-worker-provider.d.ts +2 -2
  510. package/dist/apps/control-plane/providers/api-worker-provider.js +40 -9
  511. package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
  512. package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +59 -3
  513. package/dist/apps/control-plane/providers/cli-worker-provider.js +758 -46
  514. package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
  515. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js +91 -1
  516. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js.map +1 -1
  517. package/dist/apps/control-plane/providers/output-parsers/types.d.ts +2 -0
  518. package/dist/apps/control-plane/providers/provider-defaults.d.ts +12 -0
  519. package/dist/apps/control-plane/providers/provider-defaults.js +103 -7
  520. package/dist/apps/control-plane/providers/provider-defaults.js.map +1 -1
  521. package/dist/apps/control-plane/providers/providers.d.ts +50 -4
  522. package/dist/apps/control-plane/providers/providers.js +145 -14
  523. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  524. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +2 -0
  525. package/dist/apps/control-plane/providers/worker-provider-factory.js +8 -1
  526. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
  527. package/dist/apps/control-plane/supervisor/artifact-stager.d.ts +5 -0
  528. package/dist/apps/control-plane/supervisor/artifact-stager.js +45 -0
  529. package/dist/apps/control-plane/supervisor/artifact-stager.js.map +1 -0
  530. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +24 -1
  531. package/dist/apps/control-plane/supervisor/build-wave-executor.js +362 -150
  532. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  533. package/dist/apps/control-plane/supervisor/execution-enrollment-service.d.ts +41 -0
  534. package/dist/apps/control-plane/supervisor/execution-enrollment-service.js +311 -0
  535. package/dist/apps/control-plane/supervisor/execution-enrollment-service.js.map +1 -0
  536. package/dist/apps/control-plane/supervisor/organizer-enrollment-scheduler.d.ts +15 -0
  537. package/dist/apps/control-plane/supervisor/organizer-enrollment-scheduler.js +93 -0
  538. package/dist/apps/control-plane/supervisor/organizer-enrollment-scheduler.js.map +1 -0
  539. package/dist/apps/control-plane/supervisor/organizer-sidecar-service.d.ts +44 -0
  540. package/dist/apps/control-plane/supervisor/organizer-sidecar-service.js +311 -0
  541. package/dist/apps/control-plane/supervisor/organizer-sidecar-service.js.map +1 -0
  542. package/dist/apps/control-plane/supervisor/plan-conformance-scorer.js +2 -5
  543. package/dist/apps/control-plane/supervisor/plan-conformance-scorer.js.map +1 -1
  544. package/dist/apps/control-plane/supervisor/planner-phase.d.ts +3 -0
  545. package/dist/apps/control-plane/supervisor/planner-phase.js +70 -0
  546. package/dist/apps/control-plane/supervisor/planner-phase.js.map +1 -0
  547. package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +42 -0
  548. package/dist/apps/control-plane/supervisor/planning-wave-executor.js +753 -55
  549. package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
  550. package/dist/apps/control-plane/supervisor/prompt-bundle-loader.js +19 -1
  551. package/dist/apps/control-plane/supervisor/prompt-bundle-loader.js.map +1 -1
  552. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +21 -0
  553. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +287 -156
  554. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  555. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +30 -1
  556. package/dist/apps/control-plane/supervisor/run-coordinator.js +561 -17
  557. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  558. package/dist/apps/control-plane/supervisor/runtime.d.ts +84 -0
  559. package/dist/apps/control-plane/supervisor/runtime.js +393 -3
  560. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  561. package/dist/apps/control-plane/supervisor/session-orchestrator.d.ts +54 -0
  562. package/dist/apps/control-plane/supervisor/session-orchestrator.js +176 -1
  563. package/dist/apps/control-plane/supervisor/session-orchestrator.js.map +1 -1
  564. package/dist/apps/control-plane/supervisor/types.d.ts +142 -1
  565. package/dist/apps/control-plane/supervisor/types.js.map +1 -1
  566. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +68 -2
  567. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +723 -89
  568. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  569. package/docs/core/ARCHITECTURE.md +227 -0
  570. package/docs/core/DECISIONS.md +94 -0
  571. package/docs/core/DOMAIN-LOGIC.md +60 -0
  572. package/docs/core/PATTERNS.md +201 -0
  573. package/docs/core/TROUBLESHOOTING.md +347 -0
  574. package/docs/core/intentgraph-dependencies.json +39860 -0
  575. package/docs/core/intentgraph.index.json +46580 -0
  576. package/docs/plans/2026-03-10-gate-failure-targeted-repair-design.md +224 -0
  577. package/docs/plans/2026-03-10-gate-failure-targeted-repair.md +1032 -0
  578. package/docs/superpowers/plans/2026-03-16-provider-cli-config.md +743 -0
  579. package/docs/superpowers/plans/2026-03-23-reconcile-divergence-fix.md +777 -0
  580. package/docs/superpowers/plans/2026-03-28-ordering-agent-implementation.md +1754 -0
  581. package/docs/superpowers/plans/2026-03-29-drop-zone-and-provider-optimization.md +1108 -0
  582. package/docs/superpowers/plans/2026-03-29-merge-target-feature-branch.md +685 -0
  583. package/docs/superpowers/plans/2026-03-29-organizer-sidecar-runtime-loop.md +1289 -0
  584. package/docs/superpowers/specs/2026-03-23-reconcile-divergence-fix-design.md +118 -0
  585. package/docs/superpowers/specs/2026-03-28-ordering-agent-spec-audit-design.md +50 -0
  586. package/docs/superpowers/specs/2026-03-29-drop-zone-and-provider-optimization-design.md +254 -0
  587. package/docs/superpowers/specs/2026-03-29-merge-target-feature-branch-design.md +152 -0
  588. package/docs/superpowers/specs/2026-03-29-organizer-sidecar-runtime-loop-design.md +225 -0
  589. package/package.json +3 -2
  590. package/packages/web-dashboard/package.json +2 -1
  591. package/packages/web-dashboard/src/app/analytics/page.tsx +36 -2
  592. package/packages/web-dashboard/src/app/api/actions/route.ts +274 -63
  593. package/packages/web-dashboard/src/app/api/actions/status/route.ts +35 -0
  594. package/packages/web-dashboard/src/app/api/analytics/provider/route.ts +18 -0
  595. package/packages/web-dashboard/src/app/api/collisions/approve/route.ts +58 -0
  596. package/packages/web-dashboard/src/app/api/features/[id]/checkpoint-diff/route.ts +36 -0
  597. package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/route.ts +29 -0
  598. package/packages/web-dashboard/src/app/api/features/[id]/conflicts/abort/route.ts +29 -0
  599. package/packages/web-dashboard/src/app/api/features/[id]/conflicts/files/route.ts +30 -0
  600. package/packages/web-dashboard/src/app/api/features/[id]/conflicts/resolve/route.ts +51 -0
  601. package/packages/web-dashboard/src/app/api/features/[id]/conflicts/route.ts +75 -0
  602. package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +16 -2
  603. package/packages/web-dashboard/src/app/api/features/[id]/files/route.ts +26 -0
  604. package/packages/web-dashboard/src/app/api/features/[id]/gate-history/route.ts +27 -0
  605. package/packages/web-dashboard/src/app/api/features/[id]/genealogy/route.ts +26 -0
  606. package/packages/web-dashboard/src/app/api/features/[id]/history/run/[runId]/route.ts +20 -0
  607. package/packages/web-dashboard/src/app/api/features/[id]/history/runs/route.ts +34 -0
  608. package/packages/web-dashboard/src/app/api/features/[id]/intake-workspace/route.ts +20 -0
  609. package/packages/web-dashboard/src/app/api/features/[id]/live-output/route.ts +74 -0
  610. package/packages/web-dashboard/src/app/api/features/[id]/plan/amend/route.ts +21 -0
  611. package/packages/web-dashboard/src/app/api/features/[id]/plan-progress/route.ts +20 -0
  612. package/packages/web-dashboard/src/app/api/features/[id]/planner-artifacts/[artifact]/route.ts +78 -0
  613. package/packages/web-dashboard/src/app/api/features/[id]/planner-lifecycle/route.ts +20 -0
  614. package/packages/web-dashboard/src/app/api/features/[id]/planning-workspace/route.ts +20 -0
  615. package/packages/web-dashboard/src/app/api/features/[id]/questions/[questionId]/answer/route.ts +27 -0
  616. package/packages/web-dashboard/src/app/api/features/[id]/questions/route.ts +18 -0
  617. package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +14 -7
  618. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +57 -2
  619. package/packages/web-dashboard/src/app/api/features/[id]/spec/route.ts +30 -0
  620. package/packages/web-dashboard/src/app/api/features/[id]/triage/route.ts +83 -0
  621. package/packages/web-dashboard/src/app/api/features/[id]/worker-events/route.ts +40 -0
  622. package/packages/web-dashboard/src/app/api/launch/preview/route.ts +86 -0
  623. package/packages/web-dashboard/src/app/api/launch/submit/route.ts +180 -0
  624. package/packages/web-dashboard/src/app/api/mainline/status/route.ts +74 -0
  625. package/packages/web-dashboard/src/app/api/merge-queue/route.ts +13 -0
  626. package/packages/web-dashboard/src/app/api/policy/budget/route.ts +14 -0
  627. package/packages/web-dashboard/src/app/api/projects/route.ts +11 -7
  628. package/packages/web-dashboard/src/app/api/reconciler/queue/route.ts +47 -0
  629. package/packages/web-dashboard/src/app/api/run/route.ts +26 -2
  630. package/packages/web-dashboard/src/app/api/runtime/events/route.ts +227 -0
  631. package/packages/web-dashboard/src/app/api/runtime/operations/route.ts +269 -0
  632. package/packages/web-dashboard/src/app/api/runtime/questions/route.ts +11 -0
  633. package/packages/web-dashboard/src/app/api/runtime/runs/route.ts +80 -0
  634. package/packages/web-dashboard/src/app/api/status/route.ts +4 -2
  635. package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -42
  636. package/packages/web-dashboard/src/app/globals.css +34 -3
  637. package/packages/web-dashboard/src/app/launch/page.tsx +362 -0
  638. package/packages/web-dashboard/src/app/layout.tsx +23 -1
  639. package/packages/web-dashboard/src/app/page.tsx +263 -272
  640. package/packages/web-dashboard/src/components/dashboard/attention-strip.tsx +52 -0
  641. package/packages/web-dashboard/src/components/dashboard/collision-approval-drawer.tsx +185 -0
  642. package/packages/web-dashboard/src/components/dashboard/command-center-header.tsx +102 -0
  643. package/packages/web-dashboard/src/components/dashboard/mainline-status-banner.tsx +84 -0
  644. package/packages/web-dashboard/src/components/dashboard/merged-archive.tsx +36 -0
  645. package/packages/web-dashboard/src/components/dashboard/prioritized-queues.tsx +98 -0
  646. package/packages/web-dashboard/src/components/dashboard/reconciler-queue-card.tsx +115 -0
  647. package/packages/web-dashboard/src/components/dashboard/secondary-diagnostics-rail.tsx +48 -0
  648. package/packages/web-dashboard/src/components/dashboard/task-filter-bar.tsx +74 -0
  649. package/packages/web-dashboard/src/components/dashboard/triage-drawer.tsx +455 -0
  650. package/packages/web-dashboard/src/components/diff-viewer.tsx +19 -3
  651. package/packages/web-dashboard/src/components/evidence-viewer.tsx +65 -51
  652. package/packages/web-dashboard/src/components/feature-card.tsx +90 -7
  653. package/packages/web-dashboard/src/components/feature-cost-panel.tsx +112 -11
  654. package/packages/web-dashboard/src/components/feature-list-view.tsx +25 -4
  655. package/packages/web-dashboard/src/components/features/runtime-inspector/EventsTimelineView.tsx +260 -0
  656. package/packages/web-dashboard/src/components/features/runtime-inspector/OperationsListView.tsx +172 -0
  657. package/packages/web-dashboard/src/components/features/runtime-inspector/RuntimeInspectorPanel.tsx +896 -0
  658. package/packages/web-dashboard/src/components/filter-bar.tsx +7 -39
  659. package/packages/web-dashboard/src/components/focus/ActionableRiskList.tsx +46 -0
  660. package/packages/web-dashboard/src/components/focus/AgentRolePerformanceCard.tsx +200 -0
  661. package/packages/web-dashboard/src/components/focus/BlockedGuidanceBanner.tsx +149 -0
  662. package/packages/web-dashboard/src/components/focus/CheckpointInspector.tsx +123 -0
  663. package/packages/web-dashboard/src/components/focus/CheckpointRail.tsx +118 -0
  664. package/packages/web-dashboard/src/components/focus/CheckpointScrubber.tsx +249 -0
  665. package/packages/web-dashboard/src/components/focus/CollisionApprovalBanner.tsx +192 -0
  666. package/packages/web-dashboard/src/components/focus/CollisionRadar.tsx +136 -0
  667. package/packages/web-dashboard/src/components/focus/ConflictStatusCard.tsx +52 -0
  668. package/packages/web-dashboard/src/components/focus/ContextSidebar.tsx +108 -0
  669. package/packages/web-dashboard/src/components/focus/DiagnosisPanel.tsx +68 -0
  670. package/packages/web-dashboard/src/components/focus/FeatureDecisionBanner.tsx +68 -0
  671. package/packages/web-dashboard/src/components/focus/FeatureQuestionAnswerPanel.tsx +167 -0
  672. package/packages/web-dashboard/src/components/focus/FocusHeader.tsx +54 -0
  673. package/packages/web-dashboard/src/components/focus/FocusLayout.tsx +283 -0
  674. package/packages/web-dashboard/src/components/focus/GateFlakinessSummary.tsx +144 -0
  675. package/packages/web-dashboard/src/components/focus/GenealogyTree.tsx +34 -0
  676. package/packages/web-dashboard/src/components/focus/HeroBlock.tsx +67 -0
  677. package/packages/web-dashboard/src/components/focus/LiveAgentConsole.tsx +277 -0
  678. package/packages/web-dashboard/src/components/focus/MergeQueueCard.tsx +78 -0
  679. package/packages/web-dashboard/src/components/focus/OperationalSummaryCard.tsx +227 -0
  680. package/packages/web-dashboard/src/components/focus/PinnedActions.tsx +96 -0
  681. package/packages/web-dashboard/src/components/focus/PlanAmendmentPanel.tsx +250 -0
  682. package/packages/web-dashboard/src/components/focus/PlanProgressPanel.tsx +133 -0
  683. package/packages/web-dashboard/src/components/focus/PlannerArtifactViewer.tsx +158 -0
  684. package/packages/web-dashboard/src/components/focus/PlannerLifecycleHeader.tsx +141 -0
  685. package/packages/web-dashboard/src/components/focus/ProgressSnapshotCard.tsx +113 -0
  686. package/packages/web-dashboard/src/components/focus/RecentMaterialChanges.tsx +69 -0
  687. package/packages/web-dashboard/src/components/focus/RoleLogViewer.tsx +436 -0
  688. package/packages/web-dashboard/src/components/focus/RunHistoryBrowser.tsx +62 -0
  689. package/packages/web-dashboard/src/components/focus/SpecViewer.tsx +172 -0
  690. package/packages/web-dashboard/src/components/focus/TabBar.tsx +33 -0
  691. package/packages/web-dashboard/src/components/focus/UsageBurnChart.tsx +212 -0
  692. package/packages/web-dashboard/src/components/focus/VerificationSummaryCard.tsx +122 -0
  693. package/packages/web-dashboard/src/components/focus/tabs/ChangesTab.tsx +325 -0
  694. package/packages/web-dashboard/src/components/focus/tabs/ConflictsTab.tsx +395 -0
  695. package/packages/web-dashboard/src/components/focus/tabs/GatesQaTab.tsx +38 -0
  696. package/packages/web-dashboard/src/components/focus/tabs/HistoryTab.tsx +213 -0
  697. package/packages/web-dashboard/src/components/focus/tabs/IntakeTab.tsx +429 -0
  698. package/packages/web-dashboard/src/components/focus/tabs/OverviewTab.tsx +217 -0
  699. package/packages/web-dashboard/src/components/focus/tabs/PlanningTab.tsx +390 -0
  700. package/packages/web-dashboard/src/components/focus/tabs/ReviewTab.tsx +497 -0
  701. package/packages/web-dashboard/src/components/focus/tabs/RuntimeTab.tsx +213 -0
  702. package/packages/web-dashboard/src/components/focus/tabs/TranscriptTab.tsx +315 -0
  703. package/packages/web-dashboard/src/components/gate-results.tsx +2 -2
  704. package/packages/web-dashboard/src/components/human-input-panel.tsx +33 -57
  705. package/packages/web-dashboard/src/components/kanban-board.tsx +4 -0
  706. package/packages/web-dashboard/src/components/launch/launch-draft-card.tsx +154 -0
  707. package/packages/web-dashboard/src/components/plan-viewer.tsx +147 -69
  708. package/packages/web-dashboard/src/components/quick-launch-panel.tsx +20 -47
  709. package/packages/web-dashboard/src/components/summary-bar.tsx +30 -76
  710. package/packages/web-dashboard/src/lib/aop-client.ts +2484 -36
  711. package/packages/web-dashboard/src/lib/blocked-state-guidance.ts +475 -0
  712. package/packages/web-dashboard/src/lib/collision-radar.ts +136 -0
  713. package/packages/web-dashboard/src/lib/dashboard-action-states.ts +204 -0
  714. package/packages/web-dashboard/src/lib/dashboard-runtime-client.ts +439 -0
  715. package/packages/web-dashboard/src/lib/dashboard-utils.ts +179 -18
  716. package/packages/web-dashboard/src/lib/drop-zone-utils.ts +92 -0
  717. package/packages/web-dashboard/src/lib/focus-detail-derivations.ts +958 -0
  718. package/packages/web-dashboard/src/lib/focus-view.ts +300 -0
  719. package/packages/web-dashboard/src/lib/health-diagnosis.ts +356 -0
  720. package/packages/web-dashboard/src/lib/launch-contracts.ts +77 -0
  721. package/packages/web-dashboard/src/lib/launch-markdown.ts +103 -0
  722. package/packages/web-dashboard/src/lib/launch-page-preview.ts +89 -0
  723. package/packages/web-dashboard/src/lib/live-feed.ts +1 -1
  724. package/packages/web-dashboard/src/lib/multi-project-config.ts +33 -0
  725. package/packages/web-dashboard/src/lib/orchestrator-tools.ts +881 -60
  726. package/packages/web-dashboard/src/lib/planner-workspace.ts +1285 -0
  727. package/packages/web-dashboard/src/lib/review-contracts.ts +5 -3
  728. package/packages/web-dashboard/src/lib/runtime-files.ts +285 -0
  729. package/packages/web-dashboard/src/lib/tool-catalog.ts +51 -0
  730. package/packages/web-dashboard/src/lib/types.ts +731 -3
  731. package/packages/web-dashboard/src/lib/usage-burn.ts +175 -0
  732. package/packages/web-dashboard/src/lib/worktree-diff.ts +128 -0
  733. package/packages/web-dashboard/src/styles/dashboard.module.css +1742 -459
  734. package/packages/web-dashboard/test/api/actions/route.spec.ts +675 -0
  735. package/packages/web-dashboard/test/api/features/diff.route.spec.ts +57 -0
  736. package/packages/web-dashboard/test/api/features/feature.route.spec.ts +99 -0
  737. package/packages/web-dashboard/test/api/features/live-output.route.spec.ts +123 -0
  738. package/packages/web-dashboard/test/api/features/plan-amend.route.spec.ts +95 -0
  739. package/packages/web-dashboard/test/api/features/planner-workspaces.route.spec.ts +162 -0
  740. package/packages/web-dashboard/test/api/features/question-answer.route.spec.ts +99 -0
  741. package/packages/web-dashboard/test/api/features/triage.route.spec.ts +195 -0
  742. package/packages/web-dashboard/test/api/launch/preview.route.spec.ts +149 -0
  743. package/packages/web-dashboard/test/api/launch/submit.route.spec.ts +382 -0
  744. package/packages/web-dashboard/test/api/runtime/events/route.spec.ts +164 -0
  745. package/packages/web-dashboard/test/api/runtime/operations/route.spec.ts +156 -0
  746. package/packages/web-dashboard/test/api/runtime/runs/route.spec.ts +112 -0
  747. package/packages/web-dashboard/test/components/changes-tab.spec.tsx +76 -0
  748. package/packages/web-dashboard/test/components/command-center-root.spec.tsx +87 -0
  749. package/packages/web-dashboard/test/components/diagnosis-panel.spec.tsx +59 -0
  750. package/packages/web-dashboard/test/components/feature-card.spec.tsx +45 -0
  751. package/packages/web-dashboard/test/components/focus-layout.spec.tsx +299 -0
  752. package/packages/web-dashboard/test/components/gate-results.spec.tsx +39 -0
  753. package/packages/web-dashboard/test/components/gates-qa-tab.spec.tsx +118 -0
  754. package/packages/web-dashboard/test/components/human-input-panel.spec.tsx +54 -0
  755. package/packages/web-dashboard/test/components/intake-tab.spec.tsx +210 -0
  756. package/packages/web-dashboard/test/components/kanban-board.spec.tsx +35 -0
  757. package/packages/web-dashboard/test/components/launch-draft-card.spec.tsx +114 -0
  758. package/packages/web-dashboard/test/components/launch-page.spec.tsx +79 -0
  759. package/packages/web-dashboard/test/components/overview-tab.spec.tsx +236 -0
  760. package/packages/web-dashboard/test/components/planning-tab.spec.tsx +202 -0
  761. package/packages/web-dashboard/test/components/review-tab.spec.tsx +169 -0
  762. package/packages/web-dashboard/test/components/role-log-viewer.spec.ts +42 -0
  763. package/packages/web-dashboard/test/components/runtime-inspector.spec.tsx +22 -0
  764. package/packages/web-dashboard/test/components/runtime-tab.spec.tsx +133 -0
  765. package/packages/web-dashboard/test/components/transcript-tab.spec.tsx +46 -0
  766. package/packages/web-dashboard/test/components/triage-drawer.spec.tsx +159 -0
  767. package/packages/web-dashboard/test/lib/aop-client.spec.ts +235 -0
  768. package/packages/web-dashboard/test/lib/dashboard-runtime-client.spec.ts +144 -0
  769. package/packages/web-dashboard/test/lib/focus-detail-derivations.spec.ts +314 -0
  770. package/packages/web-dashboard/test/lib/focus-view.spec.ts +248 -0
  771. package/packages/web-dashboard/test/lib/health-diagnosis.spec.ts +277 -0
  772. package/packages/web-dashboard/test/lib/launch-markdown.spec.ts +36 -0
  773. package/packages/web-dashboard/test/lib/multi-project-config.spec.ts +54 -0
  774. package/packages/web-dashboard/test/lib/orchestrator-tools.spec.ts +352 -0
  775. package/packages/web-dashboard/test/lib/planner-workspace.spec.ts +289 -0
  776. package/packages/web-dashboard/test/lib/worktree-diff.spec.ts +119 -0
  777. package/packages/web-dashboard/vitest.config.ts +2 -0
  778. package/spec-files/completed/agentic_orchestrator_add_feature_to_active_execution_spec.md +557 -0
  779. package/spec-files/completed/agentic_orchestrator_dashboard_command_center_redesign_spec.md +1147 -0
  780. package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +18 -16
  781. package/spec-files/completed/agentic_orchestrator_feature_focus_view_track_a_spec.md +672 -0
  782. package/spec-files/completed/agentic_orchestrator_feature_focus_view_track_b_spec.md +794 -0
  783. package/spec-files/completed/agentic_orchestrator_feature_focus_view_track_c_decision_centric_remediation_spec.md +1037 -0
  784. package/spec-files/completed/agentic_orchestrator_feature_focus_view_ux_redesign_spec.md +1432 -0
  785. package/spec-files/completed/agentic_orchestrator_focus_plan_tab_intake_planning_workspace_spec.md +921 -0
  786. package/spec-files/completed/agentic_orchestrator_intentional_collision_override_spec.md +584 -0
  787. package/spec-files/completed/agentic_orchestrator_interactive_planning_intake_and_requirements_verification_spec.md +1185 -0
  788. package/spec-files/completed/agentic_orchestrator_reactive_execution_enrollment_spec.md +864 -0
  789. package/spec-files/{outstanding → completed}/agentic_orchestrator_runtime_inspection_spec.md +92 -19
  790. package/spec-files/completed/agentic_orchestrator_scope_aware_run_lease_spec.md +408 -0
  791. package/spec-files/completed/git-reconciliation-engine.md +827 -0
  792. package/spec-files/outstanding/agentic_orchestrator_dashboard_quick_launch_and_control_surface_spec.md +331 -0
  793. package/spec-files/outstanding/agentic_orchestrator_enterprise_governance_dashboard_spec.md +16 -6
  794. package/spec-files/outstanding/agentic_orchestrator_evidence_integrity_doctor_spec.md +60 -9
  795. package/spec-files/outstanding/agentic_orchestrator_focus_plan_tab_execution_contract_workspace_spec.md +616 -0
  796. package/spec-files/outstanding/agentic_orchestrator_headless_standby_dashboard_runtime_spec.md +310 -0
  797. package/spec-files/outstanding/agentic_orchestrator_human_input_interaction_protocol_spec.md +175 -72
  798. package/spec-files/outstanding/agentic_orchestrator_interactive_rename_cleanup_spec.md +197 -0
  799. package/spec-files/outstanding/agentic_orchestrator_interactive_resume_and_reconciliation_disposition_spec.md +412 -0
  800. package/spec-files/outstanding/agentic_orchestrator_knowledge_canary_spec.md +166 -137
  801. package/spec-files/outstanding/agentic_orchestrator_observability_replay_spec.md +3 -3
  802. package/spec-files/outstanding/agentic_orchestrator_phase_specific_agent_profiles_and_token_telemetry_spec.md +303 -0
  803. package/spec-files/outstanding/agentic_orchestrator_planning_review_quality_spec.md +18 -5
  804. package/spec-files/outstanding/agentic_orchestrator_policy_stratification_spec.md +225 -0
  805. package/spec-files/outstanding/agentic_orchestrator_quality_adoption_execution_spec.md +77 -50
  806. package/spec-files/outstanding/agentic_orchestrator_ready_to_merge_branch_handoff_spec.md +724 -0
  807. package/spec-files/outstanding/agentic_orchestrator_remove_deterministic_mode_spec.md +263 -0
  808. package/spec-files/outstanding/agentic_orchestrator_request_more_context_and_dashboard_human_input_spec.md +456 -0
  809. package/spec-files/outstanding/agentic_orchestrator_spec_coverage_and_reconciliation_enforcement_spec.md +1411 -0
  810. package/spec-files/outstanding/agentic_orchestrator_spec_ordering_agent_spec.md +370 -0
  811. package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1 -1
  812. package/spec-files/progress.md +2026 -120
  813. package/specs/001-runtime-inspection/checklists/requirements.md +35 -0
  814. package/specs/001-runtime-inspection/design.md +338 -0
  815. package/specs/001-runtime-inspection/spec.md +95 -0
  816. package/specs/002-scope-aware-lease/checklists/requirements.md +35 -0
  817. package/specs/002-scope-aware-lease/contracts/lease-registry.schema.json +101 -0
  818. package/specs/002-scope-aware-lease/data-model.md +236 -0
  819. package/specs/002-scope-aware-lease/plan.md +766 -0
  820. package/specs/002-scope-aware-lease/quickstart.md +150 -0
  821. package/specs/002-scope-aware-lease/research.md +135 -0
  822. package/specs/002-scope-aware-lease/spec.md +128 -0
  823. package/specs/002-scope-aware-lease/tasks.md +767 -0
  824. package/tsconfig.json +1 -1
  825. package/vitest.config.ts +28 -0
  826. package/ARCHITECTURE_ADHERENCE_ANALYSIS.md +0 -871
  827. package/packages/web-dashboard/next-env.d.ts +0 -6
  828. package/packages/web-dashboard/src/components/detail-panel.tsx +0 -1124
  829. package/packages/web-dashboard/src/components/review-workspace.tsx +0 -1162
  830. /package/spec-files/{outstanding → completed}/agentic_orchestrator_artifact_database_publishing_spec.md +0 -0
  831. /package/spec-files/{outstanding → completed}/agentic_orchestrator_cli_shell_tab_completion_spec.md +0 -0
  832. /package/spec-files/{outstanding → completed}/agentic_orchestrator_dashboard_diff_and_agent_console_spec.md +0 -0
  833. /package/spec-files/{outstanding → completed}/agentic_orchestrator_performance_improvements_spec.md +0 -0
  834. /package/spec-files/{outstanding → completed}/agentic_orchestrator_persistent_worker_runtime_spec.md +0 -0
  835. /package/spec-files/{outstanding → completed}/agentic_orchestrator_provider_auth_bootstrap_spec.md +0 -0
  836. /package/spec-files/{outstanding → completed}/agentic_orchestrator_real_worker_provider_execution_spec.md +0 -0
@@ -1,40 +1,92 @@
1
1
  import { readdir, readFile, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
+ import { buildCollisionRadar } from '@/lib/collision-radar.js';
4
5
  import { getProjectByName, readMultiProjectConfig } from '@/lib/multi-project-config.js';
5
- import { parseUnifiedDiff } from '@/lib/dashboard-utils.js';
6
+ import { hasGateFailure, normalizeCiStatus, parseUnifiedDiff } from '@/lib/dashboard-utils.js';
7
+ import {
8
+ buildActionableRisks,
9
+ buildPlanProgressSummary,
10
+ buildRevisionSummary,
11
+ buildVerificationSummary,
12
+ summarizeFeatureSpec,
13
+ } from '@/lib/focus-detail-derivations.js';
14
+ import {
15
+ buildFeatureIntakeWorkspace,
16
+ buildFeaturePlanningWorkspace,
17
+ buildPlannerLifecycleSummary,
18
+ normalizeAcceptedPlanRecord,
19
+ normalizeBootstrapManifestRecord,
20
+ normalizeIntakeReviewRecord,
21
+ normalizeVerifiedManifestRecord,
22
+ } from '@/lib/planner-workspace.js';
23
+ import { readPolicyYaml } from '@/lib/policy-reader.js';
24
+ import { buildUsageBurnProjection } from '@/lib/usage-burn.js';
6
25
  import type {
7
26
  AgentLogEntry,
27
+ AgentRolePerformanceCardData,
28
+ AgentRolePerformanceMetric,
8
29
  AgentSessionCluster,
9
30
  AgentPipelineStatus,
31
+ CollisionRadarSnapshot,
10
32
  CostSummary,
11
33
  DashboardStatusPayload,
12
34
  EvidenceArtifact,
35
+ FeatureBudgetState,
36
+ FeatureCheckpointsPage,
13
37
  FeatureCheckpointComparison,
14
38
  FeatureCheckpointDiff,
15
39
  FeatureDetail,
40
+ FeatureFileIndex,
41
+ FeatureGenealogy,
42
+ FeatureGenealogyNode,
43
+ FeatureIntakeWorkspace,
16
44
  FeatureLog,
17
45
  FeatureLogEntry,
46
+ FeaturePlanningWorkspace,
47
+ FeatureQuestion,
48
+ FeatureRunHistoryDetail,
49
+ FeatureRunHistoryEntry,
50
+ FeatureSpecSummary,
18
51
  FeaturesIndex,
52
+ GateHistoryEntry,
19
53
  FeatureSummary,
54
+ FeatureTriageDetail,
20
55
  FeatureCheckpoint,
21
56
  FeatureExecutionMode,
57
+ FeatureIntakeReviewSummary,
58
+ FeatureIntakeSummary,
22
59
  GateRunEvidence,
23
60
  InteractiveExecutionModeMetrics,
24
61
  InteractivePerformanceLatestCheckpoint,
25
62
  InteractivePerformanceMetrics,
63
+ LiveAgentOutputSnapshot,
64
+ LiveAgentOutputStatusSnapshot,
26
65
  LockLease,
66
+ MergeQueueEntry,
67
+ MergeQueueSnapshot,
68
+ PlannerArtifactView,
69
+ PlannerLifecycleSummary,
70
+ RolePerformanceSummary,
27
71
  QaTestIndex,
28
72
  RawLogFileMeta,
29
73
  ReviewBrief,
30
74
  RoleStatus,
31
75
  RuntimeSession,
76
+ TokenCostConversion,
77
+ ProviderAnalytics,
32
78
  WorkerEventEntry,
79
+ WorkerEventSummary,
80
+ OrganizerStatusProjection,
33
81
  } from '@/lib/types.js';
34
82
 
35
83
  const AOP_ROOT = process.env.AOP_ROOT ?? process.cwd();
36
- const LOG_FILENAME_PATTERN = /^(planner|builder|qa)-(\d{13})\.txt$/;
84
+ const LOG_FILENAME_PATTERN = /^(planner|builder|qa|reconciler)-(\d{13})\.txt$/;
37
85
  const CHECKPOINT_METRICS_HISTORY_LIMIT = 200;
86
+ const TRACKED_AGENT_ROLES = ['planner', 'builder', 'qa', 'reconciler'] as const;
87
+ export const DEFAULT_LIVE_AGENT_OUTPUT_LINE_LIMIT = 500;
88
+
89
+ type TrackedAgentRole = (typeof TRACKED_AGENT_ROLES)[number];
38
90
 
39
91
  interface ParsedFrontMatter {
40
92
  frontMatter: Record<string, unknown>;
@@ -72,9 +124,11 @@ interface InteractiveMetricsFileShape {
72
124
  }
73
125
 
74
126
  const STATUS_TO_PHASE: Record<string, FeatureSummary['phase']> = {
127
+ intake: 'intake',
75
128
  planning: 'planning',
76
129
  building: 'building',
77
130
  qa: 'qa',
131
+ awaiting_input: 'awaiting_input',
78
132
  ready_to_merge: 'ready_to_merge',
79
133
  merged: 'merged',
80
134
  blocked: 'blocked',
@@ -83,11 +137,21 @@ const STATUS_TO_PHASE: Record<string, FeatureSummary['phase']> = {
83
137
  };
84
138
 
85
139
  const ACTIVE_PHASES = new Set<FeatureSummary['phase']>([
140
+ 'intake',
86
141
  'planning',
87
142
  'building',
88
143
  'qa',
89
144
  'ready_to_merge',
90
145
  ]);
146
+ const DEFAULT_BUDGET_ALERT_THRESHOLD = 0.8;
147
+ const STALE_FEATURE_MS = 20 * 60_000;
148
+ const RECENT_CHANGE_MS = 24 * 60_000 * 60;
149
+
150
+ interface BudgetConfig {
151
+ per_feature_limit_tokens: number | null;
152
+ alert_threshold: number;
153
+ token_cost_conversion: TokenCostConversion | null;
154
+ }
91
155
 
92
156
  function isActivityState(value: unknown): value is NonNullable<FeatureSummary['activity_state']> {
93
157
  return (
@@ -100,6 +164,331 @@ function isActivityState(value: unknown): value is NonNullable<FeatureSummary['a
100
164
  );
101
165
  }
102
166
 
167
+ function parseBudgetConfig(policy: Record<string, unknown> | null): BudgetConfig {
168
+ const budget =
169
+ policy && typeof policy.budget === 'object' ? (policy.budget as Record<string, unknown>) : null;
170
+ const tokenCostConversion =
171
+ budget?.token_cost_conversion &&
172
+ typeof budget.token_cost_conversion === 'object' &&
173
+ typeof (budget.token_cost_conversion as Record<string, unknown>).usd_per_1k_tokens === 'number'
174
+ ? {
175
+ usd_per_1k_tokens: (budget.token_cost_conversion as Record<string, number>)
176
+ .usd_per_1k_tokens,
177
+ }
178
+ : null;
179
+
180
+ return {
181
+ per_feature_limit_tokens:
182
+ typeof budget?.per_feature_limit_tokens === 'number' ? budget.per_feature_limit_tokens : null,
183
+ alert_threshold:
184
+ typeof budget?.alert_threshold === 'number'
185
+ ? budget.alert_threshold
186
+ : DEFAULT_BUDGET_ALERT_THRESHOLD,
187
+ token_cost_conversion: tokenCostConversion,
188
+ };
189
+ }
190
+
191
+ interface LiveAgentOutputReadOptions {
192
+ tailLines?: number;
193
+ }
194
+
195
+ function tailTextByLines(content: string, lineLimit: number | undefined): string {
196
+ if (!lineLimit || lineLimit < 1 || content.length === 0) {
197
+ return content;
198
+ }
199
+ let newlineCount = 0;
200
+ let start = 0;
201
+ for (let index = content.length - 1; index >= 0; index -= 1) {
202
+ if (content.charCodeAt(index) !== 10) {
203
+ continue;
204
+ }
205
+ if (index === content.length - 1) {
206
+ continue;
207
+ }
208
+ newlineCount += 1;
209
+ if (newlineCount >= lineLimit) {
210
+ start = index + 1;
211
+ break;
212
+ }
213
+ }
214
+ return content.slice(start);
215
+ }
216
+
217
+ function deriveBudgetState(
218
+ tokensUsed: number | null,
219
+ budgetConfig: BudgetConfig,
220
+ ): FeatureBudgetState {
221
+ if (tokensUsed == null || budgetConfig.per_feature_limit_tokens == null) {
222
+ return 'unknown';
223
+ }
224
+ if (budgetConfig.per_feature_limit_tokens <= 0) {
225
+ return 'unknown';
226
+ }
227
+
228
+ const ratio = tokensUsed / budgetConfig.per_feature_limit_tokens;
229
+ if (ratio >= 1) {
230
+ return 'exceeded';
231
+ }
232
+ if (ratio >= 0.9) {
233
+ return 'critical';
234
+ }
235
+ if (ratio >= budgetConfig.alert_threshold) {
236
+ return 'warning';
237
+ }
238
+ return 'normal';
239
+ }
240
+
241
+ function deriveCostFromTokens(
242
+ tokensUsed: number | null,
243
+ tokenCostConversion: TokenCostConversion | null,
244
+ ): number | null {
245
+ if (tokensUsed == null || tokenCostConversion == null) {
246
+ return null;
247
+ }
248
+ return Number(((tokensUsed / 1000) * tokenCostConversion.usd_per_1k_tokens).toFixed(4));
249
+ }
250
+
251
+ function lastMeaningfulChange(frontMatter: Record<string, unknown>): string | undefined {
252
+ const recovery =
253
+ frontMatter.recovery && typeof frontMatter.recovery === 'object'
254
+ ? (frontMatter.recovery as Record<string, unknown>)
255
+ : null;
256
+ const candidates = [
257
+ recovery?.last_attempt_at,
258
+ frontMatter.last_meaningful_change_at,
259
+ frontMatter.activity_last_event_at,
260
+ frontMatter.last_updated,
261
+ ];
262
+ for (const candidate of candidates) {
263
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
264
+ return candidate;
265
+ }
266
+ }
267
+ return undefined;
268
+ }
269
+
270
+ function isRecentChange(iso: string | undefined, nowMs = Date.now()): boolean {
271
+ if (!iso) {
272
+ return false;
273
+ }
274
+ const timestamp = Date.parse(iso);
275
+ if (Number.isNaN(timestamp) || timestamp > nowMs) {
276
+ return false;
277
+ }
278
+ return nowMs - timestamp <= RECENT_CHANGE_MS;
279
+ }
280
+
281
+ function isStalledFeature(feature: FeatureSummary, nowMs = Date.now()): boolean {
282
+ if (!ACTIVE_PHASES.has(feature.phase ?? 'unknown')) {
283
+ return false;
284
+ }
285
+ if (feature.activity_state === 'waiting_input' || feature.activity_state === 'blocked') {
286
+ return false;
287
+ }
288
+ const changeAt = feature.last_meaningful_change_at ?? feature.last_updated;
289
+ if (!changeAt) {
290
+ return false;
291
+ }
292
+ const timestamp = Date.parse(changeAt);
293
+ if (Number.isNaN(timestamp) || timestamp > nowMs) {
294
+ return false;
295
+ }
296
+ return nowMs - timestamp >= STALE_FEATURE_MS;
297
+ }
298
+
299
+ const STALE_RECOVERY_MS = 10 * 60 * 1000;
300
+
301
+ function isRetryingRecovery(
302
+ feature: Pick<FeatureSummary, 'phase' | 'recovery' | 'status_reason' | 'activity_state'>,
303
+ nowMs = Date.now(),
304
+ ): boolean {
305
+ if (!feature.recovery || feature.recovery.state !== 'retrying') {
306
+ return false;
307
+ }
308
+ if (feature.phase === 'merged' || feature.phase === 'ready_to_merge') {
309
+ return false;
310
+ }
311
+ // If the feature is not actively running and the last recovery attempt was
312
+ // long ago, the recovery is stale — the reconciler or agent is no longer
313
+ // working on it.
314
+ if (feature.activity_state !== 'active' && feature.recovery.last_attempt_at) {
315
+ const lastAttempt = Date.parse(feature.recovery.last_attempt_at);
316
+ if (!Number.isNaN(lastAttempt) && nowMs - lastAttempt > STALE_RECOVERY_MS) {
317
+ return false;
318
+ }
319
+ }
320
+ const cause = feature.recovery.cause?.trim().toLowerCase();
321
+ if (!cause) {
322
+ return true;
323
+ }
324
+ if (feature.phase !== 'blocked') {
325
+ return true;
326
+ }
327
+ return feature.status_reason?.trim().toLowerCase().startsWith(`${cause}:`) ?? false;
328
+ }
329
+
330
+ function formatRecoveryStatusLabel(feature: Pick<FeatureSummary, 'recovery'>): string {
331
+ const role = feature.recovery?.role?.trim();
332
+ if (!role) {
333
+ return 'Recovering';
334
+ }
335
+ return `${role.charAt(0).toUpperCase()}${role.slice(1)} Retrying`;
336
+ }
337
+
338
+ function deriveClientStatusLabel(feature: FeatureSummary, dependencyBlocked: boolean): string {
339
+ if (feature.status === 'failed') {
340
+ return 'Failed';
341
+ }
342
+ // Plan collision — user must approve overlap before the feature can proceed
343
+ if (feature.collision) {
344
+ return 'Plan Collision';
345
+ }
346
+ // Blocked by mainline divergence — user may need to commit outstanding changes
347
+ if (
348
+ feature.status === 'blocked' &&
349
+ feature.recovery?.state === 'retrying' &&
350
+ feature.recovery.cause === 'mainline_divergence'
351
+ ) {
352
+ return 'Merge Conflict';
353
+ }
354
+ if (feature.phase === 'intake') {
355
+ return feature.intake_status === 'awaiting_input' ? 'Needs Input' : 'In Intake';
356
+ }
357
+ if (feature.phase === 'ready_to_merge') {
358
+ return 'Ready';
359
+ }
360
+ if (feature.phase === 'merged') {
361
+ return 'Merged';
362
+ }
363
+ if (feature.activity_state === 'waiting_input' || (feature.waiting_input_count ?? 0) > 0) {
364
+ return 'Needs Input';
365
+ }
366
+ if (dependencyBlocked) {
367
+ return 'Waiting On Dependencies';
368
+ }
369
+ if (isRetryingRecovery(feature)) {
370
+ return formatRecoveryStatusLabel(feature);
371
+ }
372
+ if (normalizeCiStatus(feature.pr?.ci_status) === 'pending') {
373
+ return 'Waiting On CI';
374
+ }
375
+ if (feature.phase === 'blocked') {
376
+ return 'Blocked';
377
+ }
378
+ if (
379
+ hasGateFailure(feature) ||
380
+ feature.budget_state === 'critical' ||
381
+ feature.budget_state === 'exceeded'
382
+ ) {
383
+ return 'At Risk';
384
+ }
385
+ if (isStalledFeature(feature)) {
386
+ return 'Stalled';
387
+ }
388
+ if (feature.activity_state === 'active') {
389
+ return 'In Progress';
390
+ }
391
+ if (feature.activity_state === 'idle') {
392
+ return 'Idle';
393
+ }
394
+ return feature.status.replace(/_/g, ' ');
395
+ }
396
+
397
+ function deriveAttentionReasons(
398
+ feature: FeatureSummary,
399
+ dependencyBlocked: boolean,
400
+ nowMs = Date.now(),
401
+ ): string[] {
402
+ const reasons: string[] = [];
403
+ const ciStatus = normalizeCiStatus(feature.pr?.ci_status);
404
+
405
+ if (feature.activity_state === 'waiting_input' || (feature.waiting_input_count ?? 0) > 0) {
406
+ reasons.push('Needs Me');
407
+ }
408
+ if (feature.phase === 'intake' && feature.activity_state === 'active') {
409
+ reasons.push('Requirements Intake');
410
+ }
411
+ if (isRetryingRecovery(feature)) {
412
+ reasons.push('Recovering');
413
+ }
414
+ if (feature.collision) {
415
+ reasons.push('Plan Collision');
416
+ }
417
+ if (
418
+ (feature.phase === 'blocked' && !isRetryingRecovery(feature)) ||
419
+ hasGateFailure(feature) ||
420
+ feature.budget_state === 'critical' ||
421
+ feature.budget_state === 'exceeded'
422
+ ) {
423
+ reasons.push('At Risk');
424
+ }
425
+ if (feature.phase === 'ready_to_merge') {
426
+ reasons.push('Ready');
427
+ }
428
+ if (isStalledFeature(feature, nowMs)) {
429
+ reasons.push('Stalled');
430
+ }
431
+ if (dependencyBlocked) {
432
+ reasons.push('Waiting On Dependencies');
433
+ }
434
+ if (ciStatus === 'pending') {
435
+ reasons.push('Waiting On CI');
436
+ }
437
+ if (isRecentChange(feature.last_meaningful_change_at, nowMs)) {
438
+ reasons.push('Recently Changed');
439
+ }
440
+ return reasons;
441
+ }
442
+
443
+ function enrichFeatureSummary(
444
+ feature: FeatureSummary,
445
+ options: {
446
+ cost: CostSummary | null;
447
+ budgetConfig: BudgetConfig;
448
+ dependencyBlocked: boolean;
449
+ nowMs?: number;
450
+ },
451
+ ): FeatureSummary {
452
+ const nowMs = options.nowMs ?? Date.now();
453
+ const usageTokens = options.cost?.tokens_used ?? null;
454
+ const budgetState = deriveBudgetState(usageTokens, options.budgetConfig);
455
+ const hasWaitingInputSignal =
456
+ feature.status === 'awaiting_input' ||
457
+ feature.phase === 'awaiting_input' ||
458
+ feature.activity_state === 'waiting_input' ||
459
+ feature.status_reason?.includes('waiting_input') ||
460
+ feature.status_reason?.includes('awaiting_input') ||
461
+ false;
462
+ const enriched: FeatureSummary = {
463
+ ...feature,
464
+ usage_tokens: usageTokens,
465
+ budget_state: budgetState,
466
+ waiting_input_count: hasWaitingInputSignal ? (feature.open_question_count ?? 1) : 0,
467
+ last_meaningful_change_at:
468
+ feature.last_meaningful_change_at ?? feature.activity_last_event_at ?? feature.last_updated,
469
+ };
470
+
471
+ enriched.attention_reasons = deriveAttentionReasons(enriched, options.dependencyBlocked, nowMs);
472
+ enriched.client_status_label = deriveClientStatusLabel(enriched, options.dependencyBlocked);
473
+ return enriched;
474
+ }
475
+
476
+ function summarizeBlocker(feature: FeatureSummary, dependencySummary: string[]): string | null {
477
+ if (dependencySummary.length > 0) {
478
+ return `Waiting on ${dependencySummary.join(', ')}`;
479
+ }
480
+ if (feature.status_reason && feature.status_reason.trim().length > 0) {
481
+ return feature.status_reason;
482
+ }
483
+ if (feature.phase === 'blocked') {
484
+ return 'Blocked and needs operator attention';
485
+ }
486
+ if (feature.activity_state === 'waiting_input') {
487
+ return 'Waiting on operator input';
488
+ }
489
+ return null;
490
+ }
491
+
103
492
  function normalizeRoleStatus(value: unknown): RoleStatus {
104
493
  if (typeof value !== 'string') {
105
494
  return 'unknown';
@@ -169,7 +558,12 @@ function parseSessionCluster(frontMatter: Record<string, unknown>): AgentSession
169
558
  }
170
559
 
171
560
  function firstValidTimestamp(frontMatter: Record<string, unknown>): number | null {
561
+ const recovery =
562
+ frontMatter.recovery && typeof frontMatter.recovery === 'object'
563
+ ? (frontMatter.recovery as Record<string, unknown>)
564
+ : null;
172
565
  const candidates = [
566
+ recovery?.last_attempt_at,
173
567
  frontMatter.activity_last_event_at,
174
568
  frontMatter.last_heartbeat_at,
175
569
  frontMatter.last_updated,
@@ -190,12 +584,55 @@ function inferActivityState(
190
584
  frontMatter: Record<string, unknown>,
191
585
  phase: FeatureSummary['phase'],
192
586
  ): NonNullable<FeatureSummary['activity_state']> | undefined {
193
- if (isActivityState(frontMatter.activity_state)) {
194
- return frontMatter.activity_state;
587
+ const status = typeof frontMatter.status === 'string' ? frontMatter.status : '';
588
+ const statusReason =
589
+ typeof frontMatter.status_reason === 'string' ? frontMatter.status_reason.toLowerCase() : '';
590
+ const persistedActivity = isActivityState(frontMatter.activity_state)
591
+ ? frontMatter.activity_state
592
+ : undefined;
593
+ if (persistedActivity === 'waiting_input') {
594
+ if (
595
+ status === 'awaiting_input' ||
596
+ phase === 'awaiting_input' ||
597
+ statusReason.startsWith('waiting_input') ||
598
+ statusReason.startsWith('question_awaiting_input') ||
599
+ statusReason.includes('awaiting_input')
600
+ ) {
601
+ return 'waiting_input';
602
+ }
603
+ } else if (persistedActivity) {
604
+ return persistedActivity;
605
+ }
606
+
607
+ if (status === 'awaiting_input' || phase === 'awaiting_input') {
608
+ return 'waiting_input';
609
+ }
610
+
611
+ if (
612
+ statusReason.startsWith('waiting_input') ||
613
+ statusReason.startsWith('question_awaiting_input')
614
+ ) {
615
+ return 'waiting_input';
195
616
  }
196
617
 
197
- const status = typeof frontMatter.status === 'string' ? frontMatter.status : '';
198
618
  if (phase === 'blocked') {
619
+ const recovery =
620
+ frontMatter.recovery && typeof frontMatter.recovery === 'object'
621
+ ? (frontMatter.recovery as Record<string, unknown>)
622
+ : null;
623
+ const recoveryState = typeof recovery?.state === 'string' ? recovery.state : null;
624
+ const recoveryCause = typeof recovery?.cause === 'string' ? recovery.cause.toLowerCase() : null;
625
+ if (
626
+ recoveryState === 'retrying' &&
627
+ (!recoveryCause || statusReason.startsWith(`${recoveryCause}:`))
628
+ ) {
629
+ const timestamp = firstValidTimestamp(frontMatter);
630
+ if (timestamp == null) {
631
+ return 'blocked';
632
+ }
633
+ const ageMs = Date.now() - timestamp;
634
+ return ageMs >= 0 && ageMs <= 120_000 ? 'active' : 'blocked';
635
+ }
199
636
  return 'blocked';
200
637
  }
201
638
  if (phase === 'merged' || status === 'failed') {
@@ -239,12 +676,141 @@ function parseFrontMatter(raw: string): ParsedFrontMatter {
239
676
  return { frontMatter, body: normalized.slice(match[0].length) };
240
677
  }
241
678
 
679
+ function normalizeIntakeStatus(value: unknown): FeatureIntakeSummary['status'] | null {
680
+ return value === 'not_started' ||
681
+ value === 'in_progress' ||
682
+ value === 'awaiting_input' ||
683
+ value === 'verified'
684
+ ? value
685
+ : null;
686
+ }
687
+
688
+ function normalizeWholeNumber(value: unknown): number | null {
689
+ return typeof value === 'number' && Number.isFinite(value)
690
+ ? Math.max(0, Math.floor(value))
691
+ : null;
692
+ }
693
+
694
+ function normalizeFeatureRecoverySummary(
695
+ frontMatter: Record<string, unknown>,
696
+ ): FeatureSummary['recovery'] {
697
+ const raw = frontMatter.recovery;
698
+ if (!raw || typeof raw !== 'object') {
699
+ return null;
700
+ }
701
+ const recovery = raw as Record<string, unknown>;
702
+ const state =
703
+ recovery.state === 'retrying' || recovery.state === 'awaiting_operator' ? recovery.state : null;
704
+ if (!state) {
705
+ return null;
706
+ }
707
+ return {
708
+ state,
709
+ cause: typeof recovery.cause === 'string' ? recovery.cause : null,
710
+ role: typeof recovery.role === 'string' ? recovery.role : null,
711
+ resume_phase: typeof recovery.resume_phase === 'string' ? recovery.resume_phase : null,
712
+ attempt:
713
+ typeof recovery.attempt === 'number' && Number.isFinite(recovery.attempt)
714
+ ? Math.max(0, Math.floor(recovery.attempt))
715
+ : null,
716
+ run_id: typeof recovery.run_id === 'string' ? recovery.run_id : null,
717
+ started_at: typeof recovery.started_at === 'string' ? recovery.started_at : null,
718
+ last_attempt_at: typeof recovery.last_attempt_at === 'string' ? recovery.last_attempt_at : null,
719
+ };
720
+ }
721
+
722
+ function normalizeFeatureIntakeSummary(
723
+ frontMatter: Record<string, unknown>,
724
+ ): FeatureIntakeSummary | null {
725
+ const intake = asRecord(frontMatter.intake);
726
+ if (!intake) {
727
+ return null;
728
+ }
729
+ return {
730
+ status:
731
+ normalizeIntakeStatus(intake.status) ??
732
+ (frontMatter.status === 'awaiting_input' ? 'awaiting_input' : 'in_progress'),
733
+ bootstrap_manifest_version: normalizeWholeNumber(intake.bootstrap_manifest_version),
734
+ verified_manifest_version: normalizeWholeNumber(intake.verified_manifest_version),
735
+ open_ambiguity_count: normalizeWholeNumber(intake.open_ambiguity_count) ?? 0,
736
+ last_verified_at: typeof intake.last_verified_at === 'string' ? intake.last_verified_at : null,
737
+ promotion_basis: typeof intake.promotion_basis === 'string' ? intake.promotion_basis : null,
738
+ };
739
+ }
740
+
741
+ function normalizeMergeRepairSummary(frontMatter: Record<string, unknown>) {
742
+ const raw = asRecord(frontMatter.merge_repair);
743
+ if (!raw) {
744
+ return null;
745
+ }
746
+
747
+ const attempts = normalizeWholeNumber(raw.attempts) ?? 0;
748
+ const maxAttempts = Math.max(1, normalizeWholeNumber(raw.max_attempts) ?? 5);
749
+ const summary = {
750
+ attempts: Math.min(attempts, maxAttempts),
751
+ max_attempts: maxAttempts,
752
+ active: raw.active === true,
753
+ requested_at: typeof raw.requested_at === 'string' ? raw.requested_at : null,
754
+ last_failed_at: typeof raw.last_failed_at === 'string' ? raw.last_failed_at : null,
755
+ last_gate_evidence_path:
756
+ typeof raw.last_gate_evidence_path === 'string' ? raw.last_gate_evidence_path : null,
757
+ last_failure_summary:
758
+ typeof raw.last_failure_summary === 'string' ? raw.last_failure_summary : null,
759
+ reviewer_message: typeof raw.reviewer_message === 'string' ? raw.reviewer_message : null,
760
+ exhausted_at: typeof raw.exhausted_at === 'string' ? raw.exhausted_at : null,
761
+ };
762
+
763
+ if (
764
+ !summary.active &&
765
+ summary.attempts === 0 &&
766
+ summary.last_failed_at == null &&
767
+ summary.exhausted_at == null
768
+ ) {
769
+ return null;
770
+ }
771
+
772
+ return summary;
773
+ }
774
+
775
+ function normalizeIntakeReviewSummary(value: unknown): FeatureIntakeReviewSummary | null {
776
+ const record = asRecord(value);
777
+ if (!record) {
778
+ return null;
779
+ }
780
+ const ambiguities = Array.isArray(record.ambiguities) ? record.ambiguities : [];
781
+ return {
782
+ status: typeof record.status === 'string' ? record.status : null,
783
+ questions_open: normalizeWholeNumber(record.questions_open) ?? 0,
784
+ questions_resolved: normalizeWholeNumber(record.questions_resolved) ?? 0,
785
+ ambiguity_count: ambiguities.length,
786
+ };
787
+ }
788
+
789
+ function readQuestionAmbiguityId(details: Record<string, unknown>): string | null {
790
+ return typeof details.ambiguity_id === 'string' ? details.ambiguity_id : null;
791
+ }
792
+
793
+ function readQuestionObligationIds(details: Record<string, unknown>): string[] {
794
+ const raw = Array.isArray(details.obligation_ids) ? details.obligation_ids : [];
795
+ return raw.filter((item): item is string => typeof item === 'string');
796
+ }
797
+
242
798
  function asRecord(value: unknown): Record<string, unknown> | null {
243
799
  return value && typeof value === 'object' ? (value as Record<string, unknown>) : null;
244
800
  }
245
801
 
246
802
  function normalizeFeatureExecutionMode(value: unknown): FeatureExecutionMode | null {
247
- return value === 'deterministic' || value === 'interactive' ? value : null;
803
+ // Feature surfaces are interactive-only even when older state artifacts still
804
+ // record the legacy deterministic mode.
805
+ return value === 'interactive' || value === 'deterministic' ? 'interactive' : 'interactive';
806
+ }
807
+
808
+ function isIdleCheckpoint(checkpoint: FeatureCheckpoint): boolean {
809
+ return (
810
+ checkpoint.validation_status === 'skipped' &&
811
+ checkpoint.files_changed.length === 0 &&
812
+ checkpoint.net_new_worktree_change === false
813
+ );
248
814
  }
249
815
 
250
816
  function normalizeFeatureCheckpoints(frontMatter: Record<string, unknown>): FeatureCheckpoint[] {
@@ -286,6 +852,17 @@ function normalizeFeatureCheckpoints(frontMatter: Record<string, unknown>): Feat
286
852
  record.severity === 'critical'
287
853
  ? record.severity
288
854
  : undefined;
855
+ const headSha = typeof record.head_sha === 'string' ? record.head_sha.trim() : '';
856
+ const workspaceFingerprint =
857
+ typeof record.workspace_fingerprint === 'string' ? record.workspace_fingerprint.trim() : '';
858
+ const duplicateOfCheckpointId =
859
+ typeof record.duplicate_of_checkpoint_id === 'string'
860
+ ? record.duplicate_of_checkpoint_id.trim()
861
+ : '';
862
+ const netNewWorktreeChange =
863
+ typeof record.net_new_worktree_change === 'boolean'
864
+ ? record.net_new_worktree_change
865
+ : undefined;
289
866
  checkpoints.push({
290
867
  checkpoint_id: checkpointId,
291
868
  timestamp,
@@ -294,6 +871,10 @@ function normalizeFeatureCheckpoints(frontMatter: Record<string, unknown>): Feat
294
871
  violations,
295
872
  severity,
296
873
  diff_snapshot: diffSnapshot,
874
+ head_sha: headSha || undefined,
875
+ workspace_fingerprint: workspaceFingerprint || undefined,
876
+ duplicate_of_checkpoint_id: duplicateOfCheckpointId || undefined,
877
+ net_new_worktree_change: netNewWorktreeChange,
297
878
  });
298
879
  }
299
880
 
@@ -345,6 +926,19 @@ function truncateNumber(value: number | null, digits = 2): number | null {
345
926
  return Math.round(value * factor) / factor;
346
927
  }
347
928
 
929
+ function averageNumber(values: number[]): number | null {
930
+ if (values.length === 0) {
931
+ return null;
932
+ }
933
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
934
+ }
935
+
936
+ function uniqueSorted(values: string[]): string[] {
937
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0))).sort(
938
+ (left, right) => left.localeCompare(right),
939
+ );
940
+ }
941
+
348
942
  function resolveRepoRelativePath(repoRoot: string, relativePath: string): string | null {
349
943
  const resolved = path.resolve(repoRoot, relativePath);
350
944
  const normalizedRoot = path.resolve(repoRoot);
@@ -379,6 +973,145 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
379
973
  }
380
974
  }
381
975
 
976
+ function normalizeQuestionRecord(value: unknown): FeatureQuestion | null {
977
+ const record = asRecord(value);
978
+ if (!record) {
979
+ return null;
980
+ }
981
+ const questionId = typeof record.question_id === 'string' ? record.question_id : null;
982
+ const prompt = typeof record.prompt === 'string' ? record.prompt : null;
983
+ const status =
984
+ record.status === 'open' ||
985
+ record.status === 'answered' ||
986
+ record.status === 'expired' ||
987
+ record.status === 'withdrawn'
988
+ ? record.status
989
+ : null;
990
+ if (!questionId || !prompt || !status) {
991
+ return null;
992
+ }
993
+ return {
994
+ feature_id: typeof record.feature_id === 'string' ? record.feature_id : undefined,
995
+ question_id: questionId,
996
+ status,
997
+ blocking: record.blocking !== false,
998
+ question_type:
999
+ typeof record.question_type === 'string' ? record.question_type : 'clarification',
1000
+ role: typeof record.role === 'string' ? record.role : 'unknown',
1001
+ phase: typeof record.phase === 'string' ? record.phase : 'unknown',
1002
+ request_action:
1003
+ typeof record.request_action === 'string' ? record.request_action : 'ask_user_input',
1004
+ session_id: typeof record.session_id === 'string' ? record.session_id : undefined,
1005
+ prompt,
1006
+ details: asRecord(record.details) ?? {},
1007
+ expected_answer:
1008
+ record.expected_answer && typeof record.expected_answer === 'object'
1009
+ ? (record.expected_answer as FeatureQuestion['expected_answer'])
1010
+ : null,
1011
+ created_at:
1012
+ typeof record.created_at === 'string' ? record.created_at : new Date().toISOString(),
1013
+ expires_at: typeof record.expires_at === 'string' ? record.expires_at : null,
1014
+ answer:
1015
+ typeof record.answer === 'string' || (record.answer && typeof record.answer === 'object')
1016
+ ? (record.answer as FeatureQuestion['answer'])
1017
+ : null,
1018
+ answered_at: typeof record.answered_at === 'string' ? record.answered_at : null,
1019
+ answered_by: typeof record.answered_by === 'string' ? record.answered_by : null,
1020
+ resume_status: typeof record.resume_status === 'string' ? record.resume_status : null,
1021
+ resume_phase: typeof record.resume_phase === 'string' ? record.resume_phase : null,
1022
+ ambiguity_id: readQuestionAmbiguityId(asRecord(record.details) ?? {}),
1023
+ obligation_ids: readQuestionObligationIds(asRecord(record.details) ?? {}),
1024
+ intake_context: null,
1025
+ };
1026
+ }
1027
+
1028
+ function enrichQuestionRecord(
1029
+ question: FeatureQuestion,
1030
+ feature: FeatureSummary | null,
1031
+ ): FeatureQuestion {
1032
+ return {
1033
+ ...question,
1034
+ feature_id: feature?.feature_id ?? question.feature_id,
1035
+ intake_context:
1036
+ feature?.intake_status != null
1037
+ ? {
1038
+ status: feature.intake_status,
1039
+ bootstrap_manifest_version: feature.bootstrap_manifest_version ?? null,
1040
+ verified_manifest_version: feature.verified_manifest_version ?? null,
1041
+ open_ambiguity_count: feature.open_ambiguity_count ?? 0,
1042
+ last_verified_at: feature.last_verified_at ?? null,
1043
+ promotion_basis: feature.promotion_basis ?? null,
1044
+ }
1045
+ : null,
1046
+ };
1047
+ }
1048
+
1049
+ function isOperatorReadyQuestion(
1050
+ feature: FeatureSummary | null,
1051
+ question: FeatureQuestion,
1052
+ ): boolean {
1053
+ const baselineReady =
1054
+ question.status === 'open' &&
1055
+ question.blocking &&
1056
+ feature?.status === 'awaiting_input' &&
1057
+ feature.open_question_id === question.question_id;
1058
+ if (!baselineReady) {
1059
+ return false;
1060
+ }
1061
+ const resumesIntoIntake =
1062
+ question.resume_status === 'intake' ||
1063
+ question.resume_phase === 'intake' ||
1064
+ question.phase === 'intake';
1065
+ if (!resumesIntoIntake) {
1066
+ return true;
1067
+ }
1068
+ return feature.resume_status === 'intake' && feature.intake_status === 'awaiting_input';
1069
+ }
1070
+
1071
+ function selectOperatorReadyOpenQuestions(
1072
+ feature: FeatureSummary | null,
1073
+ questions: FeatureQuestion[],
1074
+ ): FeatureQuestion[] {
1075
+ return questions.filter((question) => isOperatorReadyQuestion(feature, question));
1076
+ }
1077
+
1078
+ export async function readFeatureQuestions(
1079
+ featureId: string,
1080
+ repoRoot = AOP_ROOT,
1081
+ feature: FeatureSummary | null = null,
1082
+ ): Promise<FeatureQuestion[]> {
1083
+ const questionsPath = path.join(getFeatureRoot(featureId, repoRoot), 'questions.json');
1084
+ const parsed = await readJsonFile<Record<string, unknown>>(questionsPath);
1085
+ if (!parsed || !Array.isArray(parsed.items)) {
1086
+ return [];
1087
+ }
1088
+ return parsed.items
1089
+ .map((item) => normalizeQuestionRecord(item))
1090
+ .filter((item): item is FeatureQuestion => item !== null)
1091
+ .map((item) => enrichQuestionRecord({ ...item, feature_id: featureId }, feature))
1092
+ .sort((left, right) => left.created_at.localeCompare(right.created_at));
1093
+ }
1094
+
1095
+ export async function readPendingQuestions(repoRoot = AOP_ROOT): Promise<FeatureQuestion[]> {
1096
+ const index = await readFeaturesIndex(repoRoot);
1097
+ const featureIds = new Set<string>([
1098
+ ...(index.active ?? []),
1099
+ ...(index.blocked ?? []),
1100
+ ...(index.merged ?? []),
1101
+ ...(index.blocked_queue ?? []).map((entry) => entry.feature_id),
1102
+ ]);
1103
+ const questions = await Promise.all(
1104
+ [...featureIds]
1105
+ .sort((a, b) => a.localeCompare(b))
1106
+ .map(async (featureId) => {
1107
+ const feature = await readFeatureState(featureId, repoRoot);
1108
+ const items = await readFeatureQuestions(featureId, repoRoot, feature);
1109
+ return selectOperatorReadyOpenQuestions(feature, items);
1110
+ }),
1111
+ );
1112
+ return questions.flat();
1113
+ }
1114
+
382
1115
  function normalizeRuntimeSession(value: unknown): RuntimeSession | undefined {
383
1116
  const record = asRecord(value);
384
1117
  if (!record) {
@@ -459,19 +1192,55 @@ function normalizeDepBlocked(index: FeaturesIndex): Array<{
459
1192
  );
460
1193
  }
461
1194
 
462
- function normalizeFeatureSummary(
1195
+ function resolveActiveCollision(
463
1196
  featureId: string,
464
- frontMatter: Record<string, unknown>,
465
1197
  index: FeaturesIndex,
466
- repoRoot = AOP_ROOT,
467
- ): FeatureSummary {
468
- const status = typeof frontMatter.status === 'string' ? frontMatter.status : 'unknown';
469
- const blockedQueue = Array.isArray(index.blocked_queue)
470
- ? index.blocked_queue.map((entry) => entry.feature_id)
471
- : [];
472
- const phase = index.merged?.includes(featureId)
473
- ? 'merged'
474
- : index.blocked?.includes(featureId) || blockedQueue.includes(featureId)
1198
+ ): FeatureSummary['collision'] {
1199
+ const records = Array.isArray(index.collision_records) ? index.collision_records : [];
1200
+ // Find the LATEST collision record for this feature to check current state.
1201
+ // Older records may still be 'rejected' while the latest is already resolved.
1202
+ const featureRecords = records
1203
+ .filter((r) => r.feature_id === featureId)
1204
+ .sort((a, b) => (b.detected_at ?? '').localeCompare(a.detected_at ?? ''));
1205
+ const latest = featureRecords[0];
1206
+ if (!latest) {
1207
+ return null;
1208
+ }
1209
+ // Only report a collision if the most recent record is still unresolved.
1210
+ if (latest.resolution !== 'rejected' && latest.resolution !== 'blocked') {
1211
+ return null;
1212
+ }
1213
+ const record = latest;
1214
+ const withIds = Array.isArray(record.with_feature_ids) ? record.with_feature_ids : [];
1215
+ const filePaths: string[] = [];
1216
+ if (record.collisions?.files) {
1217
+ for (const entry of record.collisions.files) {
1218
+ if (Array.isArray(entry.paths)) {
1219
+ filePaths.push(...entry.paths);
1220
+ }
1221
+ }
1222
+ }
1223
+ return {
1224
+ with_feature_ids: withIds,
1225
+ collision_fingerprint: record.collision_fingerprint ?? '',
1226
+ file_paths: filePaths,
1227
+ detected_at: record.detected_at ?? '',
1228
+ };
1229
+ }
1230
+
1231
+ function normalizeFeatureSummary(
1232
+ featureId: string,
1233
+ frontMatter: Record<string, unknown>,
1234
+ index: FeaturesIndex,
1235
+ repoRoot = AOP_ROOT,
1236
+ ): FeatureSummary {
1237
+ const status = typeof frontMatter.status === 'string' ? frontMatter.status : 'unknown';
1238
+ const blockedQueue = Array.isArray(index.blocked_queue)
1239
+ ? index.blocked_queue.map((entry) => entry.feature_id)
1240
+ : [];
1241
+ const phase = index.merged?.includes(featureId)
1242
+ ? 'merged'
1243
+ : index.blocked?.includes(featureId) || blockedQueue.includes(featureId)
475
1244
  ? 'blocked'
476
1245
  : (STATUS_TO_PHASE[status] ?? (index.active?.includes(featureId) ? 'planning' : 'unknown'));
477
1246
  const branchRaw =
@@ -484,6 +1253,10 @@ function normalizeFeatureSummary(
484
1253
  const gates =
485
1254
  rawGates && typeof rawGates === 'object'
486
1255
  ? {
1256
+ plan:
1257
+ typeof (rawGates as Record<string, unknown>).plan === 'string'
1258
+ ? ((rawGates as Record<string, unknown>).plan as string)
1259
+ : undefined,
487
1260
  fast:
488
1261
  typeof (rawGates as Record<string, unknown>).fast === 'string'
489
1262
  ? ((rawGates as Record<string, unknown>).fast as string)
@@ -526,6 +1299,13 @@ function normalizeFeatureSummary(
526
1299
  : typeof revisionOfRaw === 'string'
527
1300
  ? Number.parseInt(revisionOfRaw, 10)
528
1301
  : null;
1302
+ const humanInput =
1303
+ frontMatter.human_input && typeof frontMatter.human_input === 'object'
1304
+ ? (frontMatter.human_input as Record<string, unknown>)
1305
+ : null;
1306
+ const intakeSummary = normalizeFeatureIntakeSummary(frontMatter);
1307
+ const mergeRepairSummary = normalizeMergeRepairSummary(frontMatter);
1308
+ const recovery = normalizeFeatureRecoverySummary(frontMatter);
529
1309
  return {
530
1310
  id: featureId,
531
1311
  feature_id: featureId,
@@ -551,13 +1331,35 @@ function normalizeFeatureSummary(
551
1331
  : path.join(repoRoot, '.worktrees', featureId),
552
1332
  last_updated:
553
1333
  typeof frontMatter.last_updated === 'string' ? frontMatter.last_updated : undefined,
1334
+ phase_entered_at:
1335
+ typeof frontMatter.phase_entered_at === 'string' ? frontMatter.phase_entered_at : undefined,
554
1336
  status_reason:
555
1337
  typeof frontMatter.status_reason === 'string' ? frontMatter.status_reason : undefined,
1338
+ recovery,
556
1339
  activity_state: inferActivityState(frontMatter, phase),
1340
+ open_question_id:
1341
+ typeof humanInput?.open_question_id === 'string' ? humanInput.open_question_id : null,
1342
+ open_question_count:
1343
+ typeof humanInput?.open_question_count === 'number'
1344
+ ? humanInput.open_question_count
1345
+ : undefined,
1346
+ awaiting_since:
1347
+ typeof humanInput?.awaiting_since === 'string' ? humanInput.awaiting_since : null,
1348
+ requested_by_role:
1349
+ typeof humanInput?.requested_by_role === 'string' ? humanInput.requested_by_role : null,
1350
+ resume_status: typeof humanInput?.resume_status === 'string' ? humanInput.resume_status : null,
1351
+ intake_status: intakeSummary?.status,
1352
+ bootstrap_manifest_version: intakeSummary?.bootstrap_manifest_version ?? null,
1353
+ verified_manifest_version: intakeSummary?.verified_manifest_version ?? null,
1354
+ open_ambiguity_count: intakeSummary?.open_ambiguity_count ?? 0,
1355
+ last_verified_at: intakeSummary?.last_verified_at ?? null,
1356
+ promotion_basis: intakeSummary?.promotion_basis ?? null,
1357
+ merge_repair: mergeRepairSummary,
557
1358
  activity_last_event_at:
558
1359
  typeof frontMatter.activity_last_event_at === 'string'
559
1360
  ? frontMatter.activity_last_event_at
560
1361
  : undefined,
1362
+ last_meaningful_change_at: lastMeaningfulChange(frontMatter),
561
1363
  activity_detected_via:
562
1364
  typeof frontMatter.activity_detected_via === 'string'
563
1365
  ? (frontMatter.activity_detected_via as FeatureSummary['activity_detected_via'])
@@ -566,6 +1368,7 @@ function normalizeFeatureSummary(
566
1368
  frontMatter.pr && typeof frontMatter.pr === 'object'
567
1369
  ? (frontMatter.pr as FeatureSummary['pr'])
568
1370
  : null,
1371
+ collision: resolveActiveCollision(featureId, index),
569
1372
  };
570
1373
  }
571
1374
 
@@ -646,6 +1449,9 @@ export async function readFeaturesIndex(repoRoot = AOP_ROOT): Promise<FeaturesIn
646
1449
  if (Array.isArray(parsed.dep_blocked)) {
647
1450
  normalized.dep_blocked = parsed.dep_blocked as FeaturesIndex['dep_blocked'];
648
1451
  }
1452
+ if (Array.isArray(parsed.collision_records)) {
1453
+ normalized.collision_records = parsed.collision_records as FeaturesIndex['collision_records'];
1454
+ }
649
1455
  const runtimeSessions = normalizeRuntimeSession(parsed.runtime_sessions);
650
1456
  if (runtimeSessions) {
651
1457
  normalized.runtime_sessions = runtimeSessions;
@@ -708,6 +1514,14 @@ export async function readFeaturePlan(
708
1514
  }
709
1515
  }
710
1516
 
1517
+ export async function readFeatureIntakeReview(
1518
+ featureId: string,
1519
+ repoRoot = AOP_ROOT,
1520
+ ): Promise<FeatureIntakeReviewSummary | null> {
1521
+ const reviewPath = path.join(getFeatureRoot(featureId, repoRoot), 'intake.review.json');
1522
+ return normalizeIntakeReviewSummary(await readJsonFile<Record<string, unknown>>(reviewPath));
1523
+ }
1524
+
711
1525
  export async function readFeatureCost(
712
1526
  featureId: string,
713
1527
  repoRoot = AOP_ROOT,
@@ -1091,7 +1905,7 @@ function parseStateLogBody(body: string): FeatureLogEntry[] {
1091
1905
 
1092
1906
  const entries: FeatureLogEntry[] = [];
1093
1907
  for (const line of lines) {
1094
- const parsed = line.match(/^\[(.+?)\]\s*(.+)$/);
1908
+ const parsed = line.match(/^\[(.+?)]\s*(.+)$/);
1095
1909
  if (parsed) {
1096
1910
  entries.push({
1097
1911
  timestamp: parsed[1],
@@ -1128,7 +1942,7 @@ function parseDecisionEntries(raw: string): AgentLogEntry[] {
1128
1942
  if (trimmed.length === 0 || trimmed.startsWith('#')) {
1129
1943
  continue;
1130
1944
  }
1131
- const match = trimmed.match(/^-?\s*([0-9TZ:.-]{10,})\s+\[([^\]]+)\]\s+([\s\S]+)$/);
1945
+ const match = trimmed.match(/^-?\s*([0-9TZ:.-]{10,})\s+\[([^\]]+)]\s+([\s\S]+)$/);
1132
1946
  if (match) {
1133
1947
  const timestampRaw = match[1];
1134
1948
  const actor = match[2].trim();
@@ -1217,7 +2031,9 @@ export async function readFeatureLog(featureId: string, repoRoot = AOP_ROOT): Pr
1217
2031
  }
1218
2032
 
1219
2033
  function normalizeRawLogRole(value: unknown): RawLogFileMeta['role'] | null {
1220
- return value === 'planner' || value === 'builder' || value === 'qa' ? value : null;
2034
+ return value === 'planner' || value === 'builder' || value === 'qa' || value === 'reconciler'
2035
+ ? value
2036
+ : null;
1221
2037
  }
1222
2038
 
1223
2039
  export async function listRawLogFiles(
@@ -1282,6 +2098,65 @@ export async function readRawLogFile(
1282
2098
  }
1283
2099
  }
1284
2100
 
2101
+ function normalizeLiveAgentOutputStatus(value: unknown): LiveAgentOutputSnapshot['status'] {
2102
+ return value === 'running' || value === 'completed' || value === 'failed' ? value : 'idle';
2103
+ }
2104
+
2105
+ function normalizeExecutionMode(value: unknown): LiveAgentOutputSnapshot['execution_mode'] | null {
2106
+ return value === 'interactive' || value === 'deterministic' ? value : null;
2107
+ }
2108
+
2109
+ export async function readLiveAgentOutputSnapshots(
2110
+ featureId: string,
2111
+ repoRoot = AOP_ROOT,
2112
+ options: LiveAgentOutputReadOptions = {},
2113
+ ): Promise<LiveAgentOutputSnapshot[]> {
2114
+ const liveDir = path.join(getFeatureRoot(featureId, repoRoot), 'logs', 'live');
2115
+ return await Promise.all(
2116
+ TRACKED_AGENT_ROLES.map(async (role) => {
2117
+ const metadataPath = path.join(liveDir, `${role}.json`);
2118
+ const contentPath = path.join(liveDir, `${role}.txt`);
2119
+ const [metadataRaw, content, contentStat] = await Promise.all([
2120
+ readFile(metadataPath, 'utf8').catch(() => null),
2121
+ readFile(contentPath, 'utf8').catch(() => ''),
2122
+ stat(contentPath).catch(() => null),
2123
+ ]);
2124
+
2125
+ let metadata: Record<string, unknown> = {};
2126
+ if (metadataRaw) {
2127
+ try {
2128
+ const parsed = JSON.parse(metadataRaw) as unknown;
2129
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
2130
+ metadata = parsed as Record<string, unknown>;
2131
+ }
2132
+ } catch {
2133
+ metadata = {};
2134
+ }
2135
+ }
2136
+
2137
+ return {
2138
+ role,
2139
+ session_id: typeof metadata.session_id === 'string' ? metadata.session_id : null,
2140
+ provider: typeof metadata.provider === 'string' ? metadata.provider : null,
2141
+ model: typeof metadata.model === 'string' ? metadata.model : null,
2142
+ execution_mode: normalizeExecutionMode(metadata.execution_mode),
2143
+ status: normalizeLiveAgentOutputStatus(metadata.status),
2144
+ started_at: typeof metadata.started_at === 'string' ? metadata.started_at : null,
2145
+ ended_at: typeof metadata.ended_at === 'string' ? metadata.ended_at : null,
2146
+ updated_at: contentStat?.mtime.toISOString() ?? null,
2147
+ size_bytes: contentStat?.size ?? 0,
2148
+ content: tailTextByLines(content, options.tailLines),
2149
+ } satisfies LiveAgentOutputSnapshot;
2150
+ }),
2151
+ );
2152
+ }
2153
+
2154
+ function summarizeLiveAgentOutputSnapshots(
2155
+ snapshots: LiveAgentOutputSnapshot[],
2156
+ ): LiveAgentOutputStatusSnapshot[] {
2157
+ return snapshots.map(({ content: _content, ...snapshot }) => snapshot);
2158
+ }
2159
+
1285
2160
  interface WorkerTimelineReadOptions {
1286
2161
  role?: string | null;
1287
2162
  valid?: 'all' | 'valid' | 'invalid';
@@ -1536,7 +2411,7 @@ export async function readInteractivePerformanceMetrics(
1536
2411
  const averageFilesChanged = checkpointCount > 0 ? filesChangedTotal / checkpointCount : null;
1537
2412
 
1538
2413
  return {
1539
- updated_at: typeof parsed.updated_at === 'string' ? parsed.updated_at : null,
2414
+ updated_at: parsed.updated_at,
1540
2415
  checkpoint_count: checkpointCount,
1541
2416
  valid_count: validCount,
1542
2417
  invalid_count: invalidCount,
@@ -1595,28 +2470,109 @@ async function readFlakySuspects(repoRoot = AOP_ROOT): Promise<string[]> {
1595
2470
  return [];
1596
2471
  }
1597
2472
 
2473
+ const ORGANIZER_STATUS_FALLBACK: OrganizerStatusProjection = {
2474
+ available: false,
2475
+ generated_at: null,
2476
+ ready_set: [],
2477
+ deferred_set: [],
2478
+ blocked_set: [],
2479
+ };
2480
+
2481
+ export async function readOrganizerStatus(
2482
+ repoRoot = AOP_ROOT,
2483
+ instanceId?: string | null,
2484
+ ): Promise<OrganizerStatusProjection> {
2485
+ if (!instanceId) {
2486
+ return ORGANIZER_STATUS_FALLBACK;
2487
+ }
2488
+ const artifactPath = path.join(
2489
+ repoRoot,
2490
+ '.aop',
2491
+ 'runtime',
2492
+ instanceId,
2493
+ 'organizer-ordering.json',
2494
+ );
2495
+ const parsed = await readJsonFile<Record<string, unknown>>(artifactPath);
2496
+ if (!parsed) {
2497
+ return ORGANIZER_STATUS_FALLBACK;
2498
+ }
2499
+ const generatedAt = typeof parsed.generated_at === 'string' ? parsed.generated_at : null;
2500
+ const readySet = Array.isArray(parsed.ready_set)
2501
+ ? parsed.ready_set.filter((item): item is string => typeof item === 'string')
2502
+ : [];
2503
+ const deferredSet = Array.isArray(parsed.deferred_set)
2504
+ ? parsed.deferred_set.flatMap((item) => {
2505
+ if (
2506
+ item !== null &&
2507
+ typeof item === 'object' &&
2508
+ typeof (item as Record<string, unknown>).feature_id === 'string' &&
2509
+ Array.isArray((item as Record<string, unknown>).blocked_by) &&
2510
+ typeof (item as Record<string, unknown>).reason === 'string'
2511
+ ) {
2512
+ const entry = item as Record<string, unknown>;
2513
+ return [
2514
+ {
2515
+ feature_id: entry.feature_id as string,
2516
+ blocked_by: (entry.blocked_by as unknown[]).filter(
2517
+ (b): b is string => typeof b === 'string',
2518
+ ),
2519
+ reason: entry.reason as string,
2520
+ },
2521
+ ];
2522
+ }
2523
+ return [];
2524
+ })
2525
+ : [];
2526
+ const blockedSet = Array.isArray(parsed.blocked_set)
2527
+ ? parsed.blocked_set.flatMap((item) => {
2528
+ if (
2529
+ item !== null &&
2530
+ typeof item === 'object' &&
2531
+ typeof (item as Record<string, unknown>).feature_id === 'string' &&
2532
+ typeof (item as Record<string, unknown>).reason === 'string'
2533
+ ) {
2534
+ const entry = item as Record<string, unknown>;
2535
+ return [{ feature_id: entry.feature_id as string, reason: entry.reason as string }];
2536
+ }
2537
+ return [];
2538
+ })
2539
+ : [];
2540
+ return {
2541
+ available: true,
2542
+ generated_at: generatedAt,
2543
+ ready_set: readySet,
2544
+ deferred_set: deferredSet,
2545
+ blocked_set: blockedSet,
2546
+ };
2547
+ }
2548
+
1598
2549
  export async function readDashboardStatus(repoRoot = AOP_ROOT): Promise<DashboardStatusPayload> {
1599
2550
  const index = await readFeaturesIndex(repoRoot);
2551
+ const budgetConfig = parseBudgetConfig(await readPolicyYaml(repoRoot));
1600
2552
  const allFeatureIds = new Set<string>([
1601
2553
  ...(index.active ?? []),
1602
2554
  ...(index.blocked ?? []),
1603
2555
  ...(index.merged ?? []),
1604
2556
  ...(index.blocked_queue ?? []).map((entry) => entry.feature_id),
1605
2557
  ]);
2558
+ const dependencyBlockedIds = new Set(normalizeDepBlocked(index).map((entry) => entry.feature_id));
1606
2559
 
1607
2560
  const features: FeatureSummary[] = [];
2561
+ let totalTokens = 0;
1608
2562
  let totalCostUsd = 0;
2563
+ const nowMs = Date.now();
1609
2564
  for (const featureId of [...allFeatureIds].sort((a, b) => a.localeCompare(b))) {
2565
+ let feature: FeatureSummary;
1610
2566
  const statePath = path.join(getFeatureRoot(featureId, repoRoot), 'state.md');
1611
2567
  try {
1612
2568
  const raw = await readFile(statePath, 'utf-8');
1613
2569
  const { frontMatter } = parseFrontMatter(raw);
1614
- features.push(normalizeFeatureSummary(featureId, frontMatter, index, repoRoot));
2570
+ feature = normalizeFeatureSummary(featureId, frontMatter, index, repoRoot);
1615
2571
  } catch {
1616
2572
  const blockedQueue = Array.isArray(index.blocked_queue)
1617
2573
  ? index.blocked_queue.map((entry) => entry.feature_id)
1618
2574
  : [];
1619
- features.push({
2575
+ feature = {
1620
2576
  id: featureId,
1621
2577
  feature_id: featureId,
1622
2578
  status: index.merged?.includes(featureId)
@@ -1634,30 +2590,1412 @@ export async function readDashboardStatus(repoRoot = AOP_ROOT): Promise<Dashboar
1634
2590
  branch: null,
1635
2591
  worktree_path: null,
1636
2592
  pr: null,
1637
- });
2593
+ };
1638
2594
  }
1639
2595
 
1640
2596
  const cost = await readFeatureCost(featureId, repoRoot);
2597
+ const enriched = enrichFeatureSummary(feature, {
2598
+ cost,
2599
+ budgetConfig,
2600
+ dependencyBlocked: dependencyBlockedIds.has(featureId),
2601
+ nowMs,
2602
+ });
2603
+ features.push(enriched);
1641
2604
  if (cost) {
2605
+ totalTokens += cost.tokens_used;
1642
2606
  totalCostUsd += cost.estimated_cost_usd;
1643
2607
  }
1644
2608
  }
1645
2609
 
1646
2610
  const depBlocked = normalizeDepBlocked(index);
2611
+ const pendingQuestions = await readPendingQuestions(repoRoot);
1647
2612
  return {
1648
2613
  index,
1649
2614
  features,
2615
+ pending_questions: {
2616
+ count: pendingQuestions.length,
2617
+ items: pendingQuestions.slice(0, 12),
2618
+ },
1650
2619
  runtime: index.runtime_sessions,
1651
2620
  lock_map: normalizeLockMap(index),
1652
2621
  dep_blocked: depBlocked,
1653
2622
  flaky_suspects: await readFlakySuspects(repoRoot),
1654
2623
  metrics: {
1655
- total_cost_today_usd: Number(totalCostUsd.toFixed(4)),
2624
+ total_tokens_today: totalTokens,
2625
+ total_cost_today_usd:
2626
+ budgetConfig.token_cost_conversion != null ? Number(totalCostUsd.toFixed(4)) : null,
1656
2627
  merge_histogram_14d: buildMergeHistogram14d(features),
1657
2628
  },
1658
2629
  };
1659
2630
  }
1660
2631
 
2632
+ export async function readFeatureTriage(
2633
+ featureId: string,
2634
+ repoRoot = AOP_ROOT,
2635
+ ): Promise<FeatureTriageDetail | null> {
2636
+ const index = await readFeaturesIndex(repoRoot);
2637
+ const feature = await readFeatureState(featureId, repoRoot);
2638
+ if (!feature) {
2639
+ return null;
2640
+ }
2641
+
2642
+ const budgetConfig = parseBudgetConfig(await readPolicyYaml(repoRoot));
2643
+ const [cost, reviewBrief, intakeReview] = await Promise.all([
2644
+ readFeatureCost(featureId, repoRoot),
2645
+ readFeatureReviewBrief(featureId, repoRoot),
2646
+ readFeatureIntakeReview(featureId, repoRoot),
2647
+ ]);
2648
+ const dependencySummary =
2649
+ normalizeDepBlocked(index).find((entry) => entry.feature_id === featureId)
2650
+ ?.depends_on_unresolved ?? [];
2651
+ const enriched = enrichFeatureSummary(feature, {
2652
+ cost,
2653
+ budgetConfig,
2654
+ dependencyBlocked: dependencySummary.length > 0,
2655
+ });
2656
+ const questions = await readFeatureQuestions(featureId, repoRoot, enriched);
2657
+
2658
+ return {
2659
+ feature: enriched,
2660
+ why_attention: enriched.attention_reasons ?? [],
2661
+ blocker_summary: summarizeBlocker(enriched, dependencySummary),
2662
+ open_questions: selectOperatorReadyOpenQuestions(enriched, questions),
2663
+ dependency_summary: dependencySummary,
2664
+ usage: {
2665
+ tokens_used: cost?.tokens_used ?? null,
2666
+ budget_limit_tokens: budgetConfig.per_feature_limit_tokens,
2667
+ derived_cost_usd:
2668
+ budgetConfig.token_cost_conversion != null
2669
+ ? deriveCostFromTokens(cost?.tokens_used ?? null, budgetConfig.token_cost_conversion)
2670
+ : null,
2671
+ budget_state: enriched.budget_state,
2672
+ },
2673
+ intake:
2674
+ enriched.intake_status != null
2675
+ ? {
2676
+ summary: {
2677
+ status: enriched.intake_status,
2678
+ bootstrap_manifest_version: enriched.bootstrap_manifest_version ?? null,
2679
+ verified_manifest_version: enriched.verified_manifest_version ?? null,
2680
+ open_ambiguity_count: enriched.open_ambiguity_count ?? 0,
2681
+ last_verified_at: enriched.last_verified_at ?? null,
2682
+ promotion_basis: enriched.promotion_basis ?? null,
2683
+ },
2684
+ review: intakeReview,
2685
+ }
2686
+ : null,
2687
+ review_snapshot:
2688
+ reviewBrief?.generated_at || enriched.pr
2689
+ ? {
2690
+ pr_url: enriched.pr?.url ?? null,
2691
+ ci_status: enriched.pr?.ci_status ?? null,
2692
+ review_decision: enriched.pr?.review_decision ?? null,
2693
+ merge_ready: enriched.pr?.merge_ready ?? null,
2694
+ }
2695
+ : undefined,
2696
+ };
2697
+ }
2698
+
2699
+ function parsePlanFiles(plan: Record<string, unknown> | null): string[] {
2700
+ if (!plan) {
2701
+ return [];
2702
+ }
2703
+ if (Array.isArray(plan.files)) {
2704
+ return uniqueSorted(plan.files.filter((item): item is string => typeof item === 'string'));
2705
+ }
2706
+ if (!plan.files || typeof plan.files !== 'object') {
2707
+ return [];
2708
+ }
2709
+ const files = plan.files as Record<string, unknown>;
2710
+ return uniqueSorted([
2711
+ ...(Array.isArray(files.create)
2712
+ ? files.create.filter((item): item is string => typeof item === 'string')
2713
+ : []),
2714
+ ...(Array.isArray(files.modify)
2715
+ ? files.modify.filter((item): item is string => typeof item === 'string')
2716
+ : []),
2717
+ ...(Array.isArray(files.delete)
2718
+ ? files.delete.filter((item): item is string => typeof item === 'string')
2719
+ : []),
2720
+ ]);
2721
+ }
2722
+
2723
+ export async function readFeatureFiles(
2724
+ featureId: string,
2725
+ repoRoot = AOP_ROOT,
2726
+ ): Promise<FeatureFileIndex | null> {
2727
+ const feature = await readFeatureState(featureId, repoRoot);
2728
+ if (!feature) {
2729
+ return null;
2730
+ }
2731
+ const [plan, diff, checkpoints] = await Promise.all([
2732
+ readFeaturePlan(featureId, repoRoot),
2733
+ readFeatureDiff(featureId, repoRoot),
2734
+ readFeatureCheckpoints(featureId, repoRoot),
2735
+ ]);
2736
+ const planFiles = parsePlanFiles(plan);
2737
+ const diffFiles = uniqueSorted(parseUnifiedDiff(diff).map((file) => file.filePath));
2738
+ const checkpointFiles = uniqueSorted(
2739
+ (checkpoints?.checkpoints ?? []).flatMap((checkpoint) => checkpoint.files_changed),
2740
+ );
2741
+
2742
+ return {
2743
+ feature_id: featureId,
2744
+ plan_files: planFiles,
2745
+ diff_files: diffFiles,
2746
+ checkpoint_files: checkpointFiles,
2747
+ all_files: uniqueSorted([...planFiles, ...diffFiles, ...checkpointFiles]),
2748
+ };
2749
+ }
2750
+
2751
+ async function buildFeatureCollisionRadar(
2752
+ featureId: string,
2753
+ repoRoot = AOP_ROOT,
2754
+ ): Promise<CollisionRadarSnapshot | null> {
2755
+ const [currentFiles, index] = await Promise.all([
2756
+ readFeatureFiles(featureId, repoRoot),
2757
+ readFeaturesIndex(repoRoot),
2758
+ ]);
2759
+
2760
+ if (!currentFiles) {
2761
+ return null;
2762
+ }
2763
+
2764
+ const competingFeatures = index.active.filter((otherFeatureId) => otherFeatureId !== featureId);
2765
+
2766
+ const competingFileMaps = await Promise.all(
2767
+ competingFeatures.map(async (otherFeatureId) => {
2768
+ const files = await readFeatureFiles(otherFeatureId, repoRoot);
2769
+ return [otherFeatureId, files?.all_files ?? []] as const;
2770
+ }),
2771
+ );
2772
+
2773
+ return buildCollisionRadar(currentFiles.all_files, Object.fromEntries(competingFileMaps));
2774
+ }
2775
+
2776
+ export async function readGateHistory(
2777
+ featureId: string,
2778
+ repoRoot = AOP_ROOT,
2779
+ ): Promise<GateHistoryEntry[]> {
2780
+ const evidenceDir = path.join(getFeatureRoot(featureId, repoRoot), 'evidence');
2781
+ try {
2782
+ const files = (await readdir(evidenceDir))
2783
+ .filter(
2784
+ (name) =>
2785
+ /^gate-(fast|full|merge)-/.test(name) || /^latest-(fast|full|merge)\.json$/.test(name),
2786
+ )
2787
+ .sort((left, right) => left.localeCompare(right));
2788
+ const history: GateHistoryEntry[] = [];
2789
+ for (const file of files) {
2790
+ const parsed = await readJsonFile<Record<string, unknown>>(path.join(evidenceDir, file));
2791
+ if (!parsed) {
2792
+ continue;
2793
+ }
2794
+ const steps = Array.isArray(parsed.step_results) ? parsed.step_results : [];
2795
+ const stepTimestamps = steps
2796
+ .map((step) => asRecord(step))
2797
+ .flatMap((step) => [
2798
+ typeof step?.started_at === 'string' ? Date.parse(step.started_at) : Number.NaN,
2799
+ typeof step?.finished_at === 'string' ? Date.parse(step.finished_at) : Number.NaN,
2800
+ ])
2801
+ .filter((value): value is number => Number.isFinite(value));
2802
+ const durationMs =
2803
+ stepTimestamps.length >= 2
2804
+ ? Math.max(...stepTimestamps) - Math.min(...stepTimestamps)
2805
+ : null;
2806
+ history.push({
2807
+ id: `${featureId}:${file}`,
2808
+ file,
2809
+ mode: typeof parsed.mode === 'string' ? parsed.mode : 'unknown',
2810
+ profile: typeof parsed.profile === 'string' ? parsed.profile : null,
2811
+ overall: typeof parsed.overall === 'string' ? parsed.overall : 'unknown',
2812
+ finished_at: typeof parsed.finished_at === 'string' ? parsed.finished_at : null,
2813
+ step_count: steps.length,
2814
+ failing_step_count: steps.filter((step) => {
2815
+ const record = asRecord(step);
2816
+ return typeof record?.exit_code === 'number' && record.exit_code !== 0;
2817
+ }).length,
2818
+ duration_ms: durationMs,
2819
+ steps: steps.map((step) => {
2820
+ const record = asRecord(step);
2821
+ const outputTail = Array.isArray(record?.output_tail)
2822
+ ? record.output_tail.filter((item): item is string => typeof item === 'string')
2823
+ : [];
2824
+ return {
2825
+ name: typeof record?.name === 'string' ? record.name : 'unknown',
2826
+ status:
2827
+ record?.status === 'pass' || record?.status === 'fail' ? record.status : 'unknown',
2828
+ output_tail: outputTail,
2829
+ };
2830
+ }),
2831
+ });
2832
+ }
2833
+ return history.sort((left, right) => {
2834
+ const leftMs = left.finished_at ? Date.parse(left.finished_at) : Number.NaN;
2835
+ const rightMs = right.finished_at ? Date.parse(right.finished_at) : Number.NaN;
2836
+ const safeLeft = Number.isNaN(leftMs) ? 0 : leftMs;
2837
+ const safeRight = Number.isNaN(rightMs) ? 0 : rightMs;
2838
+ return safeRight - safeLeft;
2839
+ });
2840
+ } catch {
2841
+ return [];
2842
+ }
2843
+ }
2844
+
2845
+ export async function readFeatureGenealogy(
2846
+ featureId: string,
2847
+ repoRoot = AOP_ROOT,
2848
+ ): Promise<FeatureGenealogy | null> {
2849
+ const feature = await readFeatureState(featureId, repoRoot);
2850
+ if (!feature) {
2851
+ return null;
2852
+ }
2853
+ const [plan, checkpointState] = await Promise.all([
2854
+ readFeaturePlan(featureId, repoRoot),
2855
+ readFeatureCheckpoints(featureId, repoRoot),
2856
+ ]);
2857
+ const statePath = path.join(getFeatureRoot(featureId, repoRoot), 'state.md');
2858
+ const runNodes: FeatureGenealogyNode[] = await (async () => {
2859
+ try {
2860
+ const raw = await readFile(statePath, 'utf8');
2861
+ const frontMatter = parseFrontMatter(raw).frontMatter;
2862
+ const runHistory = Array.isArray(frontMatter.run_history) ? frontMatter.run_history : [];
2863
+ return runHistory
2864
+ .map<FeatureGenealogyNode | null>((entry, index) => {
2865
+ const record = asRecord(entry);
2866
+ if (!record || typeof record.run_id !== 'string') {
2867
+ return null;
2868
+ }
2869
+ const endedAt = typeof record.ended_at === 'string' ? record.ended_at : null;
2870
+ const startedAt = typeof record.started_at === 'string' ? record.started_at : null;
2871
+ return {
2872
+ id: `${featureId}:run:${index}`,
2873
+ label: record.run_id,
2874
+ kind: 'run',
2875
+ timestamp: endedAt ?? startedAt,
2876
+ status: typeof record.status === 'string' ? record.status : null,
2877
+ } satisfies FeatureGenealogyNode;
2878
+ })
2879
+ .filter((entry): entry is FeatureGenealogyNode => entry != null);
2880
+ } catch {
2881
+ return [];
2882
+ }
2883
+ })();
2884
+
2885
+ const planVersion =
2886
+ typeof plan?.plan_version === 'number'
2887
+ ? plan.plan_version
2888
+ : typeof plan?.plan_version === 'string'
2889
+ ? Number.parseInt(plan.plan_version, 10)
2890
+ : (feature.plan_version ?? null);
2891
+ const revisionOf =
2892
+ typeof plan?.revision_of === 'number'
2893
+ ? plan.revision_of
2894
+ : typeof plan?.revision_of === 'string'
2895
+ ? Number.parseInt(plan.revision_of, 10)
2896
+ : (feature.revision_of ?? null);
2897
+ const planNodes: FeatureGenealogyNode[] = [];
2898
+ if (revisionOf != null && Number.isFinite(revisionOf)) {
2899
+ for (let version = 1; version <= revisionOf; version += 1) {
2900
+ planNodes.push({
2901
+ id: `${featureId}:plan:${version}`,
2902
+ label: `Plan v${version}`,
2903
+ kind: 'plan_revision',
2904
+ timestamp: null,
2905
+ status: version === revisionOf ? 'ancestor' : null,
2906
+ });
2907
+ }
2908
+ }
2909
+ if (planVersion != null && Number.isFinite(planVersion)) {
2910
+ planNodes.push({
2911
+ id: `${featureId}:plan:${planVersion}`,
2912
+ label: `Plan v${planVersion}`,
2913
+ kind: 'plan_revision',
2914
+ timestamp: null,
2915
+ status: feature.status,
2916
+ note: feature.revision_reason ?? null,
2917
+ });
2918
+ }
2919
+
2920
+ const checkpointNodes = (checkpointState?.checkpoints ?? []).slice(0, 10).map((checkpoint) => ({
2921
+ id: checkpoint.checkpoint_id,
2922
+ label: checkpoint.checkpoint_id,
2923
+ kind: 'checkpoint',
2924
+ timestamp: checkpoint.timestamp,
2925
+ status: checkpoint.validation_status,
2926
+ note: checkpoint.severity ?? null,
2927
+ })) satisfies FeatureGenealogyNode[];
2928
+
2929
+ return {
2930
+ feature_id: featureId,
2931
+ nodes: [
2932
+ {
2933
+ id: featureId,
2934
+ label: featureId,
2935
+ kind: 'feature',
2936
+ timestamp: feature.last_updated ?? null,
2937
+ status: feature.status,
2938
+ },
2939
+ ...planNodes,
2940
+ ...checkpointNodes,
2941
+ ...runNodes,
2942
+ ],
2943
+ };
2944
+ }
2945
+
2946
+ function summarizeRolePerformance(
2947
+ entries: WorkerEventEntry[],
2948
+ role: string,
2949
+ ): RolePerformanceSummary {
2950
+ const matching = entries.filter((entry) => entry.role === role);
2951
+ const elapsedValues = matching
2952
+ .map((entry) => entry.elapsed_ms)
2953
+ .filter((value): value is number => typeof value === 'number' && Number.isFinite(value));
2954
+ const requestValues = matching
2955
+ .map((entry) => entry.request_count)
2956
+ .filter((value): value is number => Number.isFinite(value));
2957
+ const providers = uniqueSorted(matching.map((entry) => entry.provider));
2958
+ const models = uniqueSorted(matching.map((entry) => entry.model));
2959
+ const lastEventAt = matching.length > 0 ? (matching[0]?.ts ?? null) : null;
2960
+ return {
2961
+ role,
2962
+ total_events: matching.length,
2963
+ valid_events: matching.filter((entry) => entry.valid).length,
2964
+ invalid_events: matching.filter((entry) => !entry.valid).length,
2965
+ avg_elapsed_ms: truncateNumber(averageNumber(elapsedValues), 1),
2966
+ avg_requests: truncateNumber(averageNumber(requestValues), 2),
2967
+ last_event_at: lastEventAt,
2968
+ providers,
2969
+ models,
2970
+ };
2971
+ }
2972
+
2973
+ function sumRoleMetric(
2974
+ entries: WorkerEventEntry[],
2975
+ selector: (entry: WorkerEventEntry) => number | undefined,
2976
+ ): number {
2977
+ return entries.reduce((sum, entry) => {
2978
+ const value = selector(entry);
2979
+ return typeof value === 'number' && Number.isFinite(value) ? sum + value : sum;
2980
+ }, 0);
2981
+ }
2982
+
2983
+ function selectPrimaryProviderModel(entries: WorkerEventEntry[]): {
2984
+ provider: string | null;
2985
+ model: string | null;
2986
+ } {
2987
+ const aggregate = new Map<
2988
+ string,
2989
+ { provider: string; model: string; count: number; latest_ts: number }
2990
+ >();
2991
+
2992
+ for (const entry of entries) {
2993
+ const key = `${entry.provider}:${entry.model}`;
2994
+ const existing = aggregate.get(key) ?? {
2995
+ provider: entry.provider,
2996
+ model: entry.model,
2997
+ count: 0,
2998
+ latest_ts: Number.NaN,
2999
+ };
3000
+ existing.count += 1;
3001
+ const timestamp = Date.parse(entry.ts);
3002
+ if (
3003
+ !Number.isNaN(timestamp) &&
3004
+ (Number.isNaN(existing.latest_ts) || timestamp > existing.latest_ts)
3005
+ ) {
3006
+ existing.latest_ts = timestamp;
3007
+ }
3008
+ aggregate.set(key, existing);
3009
+ }
3010
+
3011
+ const selected = Array.from(aggregate.values()).sort((left, right) => {
3012
+ if (right.count !== left.count) {
3013
+ return right.count - left.count;
3014
+ }
3015
+ const leftTs = Number.isNaN(left.latest_ts) ? Number.NEGATIVE_INFINITY : left.latest_ts;
3016
+ const rightTs = Number.isNaN(right.latest_ts) ? Number.NEGATIVE_INFINITY : right.latest_ts;
3017
+ if (rightTs !== leftTs) {
3018
+ return rightTs - leftTs;
3019
+ }
3020
+ return `${left.provider}/${left.model}`.localeCompare(`${right.provider}/${right.model}`);
3021
+ })[0];
3022
+
3023
+ return selected
3024
+ ? { provider: selected.provider, model: selected.model }
3025
+ : { provider: null, model: null };
3026
+ }
3027
+
3028
+ function createPerformanceMetric(
3029
+ actual: number | null,
3030
+ baselineAverage: number | null,
3031
+ averageDigits = 1,
3032
+ ): AgentRolePerformanceMetric {
3033
+ const safeAverage = baselineAverage != null && baselineAverage > 0 ? baselineAverage : null;
3034
+ return {
3035
+ actual,
3036
+ average: truncateNumber(baselineAverage, averageDigits),
3037
+ ratio: actual != null && safeAverage != null ? truncateNumber(actual / safeAverage, 2) : null,
3038
+ };
3039
+ }
3040
+
3041
+ function summarizePerformanceCard(
3042
+ metrics: AgentRolePerformanceCardData['metrics'],
3043
+ sampleSize: number,
3044
+ ): string {
3045
+ if (sampleSize === 0) {
3046
+ return 'Historical baseline unavailable for this provider and model.';
3047
+ }
3048
+
3049
+ const comparableRatios = [
3050
+ metrics.elapsed_ms.ratio,
3051
+ metrics.request_count.ratio,
3052
+ metrics.patch_count.ratio,
3053
+ metrics.plan_submission_count.ratio,
3054
+ ].filter((value): value is number => typeof value === 'number' && Number.isFinite(value));
3055
+
3056
+ if (comparableRatios.length === 0) {
3057
+ return 'Historical baseline unavailable for this provider and model.';
3058
+ }
3059
+
3060
+ if (
3061
+ typeof metrics.elapsed_ms.ratio === 'number' &&
3062
+ metrics.elapsed_ms.ratio > 1.5 &&
3063
+ typeof metrics.request_count.ratio === 'number' &&
3064
+ metrics.request_count.ratio > 1.5
3065
+ ) {
3066
+ return 'Running slower and making more requests than average.';
3067
+ }
3068
+
3069
+ if (comparableRatios.some((value) => value > 2.5)) {
3070
+ return 'Well above historical average.';
3071
+ }
3072
+
3073
+ if (comparableRatios.some((value) => value > 1.5)) {
3074
+ return 'Above historical average.';
3075
+ }
3076
+
3077
+ if (comparableRatios.every((value) => value >= 0.75 && value <= 1.25)) {
3078
+ return 'On track.';
3079
+ }
3080
+
3081
+ if (comparableRatios.some((value) => value < 0.75)) {
3082
+ return 'Ahead of historical average.';
3083
+ }
3084
+
3085
+ return 'Mixed versus historical average.';
3086
+ }
3087
+
3088
+ function computeHistoricalRoleMetricAverages(
3089
+ entries: WorkerEventEntry[],
3090
+ currentFeatureId: string,
3091
+ role: TrackedAgentRole,
3092
+ provider: string | null,
3093
+ model: string | null,
3094
+ ): {
3095
+ sample_size: number;
3096
+ elapsed_ms: number | null;
3097
+ request_count: number | null;
3098
+ patch_count: number | null;
3099
+ plan_submission_count: number | null;
3100
+ } {
3101
+ if (!provider || !model) {
3102
+ return {
3103
+ sample_size: 0,
3104
+ elapsed_ms: null,
3105
+ request_count: null,
3106
+ patch_count: null,
3107
+ plan_submission_count: null,
3108
+ };
3109
+ }
3110
+
3111
+ const perFeature = new Map<
3112
+ string,
3113
+ {
3114
+ elapsed_ms: number;
3115
+ request_count: number;
3116
+ patch_count: number;
3117
+ plan_submission_count: number;
3118
+ }
3119
+ >();
3120
+
3121
+ for (const entry of entries) {
3122
+ if (
3123
+ entry.role !== role ||
3124
+ entry.provider !== provider ||
3125
+ entry.model !== model ||
3126
+ !entry.feature_id ||
3127
+ entry.feature_id === currentFeatureId
3128
+ ) {
3129
+ continue;
3130
+ }
3131
+ const existing = perFeature.get(entry.feature_id) ?? {
3132
+ elapsed_ms: 0,
3133
+ request_count: 0,
3134
+ patch_count: 0,
3135
+ plan_submission_count: 0,
3136
+ };
3137
+ existing.elapsed_ms +=
3138
+ typeof entry.elapsed_ms === 'number' && Number.isFinite(entry.elapsed_ms)
3139
+ ? entry.elapsed_ms
3140
+ : 0;
3141
+ existing.request_count += Number.isFinite(entry.request_count) ? entry.request_count : 0;
3142
+ existing.patch_count += Number.isFinite(entry.patch_count) ? entry.patch_count : 0;
3143
+ existing.plan_submission_count += Number.isFinite(entry.plan_submission_count)
3144
+ ? entry.plan_submission_count
3145
+ : 0;
3146
+ perFeature.set(entry.feature_id, existing);
3147
+ }
3148
+
3149
+ const totals = Array.from(perFeature.values());
3150
+ return {
3151
+ sample_size: totals.length,
3152
+ elapsed_ms: averageNumber(totals.map((item) => item.elapsed_ms)),
3153
+ request_count: averageNumber(totals.map((item) => item.request_count)),
3154
+ patch_count: averageNumber(totals.map((item) => item.patch_count)),
3155
+ plan_submission_count: averageNumber(totals.map((item) => item.plan_submission_count)),
3156
+ };
3157
+ }
3158
+
3159
+ async function buildFeatureRolePerformanceCards(
3160
+ feature: FeatureSummary,
3161
+ repoRoot = AOP_ROOT,
3162
+ ): Promise<AgentRolePerformanceCardData[]> {
3163
+ const [featureEvents, allEvents] = await Promise.all([
3164
+ readWorkerTimelineEntries(feature.feature_id, repoRoot, { limit: 200 }),
3165
+ readAllWorkerEvents(repoRoot),
3166
+ ]);
3167
+
3168
+ return TRACKED_AGENT_ROLES.flatMap((role) => {
3169
+ const roleStatusMap = feature.role_status as unknown as Record<string, string> | undefined;
3170
+ const roleStatus = roleStatusMap?.[role] ?? 'unknown';
3171
+ if (roleStatus === 'ready' || roleStatus === 'unknown') {
3172
+ return [];
3173
+ }
3174
+
3175
+ const currentEntries = featureEvents.filter((entry) => entry.role === role);
3176
+ const currentProviderModel = selectPrimaryProviderModel(currentEntries);
3177
+ const historical = computeHistoricalRoleMetricAverages(
3178
+ allEvents,
3179
+ feature.feature_id,
3180
+ role,
3181
+ currentProviderModel.provider,
3182
+ currentProviderModel.model,
3183
+ );
3184
+ const metrics = {
3185
+ elapsed_ms: createPerformanceMetric(
3186
+ currentEntries.length > 0 ? sumRoleMetric(currentEntries, (entry) => entry.elapsed_ms) : 0,
3187
+ historical.elapsed_ms,
3188
+ 1,
3189
+ ),
3190
+ request_count: createPerformanceMetric(
3191
+ currentEntries.length > 0
3192
+ ? sumRoleMetric(currentEntries, (entry) => entry.request_count)
3193
+ : 0,
3194
+ historical.request_count,
3195
+ 1,
3196
+ ),
3197
+ patch_count: createPerformanceMetric(
3198
+ currentEntries.length > 0 ? sumRoleMetric(currentEntries, (entry) => entry.patch_count) : 0,
3199
+ historical.patch_count,
3200
+ 1,
3201
+ ),
3202
+ plan_submission_count: createPerformanceMetric(
3203
+ currentEntries.length > 0
3204
+ ? sumRoleMetric(currentEntries, (entry) => entry.plan_submission_count)
3205
+ : 0,
3206
+ historical.plan_submission_count,
3207
+ 1,
3208
+ ),
3209
+ };
3210
+
3211
+ return [
3212
+ {
3213
+ role,
3214
+ role_status: roleStatus as 'ready' | 'running' | 'blocked' | 'done' | 'unknown',
3215
+ provider: currentProviderModel.provider,
3216
+ model: currentProviderModel.model,
3217
+ total_events: currentEntries.length,
3218
+ sample_size: historical.sample_size,
3219
+ summary:
3220
+ currentEntries.length === 0
3221
+ ? 'No worker-event telemetry has been recorded yet.'
3222
+ : summarizePerformanceCard(metrics, historical.sample_size),
3223
+ metrics,
3224
+ },
3225
+ ];
3226
+ });
3227
+ }
3228
+
3229
+ export async function readFeatureWorkerEventSummary(
3230
+ featureId: string,
3231
+ repoRoot = AOP_ROOT,
3232
+ options: WorkerTimelineReadOptions = {},
3233
+ ): Promise<WorkerEventSummary> {
3234
+ const entries = await readWorkerTimelineEntries(featureId, repoRoot, options);
3235
+ const roles = uniqueSorted(entries.map((entry) => entry.role));
3236
+ return {
3237
+ entries,
3238
+ by_role: roles.map((role) => summarizeRolePerformance(entries, role)),
3239
+ };
3240
+ }
3241
+
3242
+ export async function readMergeQueueSnapshot(
3243
+ featureId?: string,
3244
+ repoRoot = AOP_ROOT,
3245
+ ): Promise<MergeQueueSnapshot> {
3246
+ const payload = await readDashboardStatus(repoRoot);
3247
+ const dependencyMap = new Map(
3248
+ (payload.dep_blocked ?? []).map((entry) => [entry.feature_id, entry.depends_on_unresolved]),
3249
+ );
3250
+ const reverseDependencyMap = new Map<string, string[]>();
3251
+ for (const [blockedFeatureId, blockers] of dependencyMap.entries()) {
3252
+ for (const blocker of blockers) {
3253
+ const existing = reverseDependencyMap.get(blocker) ?? [];
3254
+ existing.push(blockedFeatureId);
3255
+ reverseDependencyMap.set(blocker, existing);
3256
+ }
3257
+ }
3258
+ const queueFeatures = payload.features
3259
+ .filter((feature) => feature.phase === 'ready_to_merge')
3260
+ .sort((left, right) => {
3261
+ const leftMs = left.phase_entered_at ? Date.parse(left.phase_entered_at) : Number.NaN;
3262
+ const rightMs = right.phase_entered_at ? Date.parse(right.phase_entered_at) : Number.NaN;
3263
+ const safeLeft = Number.isNaN(leftMs) ? Number.MAX_SAFE_INTEGER : leftMs;
3264
+ const safeRight = Number.isNaN(rightMs) ? Number.MAX_SAFE_INTEGER : rightMs;
3265
+ if (safeLeft === safeRight) {
3266
+ return left.feature_id.localeCompare(right.feature_id);
3267
+ }
3268
+ return safeLeft - safeRight;
3269
+ });
3270
+
3271
+ const queue = queueFeatures.map((feature, index) => ({
3272
+ feature_id: feature.feature_id,
3273
+ position: index + 1,
3274
+ total: queueFeatures.length,
3275
+ status: feature.status,
3276
+ phase_entered_at: feature.phase_entered_at ?? null,
3277
+ merge_ready: feature.pr?.merge_ready === true,
3278
+ ci_status: feature.pr?.ci_status ?? null,
3279
+ pending_review_threads: feature.pr?.pending_review_threads ?? 0,
3280
+ has_conflicts: feature.pr?.has_conflicts === true,
3281
+ merge_score: feature.pr?.merge_score ?? null,
3282
+ blocked_by: dependencyMap.get(feature.feature_id) ?? [],
3283
+ blocks: reverseDependencyMap.get(feature.feature_id) ?? [],
3284
+ estimated_merge_at:
3285
+ index === 0 ? null : new Date(Date.now() + index * 15 * 60_000).toISOString(),
3286
+ lock_holder: false,
3287
+ })) satisfies MergeQueueEntry[];
3288
+
3289
+ return {
3290
+ queue,
3291
+ current_feature: featureId
3292
+ ? (queue.find((entry) => entry.feature_id === featureId) ?? null)
3293
+ : null,
3294
+ };
3295
+ }
3296
+
3297
+ export async function readProviderAnalyticsSnapshot(
3298
+ repoRoot = AOP_ROOT,
3299
+ ): Promise<ProviderAnalytics[]> {
3300
+ const payload = await readDashboardStatus(repoRoot);
3301
+ const allEvents = await readAllWorkerEvents(repoRoot);
3302
+ const featureCost = new Map<string, number>();
3303
+ for (const feature of payload.features) {
3304
+ if (typeof feature.usage_tokens === 'number' && Number.isFinite(feature.usage_tokens)) {
3305
+ featureCost.set(feature.feature_id, feature.usage_tokens);
3306
+ }
3307
+ }
3308
+
3309
+ const aggregate = new Map<
3310
+ string,
3311
+ {
3312
+ provider: string;
3313
+ model: string;
3314
+ total_features: Set<string>;
3315
+ token_features: Set<string>;
3316
+ success_count: number;
3317
+ retry_count_total: number;
3318
+ duration_total: number;
3319
+ duration_count: number;
3320
+ token_total: number;
3321
+ }
3322
+ >();
3323
+
3324
+ for (const entry of allEvents) {
3325
+ const key = `${entry.provider}:${entry.model}`;
3326
+ const existing = aggregate.get(key) ?? {
3327
+ provider: entry.provider,
3328
+ model: entry.model,
3329
+ total_features: new Set<string>(),
3330
+ token_features: new Set<string>(),
3331
+ success_count: 0,
3332
+ retry_count_total: 0,
3333
+ duration_total: 0,
3334
+ duration_count: 0,
3335
+ token_total: 0,
3336
+ };
3337
+ if (entry.feature_id) {
3338
+ existing.total_features.add(entry.feature_id);
3339
+ if (!existing.token_features.has(entry.feature_id)) {
3340
+ existing.token_total += featureCost.get(entry.feature_id) ?? 0;
3341
+ existing.token_features.add(entry.feature_id);
3342
+ }
3343
+ }
3344
+ if (entry.valid) {
3345
+ existing.success_count += 1;
3346
+ }
3347
+ existing.retry_count_total += Math.max(0, entry.request_count - 1);
3348
+ if (typeof entry.elapsed_ms === 'number' && Number.isFinite(entry.elapsed_ms)) {
3349
+ existing.duration_total += entry.elapsed_ms;
3350
+ existing.duration_count += 1;
3351
+ }
3352
+ aggregate.set(key, existing);
3353
+ }
3354
+
3355
+ return Array.from(aggregate.values())
3356
+ .map((entry) => {
3357
+ const totalFeatures = entry.total_features.size;
3358
+ const avgDuration =
3359
+ entry.duration_count > 0 ? entry.duration_total / entry.duration_count : 0;
3360
+ const avgRetry = totalFeatures > 0 ? entry.retry_count_total / totalFeatures : 0;
3361
+ return {
3362
+ provider: entry.provider,
3363
+ model: entry.model,
3364
+ total_features: totalFeatures,
3365
+ success_count: entry.success_count,
3366
+ success_rate: totalFeatures > 0 ? entry.success_count / totalFeatures : 0,
3367
+ avg_retry_count: truncateNumber(avgRetry, 2) ?? 0,
3368
+ avg_duration_ms: truncateNumber(avgDuration, 1) ?? 0,
3369
+ avg_tokens_used:
3370
+ totalFeatures > 0 ? (truncateNumber(entry.token_total / totalFeatures, 1) ?? 0) : 0,
3371
+ avg_cost_usd: 0,
3372
+ } satisfies ProviderAnalytics;
3373
+ })
3374
+ .sort(
3375
+ (left, right) =>
3376
+ right.total_features - left.total_features || left.provider.localeCompare(right.provider),
3377
+ );
3378
+ }
3379
+
3380
+ function resolveUsageBaseline(
3381
+ entries: WorkerEventEntry[],
3382
+ providers: ProviderAnalytics[],
3383
+ ): ProviderAnalytics | null {
3384
+ const currentProviderModel = selectPrimaryProviderModel(entries);
3385
+ if (!currentProviderModel.provider || !currentProviderModel.model) {
3386
+ return null;
3387
+ }
3388
+ return (
3389
+ providers.find(
3390
+ (provider) =>
3391
+ provider.provider === currentProviderModel.provider &&
3392
+ provider.model === currentProviderModel.model,
3393
+ ) ?? null
3394
+ );
3395
+ }
3396
+
3397
+ function normalizeRepoRelativePath(repoRoot: string, targetPath: string): string {
3398
+ return path.relative(repoRoot, targetPath).replaceAll('\\', '/');
3399
+ }
3400
+
3401
+ async function pathExists(targetPath: string): Promise<boolean> {
3402
+ try {
3403
+ await stat(targetPath);
3404
+ return true;
3405
+ } catch {
3406
+ return false;
3407
+ }
3408
+ }
3409
+
3410
+ function specFilePath(featureId: string, repoRoot = AOP_ROOT): string {
3411
+ return path.join(getFeatureRoot(featureId, repoRoot), 'spec.md');
3412
+ }
3413
+
3414
+ function bootstrapManifestPath(featureId: string, repoRoot = AOP_ROOT): string {
3415
+ return path.join(getFeatureRoot(featureId, repoRoot), 'spec.manifest.bootstrap.json');
3416
+ }
3417
+
3418
+ function verifiedManifestPath(featureId: string, repoRoot = AOP_ROOT): string {
3419
+ return path.join(getFeatureRoot(featureId, repoRoot), 'spec.manifest.verified.json');
3420
+ }
3421
+
3422
+ function intakeReviewPath(featureId: string, repoRoot = AOP_ROOT): string {
3423
+ return path.join(getFeatureRoot(featureId, repoRoot), 'intake.review.json');
3424
+ }
3425
+
3426
+ function planMarkdownPath(featureId: string, repoRoot = AOP_ROOT): string {
3427
+ return path.join(getFeatureRoot(featureId, repoRoot), 'plan.md');
3428
+ }
3429
+
3430
+ export type PlannerArtifactId = PlannerArtifactView['id'];
3431
+
3432
+ function plannerArtifactPath(
3433
+ featureId: string,
3434
+ artifactId: PlannerArtifactId,
3435
+ repoRoot = AOP_ROOT,
3436
+ ): string {
3437
+ if (artifactId === 'source_spec') {
3438
+ return specFilePath(featureId, repoRoot);
3439
+ }
3440
+ if (artifactId === 'bootstrap_manifest') {
3441
+ return bootstrapManifestPath(featureId, repoRoot);
3442
+ }
3443
+ if (artifactId === 'verified_manifest') {
3444
+ return verifiedManifestPath(featureId, repoRoot);
3445
+ }
3446
+ if (artifactId === 'intake_review') {
3447
+ return intakeReviewPath(featureId, repoRoot);
3448
+ }
3449
+ if (artifactId === 'plan_markdown') {
3450
+ return planMarkdownPath(featureId, repoRoot);
3451
+ }
3452
+ return path.join(getFeatureRoot(featureId, repoRoot), 'plan.json');
3453
+ }
3454
+
3455
+ async function readTextArtifact(
3456
+ filePath: string,
3457
+ ): Promise<{ raw: string; updatedAt: string | null } | null> {
3458
+ try {
3459
+ const [raw, fileStats] = await Promise.all([readFile(filePath, 'utf8'), stat(filePath)]);
3460
+ return {
3461
+ raw,
3462
+ updatedAt: fileStats.mtime.toISOString(),
3463
+ };
3464
+ } catch {
3465
+ return null;
3466
+ }
3467
+ }
3468
+
3469
+ async function readJsonArtifact(
3470
+ filePath: string,
3471
+ ): Promise<{ parsed: Record<string, unknown>; raw: string; updatedAt: string | null } | null> {
3472
+ const artifact = await readTextArtifact(filePath);
3473
+ if (!artifact) {
3474
+ return null;
3475
+ }
3476
+ try {
3477
+ return {
3478
+ parsed: JSON.parse(artifact.raw) as Record<string, unknown>,
3479
+ raw: artifact.raw,
3480
+ updatedAt: artifact.updatedAt,
3481
+ };
3482
+ } catch {
3483
+ return null;
3484
+ }
3485
+ }
3486
+
3487
+ function buildPlannerArtifactView(input: {
3488
+ id: PlannerArtifactId;
3489
+ label: PlannerArtifactView['label'];
3490
+ format: PlannerArtifactView['format'];
3491
+ repoRoot: string;
3492
+ featureId: string;
3493
+ path: string;
3494
+ version: number | null;
3495
+ updatedAt: string | null;
3496
+ rawContent: string | null;
3497
+ primary: boolean;
3498
+ authorityLabel: string | null;
3499
+ emptyState: string;
3500
+ }): PlannerArtifactView {
3501
+ return {
3502
+ id: input.id,
3503
+ label: input.label,
3504
+ format: input.format,
3505
+ path: normalizeRepoRelativePath(input.repoRoot, input.path),
3506
+ version: input.version,
3507
+ updated_at: input.updatedAt,
3508
+ raw_content: input.rawContent,
3509
+ primary: input.primary,
3510
+ authority_label: input.authorityLabel,
3511
+ empty_state: input.emptyState,
3512
+ };
3513
+ }
3514
+
3515
+ export async function readPlannerArtifactRaw(
3516
+ featureId: string,
3517
+ artifactId: PlannerArtifactId,
3518
+ repoRoot = AOP_ROOT,
3519
+ ): Promise<{ content: string; path: string } | null> {
3520
+ const absolutePath = plannerArtifactPath(featureId, artifactId, repoRoot);
3521
+ const artifact = await readTextArtifact(absolutePath);
3522
+ if (!artifact) {
3523
+ return null;
3524
+ }
3525
+ return {
3526
+ content: artifact.raw,
3527
+ path: absolutePath,
3528
+ };
3529
+ }
3530
+
3531
+ function sortCheckpointsDescending(checkpoints: FeatureCheckpoint[]): FeatureCheckpoint[] {
3532
+ return [...checkpoints].sort((left, right) => {
3533
+ const leftMs = Date.parse(left.timestamp);
3534
+ const rightMs = Date.parse(right.timestamp);
3535
+ return (Number.isNaN(rightMs) ? 0 : rightMs) - (Number.isNaN(leftMs) ? 0 : leftMs);
3536
+ });
3537
+ }
3538
+
3539
+ function timestampInWindow(
3540
+ timestamp: string | null | undefined,
3541
+ startedAt: string,
3542
+ endedAt: string | null,
3543
+ ): boolean {
3544
+ if (typeof timestamp !== 'string') {
3545
+ return false;
3546
+ }
3547
+ const valueMs = Date.parse(timestamp);
3548
+ const startedMs = Date.parse(startedAt);
3549
+ const endedMs = endedAt ? Date.parse(endedAt) : Number.POSITIVE_INFINITY;
3550
+ if (Number.isNaN(valueMs) || Number.isNaN(startedMs)) {
3551
+ return false;
3552
+ }
3553
+ return (
3554
+ valueMs >= startedMs && valueMs <= (Number.isNaN(endedMs) ? Number.POSITIVE_INFINITY : endedMs)
3555
+ );
3556
+ }
3557
+
3558
+ function parseRunHistory(frontMatter: Record<string, unknown>): Array<{
3559
+ run_id: string;
3560
+ started_at: string;
3561
+ ended_at: string | null;
3562
+ status: FeatureRunHistoryEntry['status'];
3563
+ }> {
3564
+ const rawHistory = Array.isArray(frontMatter.run_history) ? frontMatter.run_history : [];
3565
+ return rawHistory
3566
+ .map((entry) => {
3567
+ const record = asRecord(entry);
3568
+ if (!record || typeof record.run_id !== 'string' || typeof record.started_at !== 'string') {
3569
+ return null;
3570
+ }
3571
+ const rawStatus = record.status;
3572
+ const status =
3573
+ rawStatus === 'active' ||
3574
+ rawStatus === 'completed' ||
3575
+ rawStatus === 'failed' ||
3576
+ rawStatus === 'timeout' ||
3577
+ rawStatus === 'interrupted'
3578
+ ? rawStatus
3579
+ : 'interrupted';
3580
+ return {
3581
+ run_id: record.run_id,
3582
+ started_at: record.started_at,
3583
+ ended_at: typeof record.ended_at === 'string' ? record.ended_at : null,
3584
+ status,
3585
+ };
3586
+ })
3587
+ .filter(
3588
+ (
3589
+ entry,
3590
+ ): entry is {
3591
+ run_id: string;
3592
+ started_at: string;
3593
+ ended_at: string | null;
3594
+ status: FeatureRunHistoryEntry['status'];
3595
+ } => entry != null,
3596
+ )
3597
+ .sort((left, right) => Date.parse(right.started_at) - Date.parse(left.started_at));
3598
+ }
3599
+
3600
+ export async function readFeatureSpec(
3601
+ featureId: string,
3602
+ repoRoot = AOP_ROOT,
3603
+ options: { includeMarkdown?: boolean } = {},
3604
+ ): Promise<FeatureSpecSummary | null> {
3605
+ const includeMarkdown = options.includeMarkdown ?? true;
3606
+ const fullPath = specFilePath(featureId, repoRoot);
3607
+ try {
3608
+ const [stats, markdown] = await Promise.all([
3609
+ stat(fullPath),
3610
+ includeMarkdown ? readFile(fullPath, 'utf8') : Promise.resolve(null),
3611
+ ]);
3612
+ return summarizeFeatureSpec(
3613
+ normalizeRepoRelativePath(repoRoot, fullPath),
3614
+ markdown,
3615
+ stats.mtime.toISOString(),
3616
+ );
3617
+ } catch {
3618
+ return null;
3619
+ }
3620
+ }
3621
+
3622
+ export async function readPlannerLifecycle(
3623
+ featureId: string,
3624
+ repoRoot = AOP_ROOT,
3625
+ ): Promise<PlannerLifecycleSummary | null> {
3626
+ const [feature, bootstrapArtifact, verifiedArtifact, plan, questions, specArtifact] =
3627
+ await Promise.all([
3628
+ readFeatureState(featureId, repoRoot),
3629
+ readJsonArtifact(bootstrapManifestPath(featureId, repoRoot)),
3630
+ readJsonArtifact(verifiedManifestPath(featureId, repoRoot)),
3631
+ readFeaturePlan(featureId, repoRoot),
3632
+ readFeatureQuestions(featureId, repoRoot),
3633
+ readTextArtifact(specFilePath(featureId, repoRoot)),
3634
+ ]);
3635
+ if (!feature) {
3636
+ return null;
3637
+ }
3638
+ return buildPlannerLifecycleSummary({
3639
+ feature,
3640
+ intakeSummary:
3641
+ feature.intake_status != null
3642
+ ? {
3643
+ status: feature.intake_status,
3644
+ bootstrap_manifest_version: feature.bootstrap_manifest_version ?? null,
3645
+ verified_manifest_version: feature.verified_manifest_version ?? null,
3646
+ open_ambiguity_count: feature.open_ambiguity_count ?? 0,
3647
+ last_verified_at: feature.last_verified_at ?? null,
3648
+ promotion_basis: feature.promotion_basis ?? null,
3649
+ }
3650
+ : null,
3651
+ hasSpec: specArtifact != null,
3652
+ bootstrapManifest: normalizeBootstrapManifestRecord(bootstrapArtifact?.parsed ?? null),
3653
+ verifiedManifest: normalizeVerifiedManifestRecord(verifiedArtifact?.parsed ?? null),
3654
+ acceptedPlan: normalizeAcceptedPlanRecord(plan),
3655
+ questions,
3656
+ });
3657
+ }
3658
+
3659
+ export async function readFeatureIntakeWorkspace(
3660
+ featureId: string,
3661
+ repoRoot = AOP_ROOT,
3662
+ ): Promise<FeatureIntakeWorkspace | null> {
3663
+ const [feature, bootstrapArtifact, verifiedArtifact, intakeReviewArtifact, spec, questions] =
3664
+ await Promise.all([
3665
+ readFeatureState(featureId, repoRoot),
3666
+ readJsonArtifact(bootstrapManifestPath(featureId, repoRoot)),
3667
+ readJsonArtifact(verifiedManifestPath(featureId, repoRoot)),
3668
+ readJsonArtifact(intakeReviewPath(featureId, repoRoot)),
3669
+ readFeatureSpec(featureId, repoRoot, { includeMarkdown: true }),
3670
+ readFeatureQuestions(featureId, repoRoot),
3671
+ ]);
3672
+ if (!feature) {
3673
+ return null;
3674
+ }
3675
+
3676
+ const intakeSummary: FeatureIntakeSummary | null =
3677
+ feature.intake_status != null
3678
+ ? {
3679
+ status: feature.intake_status,
3680
+ bootstrap_manifest_version: feature.bootstrap_manifest_version ?? null,
3681
+ verified_manifest_version: feature.verified_manifest_version ?? null,
3682
+ open_ambiguity_count: feature.open_ambiguity_count ?? 0,
3683
+ last_verified_at: feature.last_verified_at ?? null,
3684
+ promotion_basis: feature.promotion_basis ?? null,
3685
+ }
3686
+ : null;
3687
+ const intakeReview = normalizeIntakeReviewRecord(intakeReviewArtifact?.parsed ?? null);
3688
+ const intakeReviewSummary: FeatureIntakeReviewSummary | null = intakeReview
3689
+ ? {
3690
+ status: intakeReview.status,
3691
+ questions_open: intakeReview.questions_open,
3692
+ questions_resolved: intakeReview.questions_resolved,
3693
+ ambiguity_count: intakeReview.ambiguities.length,
3694
+ }
3695
+ : null;
3696
+ const bootstrapManifest = normalizeBootstrapManifestRecord(bootstrapArtifact?.parsed ?? null);
3697
+ const verifiedManifest = normalizeVerifiedManifestRecord(verifiedArtifact?.parsed ?? null);
3698
+ const artifacts: PlannerArtifactView[] = [
3699
+ buildPlannerArtifactView({
3700
+ id: 'source_spec',
3701
+ label: 'Source Spec',
3702
+ format: 'markdown',
3703
+ repoRoot,
3704
+ featureId,
3705
+ path: specFilePath(featureId, repoRoot),
3706
+ version: null,
3707
+ updatedAt: spec?.last_updated ?? null,
3708
+ rawContent: spec?.markdown ?? null,
3709
+ primary: verifiedManifest == null,
3710
+ authorityLabel: verifiedManifest == null ? 'Primary input' : 'Reference input',
3711
+ emptyState: 'No source spec is available for this feature.',
3712
+ }),
3713
+ buildPlannerArtifactView({
3714
+ id: 'bootstrap_manifest',
3715
+ label: 'Bootstrap Manifest',
3716
+ format: 'json',
3717
+ repoRoot,
3718
+ featureId,
3719
+ path: bootstrapManifestPath(featureId, repoRoot),
3720
+ version: bootstrapManifest?.manifest_version ?? null,
3721
+ updatedAt: bootstrapArtifact?.updatedAt ?? null,
3722
+ rawContent:
3723
+ bootstrapArtifact?.parsed != null
3724
+ ? JSON.stringify(bootstrapArtifact.parsed, null, 2)
3725
+ : null,
3726
+ primary: verifiedManifest == null,
3727
+ authorityLabel:
3728
+ verifiedManifest == null ? 'Current intake contract' : 'Original intake proposal',
3729
+ emptyState: 'Bootstrap manifest has not been generated yet.',
3730
+ }),
3731
+ buildPlannerArtifactView({
3732
+ id: 'verified_manifest',
3733
+ label: 'Verified Manifest',
3734
+ format: 'json',
3735
+ repoRoot,
3736
+ featureId,
3737
+ path: verifiedManifestPath(featureId, repoRoot),
3738
+ version: verifiedManifest?.manifest_version ?? null,
3739
+ updatedAt: verifiedArtifact?.updatedAt ?? null,
3740
+ rawContent:
3741
+ verifiedArtifact?.parsed != null ? JSON.stringify(verifiedArtifact.parsed, null, 2) : null,
3742
+ primary: verifiedManifest != null,
3743
+ authorityLabel: verifiedManifest != null ? 'Contract of record' : 'Pending verification',
3744
+ emptyState: 'Verified manifest has not been promoted yet.',
3745
+ }),
3746
+ buildPlannerArtifactView({
3747
+ id: 'intake_review',
3748
+ label: 'Intake Review',
3749
+ format: 'json',
3750
+ repoRoot,
3751
+ featureId,
3752
+ path: intakeReviewPath(featureId, repoRoot),
3753
+ version: intakeReview?.version ?? null,
3754
+ updatedAt: intakeReviewArtifact?.updatedAt ?? intakeReview?.last_updated_at ?? null,
3755
+ rawContent:
3756
+ intakeReviewArtifact?.parsed != null
3757
+ ? JSON.stringify(intakeReviewArtifact.parsed, null, 2)
3758
+ : null,
3759
+ primary: false,
3760
+ authorityLabel: 'Review record',
3761
+ emptyState: 'No intake review artifact has been recorded yet.',
3762
+ }),
3763
+ ];
3764
+
3765
+ return buildFeatureIntakeWorkspace({
3766
+ feature,
3767
+ intakeSummary,
3768
+ intakeReviewSummary,
3769
+ bootstrapManifest,
3770
+ verifiedManifest,
3771
+ intakeReview,
3772
+ questions,
3773
+ artifacts,
3774
+ });
3775
+ }
3776
+
3777
+ export async function readFeaturePlanningWorkspace(
3778
+ featureId: string,
3779
+ repoRoot = AOP_ROOT,
3780
+ ): Promise<FeaturePlanningWorkspace | null> {
3781
+ const [feature, verifiedArtifact, planArtifact, planMarkdownArtifact, questions] =
3782
+ await Promise.all([
3783
+ readFeatureState(featureId, repoRoot),
3784
+ readJsonArtifact(verifiedManifestPath(featureId, repoRoot)),
3785
+ readJsonArtifact(plannerArtifactPath(featureId, 'plan_json', repoRoot)),
3786
+ readTextArtifact(planMarkdownPath(featureId, repoRoot)),
3787
+ readFeatureQuestions(featureId, repoRoot),
3788
+ ]);
3789
+ if (!feature) {
3790
+ return null;
3791
+ }
3792
+ const intakeSummary: FeatureIntakeSummary | null =
3793
+ feature.intake_status != null
3794
+ ? {
3795
+ status: feature.intake_status,
3796
+ bootstrap_manifest_version: feature.bootstrap_manifest_version ?? null,
3797
+ verified_manifest_version: feature.verified_manifest_version ?? null,
3798
+ open_ambiguity_count: feature.open_ambiguity_count ?? 0,
3799
+ last_verified_at: feature.last_verified_at ?? null,
3800
+ promotion_basis: feature.promotion_basis ?? null,
3801
+ }
3802
+ : null;
3803
+ const verifiedManifest = normalizeVerifiedManifestRecord(verifiedArtifact?.parsed ?? null);
3804
+ const acceptedPlan = normalizeAcceptedPlanRecord(planArtifact?.parsed ?? null);
3805
+ const artifacts: PlannerArtifactView[] = [
3806
+ buildPlannerArtifactView({
3807
+ id: 'verified_manifest',
3808
+ label: 'Verified Manifest',
3809
+ format: 'json',
3810
+ repoRoot,
3811
+ featureId,
3812
+ path: verifiedManifestPath(featureId, repoRoot),
3813
+ version: verifiedManifest?.manifest_version ?? null,
3814
+ updatedAt: verifiedArtifact?.updatedAt ?? null,
3815
+ rawContent:
3816
+ verifiedArtifact?.parsed != null ? JSON.stringify(verifiedArtifact.parsed, null, 2) : null,
3817
+ primary: true,
3818
+ authorityLabel: 'Contract of record',
3819
+ emptyState: 'Verified manifest has not been promoted yet.',
3820
+ }),
3821
+ buildPlannerArtifactView({
3822
+ id: 'plan_markdown',
3823
+ label: 'Plan Markdown',
3824
+ format: 'markdown',
3825
+ repoRoot,
3826
+ featureId,
3827
+ path: planMarkdownPath(featureId, repoRoot),
3828
+ version: acceptedPlan?.plan_version ?? null,
3829
+ updatedAt: planMarkdownArtifact?.updatedAt ?? null,
3830
+ rawContent: planMarkdownArtifact?.raw ?? null,
3831
+ primary: acceptedPlan != null,
3832
+ authorityLabel: acceptedPlan != null ? 'Planner baseline' : 'Planning scratchpad',
3833
+ emptyState: 'No plan markdown has been recorded yet.',
3834
+ }),
3835
+ buildPlannerArtifactView({
3836
+ id: 'plan_json',
3837
+ label: 'Plan JSON',
3838
+ format: 'json',
3839
+ repoRoot,
3840
+ featureId,
3841
+ path: plannerArtifactPath(featureId, 'plan_json', repoRoot),
3842
+ version: acceptedPlan?.plan_version ?? null,
3843
+ updatedAt: planArtifact?.updatedAt ?? null,
3844
+ rawContent:
3845
+ planArtifact?.parsed != null ? JSON.stringify(planArtifact.parsed, null, 2) : null,
3846
+ primary: acceptedPlan != null,
3847
+ authorityLabel: acceptedPlan != null ? 'Accepted execution contract' : 'No accepted plan yet',
3848
+ emptyState: 'No accepted execution plan has been submitted yet.',
3849
+ }),
3850
+ ];
3851
+
3852
+ return buildFeaturePlanningWorkspace({
3853
+ feature,
3854
+ intakeSummary,
3855
+ verifiedManifest,
3856
+ acceptedPlan,
3857
+ plannerQuestions: questions,
3858
+ artifacts,
3859
+ });
3860
+ }
3861
+
3862
+ export async function readFeaturePlanProgress(
3863
+ featureId: string,
3864
+ repoRoot = AOP_ROOT,
3865
+ ): Promise<FeatureDetail['plan_progress']> {
3866
+ const [feature, plan, checkpointState, gateEvidence, qaTestIndex] = await Promise.all([
3867
+ readFeatureState(featureId, repoRoot),
3868
+ readFeaturePlan(featureId, repoRoot),
3869
+ readFeatureCheckpoints(featureId, repoRoot),
3870
+ readLatestGateEvidence(featureId, repoRoot),
3871
+ readFeatureQaTestIndex(featureId, repoRoot),
3872
+ ]);
3873
+ if (!feature) {
3874
+ return null;
3875
+ }
3876
+ return buildPlanProgressSummary({
3877
+ feature,
3878
+ plan,
3879
+ checkpoints: sortCheckpointsDescending(checkpointState?.checkpoints ?? []),
3880
+ gateEvidence,
3881
+ qaTestIndex,
3882
+ });
3883
+ }
3884
+
3885
+ export async function readFeatureCheckpointsPage(
3886
+ featureId: string,
3887
+ repoRoot = AOP_ROOT,
3888
+ options: { cursor?: string | null; limit?: number; includeIdle?: boolean } = {},
3889
+ ): Promise<FeatureCheckpointsPage | null> {
3890
+ const checkpointState = await readFeatureCheckpoints(featureId, repoRoot);
3891
+ if (!checkpointState) {
3892
+ return null;
3893
+ }
3894
+ const checkpoints = sortCheckpointsDescending(checkpointState.checkpoints);
3895
+ const visibleCheckpoints =
3896
+ options.includeIdle === false
3897
+ ? checkpoints.filter((checkpoint) => !isIdleCheckpoint(checkpoint))
3898
+ : checkpoints;
3899
+ const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);
3900
+ const parsedCursor = Number.parseInt(options.cursor ?? '0', 10);
3901
+ const offset = Number.isNaN(parsedCursor) ? 0 : Math.max(0, parsedCursor);
3902
+ const items = visibleCheckpoints.slice(offset, offset + limit);
3903
+ return {
3904
+ items,
3905
+ total_count: visibleCheckpoints.length,
3906
+ suppressed_count: checkpoints.length - visibleCheckpoints.length,
3907
+ cursor: String(offset),
3908
+ next_cursor: offset + limit < visibleCheckpoints.length ? String(offset + limit) : null,
3909
+ prev_cursor: offset > 0 ? String(Math.max(0, offset - limit)) : null,
3910
+ limit,
3911
+ };
3912
+ }
3913
+
3914
+ export async function readFeatureRunHistory(
3915
+ featureId: string,
3916
+ repoRoot = AOP_ROOT,
3917
+ ): Promise<FeatureRunHistoryEntry[]> {
3918
+ const [featureState, evidence, checkpointState, rawLogs] = await Promise.all([
3919
+ readFile(path.join(getFeatureRoot(featureId, repoRoot), 'state.md'), 'utf8').catch(() => null),
3920
+ listEvidenceArtifacts(featureId, repoRoot),
3921
+ readFeatureCheckpoints(featureId, repoRoot),
3922
+ listRawLogFiles(featureId, repoRoot, { limit: 200 }),
3923
+ ]);
3924
+ if (!featureState) {
3925
+ return [];
3926
+ }
3927
+ const frontMatter = parseFrontMatter(featureState).frontMatter;
3928
+ const runHistory = parseRunHistory(frontMatter);
3929
+ return Promise.all(
3930
+ runHistory.map(async (run) => {
3931
+ const evidenceCount = evidence.filter((artifact) =>
3932
+ timestampInWindow(artifact.updated_at, run.started_at, run.ended_at),
3933
+ ).length;
3934
+ const checkpointCount = (checkpointState?.checkpoints ?? []).filter((checkpoint) =>
3935
+ timestampInWindow(checkpoint.timestamp, run.started_at, run.ended_at),
3936
+ ).length;
3937
+ const rawLogCount = rawLogs.filter((log) =>
3938
+ timestampInWindow(log.timestamp, run.started_at, run.ended_at),
3939
+ ).length;
3940
+ const hasEvents = await pathExists(
3941
+ path.join(repoRoot, '.aop', 'runtime', 'worker-events', `${run.run_id}.jsonl`),
3942
+ );
3943
+ const hasOperations = await pathExists(
3944
+ path.join(repoRoot, '.aop', 'runtime', 'operation-ledger', `${run.run_id}.json`),
3945
+ );
3946
+ return {
3947
+ ...run,
3948
+ evidence_count: evidenceCount,
3949
+ checkpoint_count: checkpointCount,
3950
+ raw_log_count: rawLogCount,
3951
+ has_events: hasEvents,
3952
+ has_operations: hasOperations,
3953
+ } satisfies FeatureRunHistoryEntry;
3954
+ }),
3955
+ );
3956
+ }
3957
+
3958
+ export async function readFeatureRunHistoryDetail(
3959
+ featureId: string,
3960
+ runId: string,
3961
+ repoRoot = AOP_ROOT,
3962
+ ): Promise<FeatureRunHistoryDetail | null> {
3963
+ const [runs, evidence, checkpointState, rawLogs, feature, plan, gateHistory] = await Promise.all([
3964
+ readFeatureRunHistory(featureId, repoRoot),
3965
+ listEvidenceArtifacts(featureId, repoRoot),
3966
+ readFeatureCheckpoints(featureId, repoRoot),
3967
+ listRawLogFiles(featureId, repoRoot, { limit: 200 }),
3968
+ readFeatureState(featureId, repoRoot),
3969
+ readFeaturePlan(featureId, repoRoot),
3970
+ readGateHistory(featureId, repoRoot),
3971
+ ]);
3972
+ const run = runs.find((entry) => entry.run_id === runId);
3973
+ if (!run || !feature) {
3974
+ return null;
3975
+ }
3976
+ const revisionEvents = buildRevisionSummary({
3977
+ feature,
3978
+ plan,
3979
+ checkpoints: sortCheckpointsDescending(checkpointState?.checkpoints ?? []),
3980
+ gateHistory,
3981
+ }).filter((entry) => timestampInWindow(entry.timestamp, run.started_at, run.ended_at));
3982
+ return {
3983
+ run,
3984
+ evidence: evidence.filter((artifact) =>
3985
+ timestampInWindow(artifact.updated_at, run.started_at, run.ended_at),
3986
+ ),
3987
+ checkpoints: sortCheckpointsDescending(
3988
+ (checkpointState?.checkpoints ?? []).filter((checkpoint) =>
3989
+ timestampInWindow(checkpoint.timestamp, run.started_at, run.ended_at),
3990
+ ),
3991
+ ),
3992
+ raw_logs: rawLogs.filter((log) =>
3993
+ timestampInWindow(log.timestamp, run.started_at, run.ended_at),
3994
+ ),
3995
+ revision_events: revisionEvents,
3996
+ };
3997
+ }
3998
+
1661
3999
  export async function readFeatureDetail(
1662
4000
  featureId: string,
1663
4001
  repoRoot = AOP_ROOT,
@@ -1668,18 +4006,128 @@ export async function readFeatureDetail(
1668
4006
  }
1669
4007
 
1670
4008
  const checkpointState = await readFeatureCheckpoints(featureId, repoRoot);
4009
+ const budgetConfig = parseBudgetConfig(await readPolicyYaml(repoRoot));
4010
+ const cost = await readFeatureCost(featureId, repoRoot);
4011
+ const enrichedFeature = enrichFeatureSummary(feature, {
4012
+ cost,
4013
+ budgetConfig,
4014
+ dependencyBlocked: false,
4015
+ });
4016
+
4017
+ const [
4018
+ plan,
4019
+ diff,
4020
+ evidence,
4021
+ gateEvidence,
4022
+ qaTestIndex,
4023
+ reviewBrief,
4024
+ logEntries,
4025
+ collisionRadar,
4026
+ rolePerformance,
4027
+ workerSummary,
4028
+ liveOutputSnapshots,
4029
+ genealogy,
4030
+ gateHistory,
4031
+ mergeQueue,
4032
+ providerAnalytics,
4033
+ spec,
4034
+ intakeReview,
4035
+ ] = await Promise.all([
4036
+ readFeaturePlan(featureId, repoRoot),
4037
+ readFeatureDiff(featureId, repoRoot),
4038
+ listEvidenceArtifacts(featureId, repoRoot),
4039
+ readLatestGateEvidence(featureId, repoRoot),
4040
+ readFeatureQaTestIndex(featureId, repoRoot),
4041
+ readFeatureReviewBrief(featureId, repoRoot),
4042
+ readDecisionLogEntries(featureId, repoRoot),
4043
+ buildFeatureCollisionRadar(featureId, repoRoot),
4044
+ buildFeatureRolePerformanceCards(enrichedFeature, repoRoot),
4045
+ readFeatureWorkerEventSummary(featureId, repoRoot, { limit: 200 }),
4046
+ readLiveAgentOutputSnapshots(featureId, repoRoot),
4047
+ readFeatureGenealogy(featureId, repoRoot),
4048
+ readGateHistory(featureId, repoRoot),
4049
+ readMergeQueueSnapshot(featureId, repoRoot),
4050
+ readProviderAnalyticsSnapshot(repoRoot),
4051
+ readFeatureSpec(featureId, repoRoot, { includeMarkdown: false }),
4052
+ readFeatureIntakeReview(featureId, repoRoot),
4053
+ ]);
4054
+ const questions = await readFeatureQuestions(featureId, repoRoot, enrichedFeature);
4055
+
4056
+ const usageBurn = buildUsageBurnProjection(
4057
+ cost,
4058
+ workerSummary.entries,
4059
+ resolveUsageBaseline(workerSummary.entries, providerAnalytics),
4060
+ budgetConfig.per_feature_limit_tokens,
4061
+ budgetConfig.token_cost_conversion,
4062
+ enrichedFeature.phase ?? 'unknown',
4063
+ );
4064
+ const checkpoints = sortCheckpointsDescending(checkpointState?.checkpoints ?? []);
4065
+ const planProgress = buildPlanProgressSummary({
4066
+ feature: enrichedFeature,
4067
+ plan,
4068
+ checkpoints,
4069
+ gateEvidence,
4070
+ qaTestIndex,
4071
+ });
4072
+ const verificationSummary = buildVerificationSummary({
4073
+ feature: enrichedFeature,
4074
+ gateEvidence,
4075
+ qaTestIndex,
4076
+ usageBurn,
4077
+ });
4078
+ const revisionSummary = buildRevisionSummary({
4079
+ feature: enrichedFeature,
4080
+ plan,
4081
+ checkpoints,
4082
+ gateHistory,
4083
+ });
4084
+ const actionableRisks = buildActionableRisks(plan, enrichedFeature);
1671
4085
 
1672
4086
  return {
1673
- feature,
1674
- plan: await readFeaturePlan(featureId, repoRoot),
1675
- diff: await readFeatureDiff(featureId, repoRoot),
1676
- evidence: await listEvidenceArtifacts(featureId, repoRoot),
1677
- gate_evidence: await readLatestGateEvidence(featureId, repoRoot),
1678
- cost: await readFeatureCost(featureId, repoRoot),
1679
- qa_test_index: await readFeatureQaTestIndex(featureId, repoRoot),
1680
- review_brief: await readFeatureReviewBrief(featureId, repoRoot),
1681
- log_entries: (await readDecisionLogEntries(featureId, repoRoot)).slice(0, 50),
1682
- execution_mode: checkpointState?.execution_mode ?? null,
1683
- checkpoints: checkpointState?.checkpoints ?? [],
4087
+ feature: enrichedFeature,
4088
+ plan,
4089
+ diff,
4090
+ evidence,
4091
+ spec,
4092
+ plan_progress: planProgress,
4093
+ verification_summary: verificationSummary,
4094
+ revision_summary: revisionSummary,
4095
+ actionable_risks: actionableRisks,
4096
+ gate_evidence: gateEvidence,
4097
+ cost,
4098
+ budget_limit_tokens: budgetConfig.per_feature_limit_tokens,
4099
+ qa_test_index: qaTestIndex,
4100
+ review_brief: reviewBrief,
4101
+ log_entries: logEntries.slice(0, 50),
4102
+ execution_mode: checkpointState?.execution_mode ?? 'interactive',
4103
+ checkpoints,
4104
+ collision_radar: collisionRadar,
4105
+ role_performance: rolePerformance,
4106
+ usage_burn: usageBurn,
4107
+ genealogy,
4108
+ gate_history: gateHistory,
4109
+ merge_queue: mergeQueue,
4110
+ worker_summary: workerSummary,
4111
+ live_output_snapshots: summarizeLiveAgentOutputSnapshots(liveOutputSnapshots),
4112
+ provider_analytics: providerAnalytics,
4113
+ token_cost_conversion: budgetConfig.token_cost_conversion,
4114
+ questions: questions.filter(
4115
+ (question) =>
4116
+ question.status !== 'open' || isOperatorReadyQuestion(enrichedFeature, question),
4117
+ ),
4118
+ intake:
4119
+ enrichedFeature.intake_status != null
4120
+ ? {
4121
+ summary: {
4122
+ status: enrichedFeature.intake_status,
4123
+ bootstrap_manifest_version: enrichedFeature.bootstrap_manifest_version ?? null,
4124
+ verified_manifest_version: enrichedFeature.verified_manifest_version ?? null,
4125
+ open_ambiguity_count: enrichedFeature.open_ambiguity_count ?? 0,
4126
+ last_verified_at: enrichedFeature.last_verified_at ?? null,
4127
+ promotion_basis: enrichedFeature.promotion_basis ?? null,
4128
+ },
4129
+ review: intakeReview,
4130
+ }
4131
+ : null,
1684
4132
  };
1685
4133
  }