@tea-agent/loop-agent 0.1.0-fe-test.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1050) hide show
  1. package/AGENTS.md +114 -0
  2. package/CHANGELOG.md +2283 -0
  3. package/README.md +177 -0
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +57 -0
  6. package/dist/adapters/aimax.js +91 -0
  7. package/dist/adapters/context.js +32 -0
  8. package/dist/adapters/index.js +29 -0
  9. package/dist/adapters/loop-agent.js +192 -0
  10. package/dist/adapters/types.js +1 -0
  11. package/dist/application/context-usage/skill-resolution-stats.js +263 -0
  12. package/dist/application/dag/args.js +486 -0
  13. package/dist/application/dag/generate-task-dag.js +408 -0
  14. package/dist/application/dag/report-dag.js +14 -0
  15. package/dist/application/dag/run-dag.js +109 -0
  16. package/dist/application/dag/validate-dag.js +166 -0
  17. package/dist/application/evaluation/alias.js +184 -0
  18. package/dist/application/evaluation/budget.js +192 -0
  19. package/dist/application/evaluation/campaign-hash.js +47 -0
  20. package/dist/application/evaluation/campaign-matrix.js +372 -0
  21. package/dist/application/evaluation/campaign-scorecard.js +135 -0
  22. package/dist/application/evaluation/campaign.js +370 -0
  23. package/dist/application/evaluation/candidate-hash.js +75 -0
  24. package/dist/application/evaluation/candidate.js +69 -0
  25. package/dist/application/evaluation/corpus-hash.js +38 -0
  26. package/dist/application/evaluation/corpus.js +56 -0
  27. package/dist/application/evaluation/experiment.js +294 -0
  28. package/dist/application/evaluation/ignition.js +198 -0
  29. package/dist/application/evaluation/integrity-audit.js +162 -0
  30. package/dist/application/evaluation/outer-loop.js +132 -0
  31. package/dist/application/evaluation/pi-cell-executor.js +39 -0
  32. package/dist/application/evaluation/private-verifier.js +46 -0
  33. package/dist/application/evaluation/promotion-policy.js +151 -0
  34. package/dist/application/evaluation/proposer.js +98 -0
  35. package/dist/application/evaluation/replay.js +289 -0
  36. package/dist/application/evaluation/types.js +652 -0
  37. package/dist/application/loop/run-action.js +19 -0
  38. package/dist/application/task-lifecycle/advance.js +1204 -0
  39. package/dist/application/task-lifecycle/gates.js +130 -0
  40. package/dist/application/task-lifecycle/index.js +7 -0
  41. package/dist/application/task-lifecycle/observe.js +424 -0
  42. package/dist/application/task-lifecycle/plan-transitions.js +251 -0
  43. package/dist/application/task-lifecycle/recommendations.js +180 -0
  44. package/dist/application/task-lifecycle/record.js +73 -0
  45. package/dist/application/task-lifecycle/types.js +1 -0
  46. package/dist/build-stamp.json +6 -0
  47. package/dist/cli/catalog.js +24 -0
  48. package/dist/cli/command-definitions.js +659 -0
  49. package/dist/cli/help.js +56 -0
  50. package/dist/cli/index.js +5 -0
  51. package/dist/cli/program.js +647 -0
  52. package/dist/cli/router.js +13 -0
  53. package/dist/cli/update/init-surface-notifier.js +167 -0
  54. package/dist/cli/update/notifier.js +117 -0
  55. package/dist/cli/update/npm-client.js +151 -0
  56. package/dist/cli/update/policy.js +92 -0
  57. package/dist/cli/update/runtime-activity.js +29 -0
  58. package/dist/cli/update/state.js +68 -0
  59. package/dist/cli-governance/active-residue-check.js +38 -0
  60. package/dist/cli.js +57 -0
  61. package/dist/commands/client-recovery.js +1373 -0
  62. package/dist/commands/closeout.js +13 -0
  63. package/dist/commands/coverage-audit.js +14 -0
  64. package/dist/commands/coverage-report.js +50 -0
  65. package/dist/commands/cursor-prompt.js +182 -0
  66. package/dist/commands/dag-approve.js +142 -0
  67. package/dist/commands/dag-final-verification.js +76 -0
  68. package/dist/commands/dag-init-hybrid.js +57 -0
  69. package/dist/commands/dag-reconcile-run.js +5 -0
  70. package/dist/commands/dag-reconcile-tasks.js +51 -0
  71. package/dist/commands/dag-reject.js +91 -0
  72. package/dist/commands/dag-report.js +76 -0
  73. package/dist/commands/dag-request-interrupt.js +20 -0
  74. package/dist/commands/dag-rerun-task.js +19 -0
  75. package/dist/commands/dag-rerun.js +111 -0
  76. package/dist/commands/dag-resume.js +35 -0
  77. package/dist/commands/dag-run-task.js +12 -0
  78. package/dist/commands/dag-validate.js +20 -0
  79. package/dist/commands/dag-workflow-compile.js +91 -0
  80. package/dist/commands/dag-workflow-plan.js +130 -0
  81. package/dist/commands/dag-workflow-validate.js +66 -0
  82. package/dist/commands/delegate.js +130 -0
  83. package/dist/commands/docs-archive.js +5 -0
  84. package/dist/commands/docs-audit.js +6 -0
  85. package/dist/commands/doctor.js +283 -0
  86. package/dist/commands/eval.js +1398 -0
  87. package/dist/commands/examples.js +90 -0
  88. package/dist/commands/goal.js +92 -0
  89. package/dist/commands/handoff-check.js +5 -0
  90. package/dist/commands/harvest.js +44 -0
  91. package/dist/commands/import-prd.js +81 -0
  92. package/dist/commands/init-upgrade.js +3085 -0
  93. package/dist/commands/init.js +3630 -0
  94. package/dist/commands/inspect.js +11 -0
  95. package/dist/commands/instructions.js +223 -0
  96. package/dist/commands/knowledge.js +162 -0
  97. package/dist/commands/loop-benchmark.js +72 -0
  98. package/dist/commands/loop.js +251 -0
  99. package/dist/commands/new-task.js +5 -0
  100. package/dist/commands/operator.js +44 -0
  101. package/dist/commands/pi-prompt.js +174 -0
  102. package/dist/commands/pi-reuse-benchmark.js +153 -0
  103. package/dist/commands/plan-list.js +5 -0
  104. package/dist/commands/plan.js +50 -0
  105. package/dist/commands/promote-run.js +29 -0
  106. package/dist/commands/reference-index.js +16 -0
  107. package/dist/commands/run-dag-progress.js +123 -0
  108. package/dist/commands/run-dag.js +22 -0
  109. package/dist/commands/spine.js +38 -0
  110. package/dist/commands/stats.js +113 -0
  111. package/dist/commands/status.js +57 -0
  112. package/dist/commands/study-init.js +192 -0
  113. package/dist/commands/task-advance.js +366 -0
  114. package/dist/commands/task-contract.js +269 -0
  115. package/dist/commands/task-source-prepare.js +479 -0
  116. package/dist/commands/task-status.js +133 -0
  117. package/dist/commands/workflow.js +259 -0
  118. package/dist/commands/worktree-create.js +31 -0
  119. package/dist/commands/worktree-list.js +5 -0
  120. package/dist/commands/worktree-remove.js +26 -0
  121. package/dist/executors/config-core.js +5 -0
  122. package/dist/executors/config.js +2 -0
  123. package/dist/executors/dag-pi-executor.js +1277 -0
  124. package/dist/executors/dag-static-executor.js +42 -0
  125. package/dist/executors/dag.js +3 -0
  126. package/dist/executors/index.js +6 -0
  127. package/dist/executors/model-routing.js +71 -0
  128. package/dist/executors/pi-defaults.js +9 -0
  129. package/dist/executors/pi-event-serializer.js +65 -0
  130. package/dist/executors/pi-executor.js +981 -0
  131. package/dist/executors/pi-playwright-cli-tool.js +1006 -0
  132. package/dist/executors/pi-prompt-transport.js +198 -0
  133. package/dist/executors/pi-reuse-benchmark.js +316 -0
  134. package/dist/executors/pi-runtime-reuse.js +29 -0
  135. package/dist/executors/pi-sdk-executor.js +722 -0
  136. package/dist/executors/pi-sdk.js +1 -0
  137. package/dist/executors/pi-writer-tool-policy.js +266 -0
  138. package/dist/executors/pi.js +3 -0
  139. package/dist/executors/playwright-cli-launcher.js +63 -0
  140. package/dist/executors/process-tree.js +33 -0
  141. package/dist/executors/shell-executor.js +3055 -0
  142. package/dist/executors/shell-presets.js +99 -0
  143. package/dist/executors/shell-verification.js +262 -0
  144. package/dist/executors/shell-write-guard.js +543 -0
  145. package/dist/executors/shell.js +3 -0
  146. package/dist/executors/static.js +1 -0
  147. package/dist/governance/checks.js +437 -0
  148. package/dist/governance/document-index-closure.js +164 -0
  149. package/dist/governance/exec-plans.js +549 -0
  150. package/dist/governance/harness.js +9 -0
  151. package/dist/governance/index.js +3 -0
  152. package/dist/governance/manifest-types.js +254 -0
  153. package/dist/governance/manifest.js +2 -0
  154. package/dist/governance/path-guard.js +69 -0
  155. package/dist/governance/path-guards.js +2 -0
  156. package/dist/governance/profiles.js +3 -0
  157. package/dist/governance/requirement-coverage.js +425 -0
  158. package/dist/governance/skill-safety.js +135 -0
  159. package/dist/governance/spine-audit.js +155 -0
  160. package/dist/infrastructure/evaluation/alias-store.js +199 -0
  161. package/dist/infrastructure/evaluation/campaign-store.js +154 -0
  162. package/dist/infrastructure/evaluation/candidate-store.js +439 -0
  163. package/dist/infrastructure/evaluation/corpus-store.js +181 -0
  164. package/dist/infrastructure/evaluation/experiment-store.js +124 -0
  165. package/dist/infrastructure/evaluation/ignition-store.js +82 -0
  166. package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
  167. package/dist/infrastructure/evaluation/proposer-store.js +78 -0
  168. package/dist/infrastructure/evaluation/store.js +40 -0
  169. package/dist/infrastructure/harness/active-residue-policy.js +73 -0
  170. package/dist/infrastructure/harness/artifact-store.js +72 -0
  171. package/dist/infrastructure/harness/atomic-write.js +50 -0
  172. package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
  173. package/dist/infrastructure/harness/loop-action-store.js +20 -0
  174. package/dist/infrastructure/harness/loop-store.js +41 -0
  175. package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
  176. package/dist/infrastructure/harness/task-store.js +108 -0
  177. package/dist/records/closeout.js +2 -0
  178. package/dist/records/harvest.js +215 -0
  179. package/dist/records/index.js +3 -0
  180. package/dist/records/one-shot-runs.js +386 -0
  181. package/dist/records/promotion.js +200 -0
  182. package/dist/shared/artifacts-core.js +107 -0
  183. package/dist/shared/artifacts.js +2 -0
  184. package/dist/shared/context-files.js +32 -0
  185. package/dist/shared/context.js +2 -0
  186. package/dist/shared/copy-dir.js +17 -0
  187. package/dist/shared/git-progress.js +172 -0
  188. package/dist/shared/index.js +5 -0
  189. package/dist/shared/logger.js +17 -0
  190. package/dist/shared/one-shot-prompt-args.js +98 -0
  191. package/dist/shared/openspec-spec.js +125 -0
  192. package/dist/shared/operator/capabilities.js +2659 -0
  193. package/dist/shared/operator/command-lifecycle.js +94 -0
  194. package/dist/shared/operator/envelope.js +59 -0
  195. package/dist/shared/operator/index.js +5 -0
  196. package/dist/shared/operator/registry.js +38 -0
  197. package/dist/shared/operator/types.js +5 -0
  198. package/dist/shared/output-truncation.js +37 -0
  199. package/dist/shared/package-metadata.js +530 -0
  200. package/dist/shared/path-refs.js +31 -0
  201. package/dist/shared/pi-retry-settings.js +23 -0
  202. package/dist/shared/playwright-cli-command-policy.js +41 -0
  203. package/dist/shared/preview.js +39 -0
  204. package/dist/shared/prompts.js +26 -0
  205. package/dist/shared/reference-context.js +264 -0
  206. package/dist/shared/resilient-git.js +133 -0
  207. package/dist/shared/runtime-activity.js +6 -0
  208. package/dist/shared/timeout-policy.js +24 -0
  209. package/dist/shared/timeout.js +1 -0
  210. package/dist/shared/types.js +5 -0
  211. package/dist/sidecars/cursor-prompt/executor.js +428 -0
  212. package/dist/sidecars/cursor-prompt/index.js +3 -0
  213. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  214. package/dist/task/config-types.js +235 -0
  215. package/dist/task/config.js +2 -0
  216. package/dist/task/contract/adopt.js +166 -0
  217. package/dist/task/contract/apply.js +326 -0
  218. package/dist/task/contract/canonicalize.js +60 -0
  219. package/dist/task/contract/constants.js +30 -0
  220. package/dist/task/contract/diff.js +177 -0
  221. package/dist/task/contract/hash.js +42 -0
  222. package/dist/task/contract/import-revision.js +96 -0
  223. package/dist/task/contract/index.js +17 -0
  224. package/dist/task/contract/journal.js +155 -0
  225. package/dist/task/contract/lock.js +153 -0
  226. package/dist/task/contract/observe.js +296 -0
  227. package/dist/task/contract/paths.js +19 -0
  228. package/dist/task/contract/project.js +185 -0
  229. package/dist/task/contract/recover.js +312 -0
  230. package/dist/task/contract/request-ledger.js +37 -0
  231. package/dist/task/contract/schema.js +153 -0
  232. package/dist/task/contract/transaction.js +160 -0
  233. package/dist/task/contract/types.js +1 -0
  234. package/dist/task/contract/validate-draft.js +106 -0
  235. package/dist/task/dag-source-paths.js +50 -0
  236. package/dist/task/delegate.js +208 -0
  237. package/dist/task/frontend-preflight.js +131 -0
  238. package/dist/task/frontend-project-capability.js +570 -0
  239. package/dist/task/goal-audit.js +51 -0
  240. package/dist/task/goal-policy.js +8 -0
  241. package/dist/task/goal.js +3 -0
  242. package/dist/task/ids.js +1 -0
  243. package/dist/task/index.js +12 -0
  244. package/dist/task/lifecycle.js +1 -0
  245. package/dist/task/operator/capabilities.js +6 -0
  246. package/dist/task/operator/envelope.js +2 -0
  247. package/dist/task/operator/index.js +5 -0
  248. package/dist/task/operator/registry.js +2 -0
  249. package/dist/task/operator/types.js +1 -0
  250. package/dist/task/paths.js +1 -0
  251. package/dist/task/read-model.js +140 -0
  252. package/dist/task/runtime.js +708 -0
  253. package/dist/task/source-prepare/artifact-meta.js +137 -0
  254. package/dist/task/source-prepare/build-draft.js +232 -0
  255. package/dist/task/source-prepare/completeness.js +248 -0
  256. package/dist/task/source-prepare/index.js +10 -0
  257. package/dist/task/source-prepare/parse-intent.js +496 -0
  258. package/dist/task/source-prepare/path-policy.js +197 -0
  259. package/dist/task/source-prepare/placeholder-paths.js +77 -0
  260. package/dist/task/source-prepare/prepare.js +920 -0
  261. package/dist/task/source-prepare/reference-integrity.js +290 -0
  262. package/dist/task/source-prepare/semantic-intake.js +717 -0
  263. package/dist/task/source-prepare/types.js +7 -0
  264. package/dist/task/source-references.js +228 -0
  265. package/dist/task/source-state.js +1 -0
  266. package/dist/task/state.js +41 -0
  267. package/dist/task/subagent-guidance.js +1 -0
  268. package/dist/task/task-demand-routing.js +443 -0
  269. package/dist/task/workflow-state-types.js +92 -0
  270. package/dist/task/worktree-cleanup.js +140 -0
  271. package/dist/task/worktree.js +388 -0
  272. package/dist/verification/maven/cache.js +142 -0
  273. package/dist/verification/maven/index.js +120 -0
  274. package/dist/verification/maven/plan-commands.js +421 -0
  275. package/dist/verification/maven/pom-static.js +136 -0
  276. package/dist/verification/maven/scope-resolve.js +153 -0
  277. package/dist/verification/maven/stale.js +130 -0
  278. package/dist/verification/maven/types.js +23 -0
  279. package/dist/verification/maven/workspace-graph.js +322 -0
  280. package/dist/worker/cli.js +957 -0
  281. package/dist/worker/closeout/apply.js +73 -0
  282. package/dist/worker/closeout/preview.js +30 -0
  283. package/dist/worker/console/app-data.js +316 -0
  284. package/dist/worker/console/chat/artifact-card.js +30 -0
  285. package/dist/worker/console/chat/assistant-content.js +121 -0
  286. package/dist/worker/console/chat/chat-event-store.js +981 -0
  287. package/dist/worker/console/chat/chat-ui-policy.js +25 -0
  288. package/dist/worker/console/chat/compaction-errors.js +64 -0
  289. package/dist/worker/console/chat/composer-draft-store.js +45 -0
  290. package/dist/worker/console/chat/context-panel.js +54 -0
  291. package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
  292. package/dist/worker/console/chat/explore-tools.js +299 -0
  293. package/dist/worker/console/chat/human-gate-card.js +45 -0
  294. package/dist/worker/console/chat/interview-adapter.js +149 -0
  295. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  296. package/dist/worker/console/chat/model-resolver.js +215 -0
  297. package/dist/worker/console/chat/operation-card.js +92 -0
  298. package/dist/worker/console/chat/pi-console-config.js +376 -0
  299. package/dist/worker/console/chat/pi-runtime.js +2446 -0
  300. package/dist/worker/console/chat/repo-browser.js +140 -0
  301. package/dist/worker/console/chat/repo-walk.js +125 -0
  302. package/dist/worker/console/chat/resource-loader.js +59 -0
  303. package/dist/worker/console/chat/resource-preferences-store.js +152 -0
  304. package/dist/worker/console/chat/routes.js +3137 -0
  305. package/dist/worker/console/chat/runtime-context.js +102 -0
  306. package/dist/worker/console/chat/runtime-selection.js +96 -0
  307. package/dist/worker/console/chat/semantic-activity.js +489 -0
  308. package/dist/worker/console/chat/session-store.js +484 -0
  309. package/dist/worker/console/chat/shortcuts.js +217 -0
  310. package/dist/worker/console/chat/tool-adapter.js +131 -0
  311. package/dist/worker/console/chat/tool-preview.js +247 -0
  312. package/dist/worker/console/chat/tools.js +212 -0
  313. package/dist/worker/console/chat/turn-execution-registry.js +82 -0
  314. package/dist/worker/console/chat/turn-process.js +346 -0
  315. package/dist/worker/console/chat/usage.js +165 -0
  316. package/dist/worker/console/chat/workspace-landing.js +103 -0
  317. package/dist/worker/console/dag-confirmation.js +347 -0
  318. package/dist/worker/console/dag-execution-receipt.js +393 -0
  319. package/dist/worker/console/doctor.js +230 -0
  320. package/dist/worker/console/draft-store.js +158 -0
  321. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  322. package/dist/worker/console/human-gate-token.js +130 -0
  323. package/dist/worker/console/index.js +17 -0
  324. package/dist/worker/console/inspect-split.js +82 -0
  325. package/dist/worker/console/interview/assessment.js +67 -0
  326. package/dist/worker/console/interview/grill-me.js +277 -0
  327. package/dist/worker/console/interview/session.js +100 -0
  328. package/dist/worker/console/interview/tools.js +109 -0
  329. package/dist/worker/console/loopback.js +16 -0
  330. package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
  331. package/dist/worker/console/night-aux-ticker.js +146 -0
  332. package/dist/worker/console/observe-health-match.js +179 -0
  333. package/dist/worker/console/observe-link.js +42 -0
  334. package/dist/worker/console/open-browser.js +134 -0
  335. package/dist/worker/console/operation-run-facts.js +190 -0
  336. package/dist/worker/console/operation-runner.js +353 -0
  337. package/dist/worker/console/operation-sse.js +184 -0
  338. package/dist/worker/console/operation-store.js +169 -0
  339. package/dist/worker/console/operation-wait.js +355 -0
  340. package/dist/worker/console/operator-actions.js +3768 -0
  341. package/dist/worker/console/operator-selection.js +105 -0
  342. package/dist/worker/console/operator-surface-health.js +24 -0
  343. package/dist/worker/console/operator-user-error.js +45 -0
  344. package/dist/worker/console/pi-readiness.js +288 -0
  345. package/dist/worker/console/prd-identity.js +102 -0
  346. package/dist/worker/console/prd-intake-bridge.js +441 -0
  347. package/dist/worker/console/prd-reference-discovery.js +124 -0
  348. package/dist/worker/console/recovery-cta.js +320 -0
  349. package/dist/worker/console/recovery-error-copy.js +198 -0
  350. package/dist/worker/console/recovery-selection.js +107 -0
  351. package/dist/worker/console/repo-fingerprint.js +35 -0
  352. package/dist/worker/console/resolve-dag-run-for-task.js +115 -0
  353. package/dist/worker/console/resource-loader.js +95 -0
  354. package/dist/worker/console/routes.js +475 -0
  355. package/dist/worker/console/security.js +170 -0
  356. package/dist/worker/console/server.js +357 -0
  357. package/dist/worker/console/sibling-controller.js +33 -0
  358. package/dist/worker/console/static/assets/abnfDiagram-N423BO3Z-BmOEx0gA.js +1 -0
  359. package/dist/worker/console/static/assets/arc-BGiVVe8b.js +1 -0
  360. package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-nDdu4rgJ.js +36 -0
  361. package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-Bb5u0BWf.js +132 -0
  362. package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-BlhC0j94.js +10 -0
  363. package/dist/worker/console/static/assets/channel-Tl-FkUqr.js +1 -0
  364. package/dist/worker/console/static/assets/chunk-2GRJ4B5K-Ai8_qQXI.js +1 -0
  365. package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B9gyCjqX.js +1 -0
  366. package/dist/worker/console/static/assets/chunk-5RXB4S5H-Bzbj3zGH.js +231 -0
  367. package/dist/worker/console/static/assets/chunk-5VM5RSS4-BT415PvT.js +15 -0
  368. package/dist/worker/console/static/assets/chunk-6Q2QTUOP-D_A5_5QL.js +88 -0
  369. package/dist/worker/console/static/assets/chunk-GF5L2VYU-srJkaP3f.js +206 -0
  370. package/dist/worker/console/static/assets/chunk-JWPE2WC7-UYGvEP4l.js +1 -0
  371. package/dist/worker/console/static/assets/chunk-KBJHAD2P-B8luBrrz.js +1 -0
  372. package/dist/worker/console/static/assets/chunk-RYQCIY6F-DN7wXZSq.js +1 -0
  373. package/dist/worker/console/static/assets/chunk-XXDRQBXY-DXKyMuKS.js +1 -0
  374. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BaehtRk4.js +1 -0
  375. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BaehtRk4.js +1 -0
  376. package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-dqvoynXj.js +1 -0
  377. package/dist/worker/console/static/assets/cynefin-VYW2F7L2--PcyL34A.js +166 -0
  378. package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DvWs_hTY.js +62 -0
  379. package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +321 -0
  380. package/dist/worker/console/static/assets/dagre-VZM6K2ZE-DxeIxbyO.js +4 -0
  381. package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  382. package/dist/worker/console/static/assets/diagram-7IWD3JNH-DPCG61Of.js +30 -0
  383. package/dist/worker/console/static/assets/diagram-B4RE2ZJO-CmuU50-U.js +3 -0
  384. package/dist/worker/console/static/assets/diagram-LBJQPF4R-B61XLD_v.js +24 -0
  385. package/dist/worker/console/static/assets/diagram-Q27KOJAE-BB7HTRRY.js +24 -0
  386. package/dist/worker/console/static/assets/diagram-UB23O5K3-DvkboAIp.js +41 -0
  387. package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-BdJ_y1fL.js +1 -0
  388. package/dist/worker/console/static/assets/erDiagram-JOGREHBK-9bUBZTRi.js +85 -0
  389. package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-Dw4L4RBa.js +156 -0
  390. package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-Jr82N_QP.js +292 -0
  391. package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-DAz9wFLr.js +106 -0
  392. package/dist/worker/console/static/assets/graph-DOmOIIwC.js +1 -0
  393. package/dist/worker/console/static/assets/index-B7Y-0g3J.css +1 -0
  394. package/dist/worker/console/static/assets/index-DkCrqaYg.js +325 -0
  395. package/dist/worker/console/static/assets/infoDiagram-6WML65LV-CBB5Np7y.js +2 -0
  396. package/dist/worker/console/static/assets/init-Gi6I4Gst.js +1 -0
  397. package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-D8IOp94y.js +70 -0
  398. package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-Dgd3AHGG.js +139 -0
  399. package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BmYOxLE9.js +89 -0
  400. package/dist/worker/console/static/assets/layout-D-LzfAck.js +1 -0
  401. package/dist/worker/console/static/assets/linear-DfYTEF6J.js +1 -0
  402. package/dist/worker/console/static/assets/map-DxJ2ADlA.js +1 -0
  403. package/dist/worker/console/static/assets/mermaid.core-BjuFPRqa.js +308 -0
  404. package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS-CLyUViGd.js +96 -0
  405. package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +1 -0
  406. package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-BvX2Di4-.js +1 -0
  407. package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-BFj0_OLv.js +39 -0
  408. package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-BIUrD_hx.js +7 -0
  409. package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-CHBB48fj.js +1 -0
  410. package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-sJrYw2Sf.js +84 -0
  411. package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-BzHH6fPR.js +40 -0
  412. package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-7QCnelCt.js +162 -0
  413. package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-sDYSxn72.js +1 -0
  414. package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-XpiUdwzm.js +1 -0
  415. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-eEhq2A8Y.js +1 -0
  416. package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-DvqgNPzD.js +2 -0
  417. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-la8AC7fo.js +8 -0
  418. package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-2TyOYNx0.js +120 -0
  419. package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-B-nM6wM2.js +34 -0
  420. package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-B86wPdbr.js +78 -0
  421. package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-Dr1C4AQz.js +7 -0
  422. package/dist/worker/console/static/favicon.svg +37 -0
  423. package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  424. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  425. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  426. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  427. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  428. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  429. package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  430. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  431. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  432. package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  433. package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  434. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  435. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  436. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  437. package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  438. package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  439. package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  440. package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  441. package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  442. package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  443. package/dist/worker/console/static/index.html +15 -0
  444. package/dist/worker/console/static-src/app/console-types.js +184 -0
  445. package/dist/worker/console/static-src/app/useConsoleShell.js +101 -0
  446. package/dist/worker/console/static-src/app/useOperatorActions.js +324 -0
  447. package/dist/worker/console/static-src/app/usePrdImport.js +172 -0
  448. package/dist/worker/console/static-src/app/useRecoveryActions.js +289 -0
  449. package/dist/worker/console/static-src/app/useRecoveryConsole.js +425 -0
  450. package/dist/worker/console/static-src/app/useTaskWizard.js +296 -0
  451. package/dist/worker/console/static-src/chat-markdown-security.js +38 -0
  452. package/dist/worker/console/static-src/chat-view-types.js +2 -0
  453. package/dist/worker/console/static-src/night/night-types.js +24 -0
  454. package/dist/worker/console/static-src/night/useNightBoard.js +94 -0
  455. package/dist/worker/console/static-src/night/useNightWizard.js +171 -0
  456. package/dist/worker/console/static-src/night-prepare-result.js +118 -0
  457. package/dist/worker/console/static-src/operator-chat/activity-journey.js +125 -0
  458. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +73 -0
  459. package/dist/worker/console/static-src/operator-chat/activity-references.js +20 -0
  460. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +420 -0
  461. package/dist/worker/console/static-src/operator-chat/format.js +71 -0
  462. package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
  463. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  464. package/dist/worker/console/static-src/operator-chat/refs.js +86 -0
  465. package/dist/worker/console/static-src/operator-chat/resource-auto-invocation.js +91 -0
  466. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  467. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  468. package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
  469. package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
  470. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +24 -0
  471. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +141 -0
  472. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +38 -0
  473. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +77 -0
  474. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +690 -0
  475. package/dist/worker/console/static-src/operator-chat/turn-submission.js +158 -0
  476. package/dist/worker/console/static-src/operator-chat/types.js +1 -0
  477. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +59 -0
  478. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +495 -0
  479. package/dist/worker/console/static-src/operator-chat/useChatStream.js +780 -0
  480. package/dist/worker/console/static-src/operator-chat/useChatThread.js +280 -0
  481. package/dist/worker/console/static-src/operator-chat/useComposer.js +257 -0
  482. package/dist/worker/console/static-src/operator-chat/useInterview.js +108 -0
  483. package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
  484. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +148 -0
  485. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +358 -0
  486. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  487. package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
  488. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +35 -0
  489. package/dist/worker/console/static-src/prd-file-import.js +218 -0
  490. package/dist/worker/console/vite.config.js +27 -0
  491. package/dist/worker/console/workflow-kinds.js +46 -0
  492. package/dist/worker/delivery/final-verification.js +302 -0
  493. package/dist/worker/delivery/git-transaction.js +497 -0
  494. package/dist/worker/delivery/package.js +537 -0
  495. package/dist/worker/delivery/verification-bundle.js +544 -0
  496. package/dist/worker/feature/acceptance-policy.js +227 -0
  497. package/dist/worker/feature/advance.js +301 -0
  498. package/dist/worker/feature/decision-loader.js +99 -0
  499. package/dist/worker/feature/discover.js +14 -0
  500. package/dist/worker/feature/doctor.js +223 -0
  501. package/dist/worker/feature/fullstack-validate.js +337 -0
  502. package/dist/worker/feature/next-action.js +140 -0
  503. package/dist/worker/feature/profile-schema.js +47 -0
  504. package/dist/worker/feature/ready-plan-projection.js +82 -0
  505. package/dist/worker/feature/reducer.js +137 -0
  506. package/dist/worker/feature/review.js +678 -0
  507. package/dist/worker/feature/run.js +390 -0
  508. package/dist/worker/feature/scaffold.js +812 -0
  509. package/dist/worker/feature/types.js +1 -0
  510. package/dist/worker/follow-up/approve.js +273 -0
  511. package/dist/worker/follow-up/factory.js +234 -0
  512. package/dist/worker/follow-up/paths.js +25 -0
  513. package/dist/worker/follow-up/policy.js +26 -0
  514. package/dist/worker/follow-up/schema.js +93 -0
  515. package/dist/worker/follow-up/store.js +96 -0
  516. package/dist/worker/loop-agent/command-result.js +1 -0
  517. package/dist/worker/loop-agent/controller-protocol.js +143 -0
  518. package/dist/worker/loop-agent/loop-agent-client.js +489 -0
  519. package/dist/worker/loop-agent/parse-json.js +14 -0
  520. package/dist/worker/materialize/harness-task-lifecycle-probe.js +126 -0
  521. package/dist/worker/materialize/harness-task-lineage.js +220 -0
  522. package/dist/worker/materialize/harness-task-materializer.js +648 -0
  523. package/dist/worker/metrics/projector.js +139 -0
  524. package/dist/worker/observability/dag-execution-trajectory.js +591 -0
  525. package/dist/worker/observability/event-history.js +216 -0
  526. package/dist/worker/observability/event-store.js +83 -0
  527. package/dist/worker/observability/events.js +79 -0
  528. package/dist/worker/observability/interrupt-eligibility.js +264 -0
  529. package/dist/worker/observability/progress-composite.js +34 -0
  530. package/dist/worker/observability/read-model.js +2499 -0
  531. package/dist/worker/observability/snapshot-store.js +43 -0
  532. package/dist/worker/observability/types.js +1 -0
  533. package/dist/worker/observe/dag-node-execution-output.js +180 -0
  534. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  535. package/dist/worker/observe/health.js +58 -0
  536. package/dist/worker/observe/night-jobs.js +104 -0
  537. package/dist/worker/observe/node-input.js +441 -0
  538. package/dist/worker/observe/paths.js +174 -0
  539. package/dist/worker/observe/routes.js +1077 -0
  540. package/dist/worker/observe/server.js +117 -0
  541. package/dist/worker/observe/spec-evidence.js +398 -0
  542. package/dist/worker/observe/static/api.js +117 -0
  543. package/dist/worker/observe/static/app.js +169 -0
  544. package/dist/worker/observe/static/constants.js +182 -0
  545. package/dist/worker/observe/static/copy.js +67 -0
  546. package/dist/worker/observe/static/custom-select.js +567 -0
  547. package/dist/worker/observe/static/dag-edge-routing.js +368 -0
  548. package/dist/worker/observe/static/dag-helpers.js +161 -0
  549. package/dist/worker/observe/static/dag-history-labels.js +95 -0
  550. package/dist/worker/observe/static/dag-layout.d.ts +36 -0
  551. package/dist/worker/observe/static/dag-layout.js +163 -0
  552. package/dist/worker/observe/static/dag-model.js +145 -0
  553. package/dist/worker/observe/static/dom.js +220 -0
  554. package/dist/worker/observe/static/favicon.svg +37 -0
  555. package/dist/worker/observe/static/format-pool.d.ts +71 -0
  556. package/dist/worker/observe/static/format-pool.js +153 -0
  557. package/dist/worker/observe/static/format.js +347 -0
  558. package/dist/worker/observe/static/index.html +409 -0
  559. package/dist/worker/observe/static/kpi.js +78 -0
  560. package/dist/worker/observe/static/markdown-render.js +124 -0
  561. package/dist/worker/observe/static/operator-chrome.css +517 -0
  562. package/dist/worker/observe/static/operator-chrome.d.ts +83 -0
  563. package/dist/worker/observe/static/operator-chrome.js +623 -0
  564. package/dist/worker/observe/static/relations.js +133 -0
  565. package/dist/worker/observe/static/router.js +156 -0
  566. package/dist/worker/observe/static/run-processing.js +148 -0
  567. package/dist/worker/observe/static/shell-chrome.js +65 -0
  568. package/dist/worker/observe/static/state.js +496 -0
  569. package/dist/worker/observe/static/styles.css +3567 -0
  570. package/dist/worker/observe/static/views/batch.js +227 -0
  571. package/dist/worker/observe/static/views/dag-graph.js +508 -0
  572. package/dist/worker/observe/static/views/dag-inspector.js +1722 -0
  573. package/dist/worker/observe/static/views/dag-trajectory.js +313 -0
  574. package/dist/worker/observe/static/views/dag.js +483 -0
  575. package/dist/worker/observe/static/views/dags.js +877 -0
  576. package/dist/worker/observe/static/views/dashboard.js +747 -0
  577. package/dist/worker/observe/static/views/failures.js +143 -0
  578. package/dist/worker/observe/static/views/feature.js +492 -0
  579. package/dist/worker/observe/static/views/night.js +201 -0
  580. package/dist/worker/observe/static/views/pool.js +708 -0
  581. package/dist/worker/observe/static/views/run.js +453 -0
  582. package/dist/worker/observe/static/views/session-timeline.js +771 -0
  583. package/dist/worker/observe/static/views/shell.js +7 -0
  584. package/dist/worker/observe/static/views/task.js +315 -0
  585. package/dist/worker/observe/static/views/timeline.js +163 -0
  586. package/dist/worker/outcomes/adapters.js +153 -0
  587. package/dist/worker/outcomes/declared-artifacts.js +103 -0
  588. package/dist/worker/outcomes/evidence-tokens.js +29 -0
  589. package/dist/worker/outcomes/gate.js +40 -0
  590. package/dist/worker/outcomes/projector.js +227 -0
  591. package/dist/worker/outcomes/registry.js +1 -0
  592. package/dist/worker/outcomes/store.js +131 -0
  593. package/dist/worker/outcomes/types.js +79 -0
  594. package/dist/worker/pool/attempt-identity.js +41 -0
  595. package/dist/worker/pool/attempt-lease.js +184 -0
  596. package/dist/worker/pool/attempt-transition.js +210 -0
  597. package/dist/worker/pool/begin-attempt-with-lease.js +26 -0
  598. package/dist/worker/pool/begin-attempt.js +35 -0
  599. package/dist/worker/pool/doctor.js +165 -0
  600. package/dist/worker/pool/failure-routing.js +182 -0
  601. package/dist/worker/pool/migrate-state.js +303 -0
  602. package/dist/worker/pool/reconcile.js +285 -0
  603. package/dist/worker/pool/recovery-decision.js +163 -0
  604. package/dist/worker/pool/run-owner-store.js +126 -0
  605. package/dist/worker/pool/run-store.js +360 -0
  606. package/dist/worker/pool/runtime-reconcile-inventory.js +127 -0
  607. package/dist/worker/pool/state-projection.js +57 -0
  608. package/dist/worker/pool/types.js +17 -0
  609. package/dist/worker/pool/validation.js +144 -0
  610. package/dist/worker/preflight.js +157 -0
  611. package/dist/worker/profile-mapping.js +76 -0
  612. package/dist/worker/progress-reporter.js +63 -0
  613. package/dist/worker/report/morning-report.js +155 -0
  614. package/dist/worker/repos/repo-resolver.js +23 -0
  615. package/dist/worker/run-task/execute-prepared-task.js +178 -0
  616. package/dist/worker/run-task/run-task.js +912 -0
  617. package/dist/worker/runner/run-ready.js +561 -0
  618. package/dist/worker/runner/single-task-attempt.js +176 -0
  619. package/dist/worker/scheduler/admission.js +547 -0
  620. package/dist/worker/scheduler/auto-followup.js +99 -0
  621. package/dist/worker/scheduler/cli.js +644 -0
  622. package/dist/worker/scheduler/clock-install/darwin-launchd.js +238 -0
  623. package/dist/worker/scheduler/clock-install/linux-systemd-user.js +224 -0
  624. package/dist/worker/scheduler/clock-install/types.js +1 -0
  625. package/dist/worker/scheduler/clock-install/win32-schtasks.js +227 -0
  626. package/dist/worker/scheduler/clock-install.js +284 -0
  627. package/dist/worker/scheduler/clock.js +420 -0
  628. package/dist/worker/scheduler/dispatcher.js +503 -0
  629. package/dist/worker/scheduler/doctor.js +431 -0
  630. package/dist/worker/scheduler/evidence.js +170 -0
  631. package/dist/worker/scheduler/git-base.js +90 -0
  632. package/dist/worker/scheduler/index.js +25 -0
  633. package/dist/worker/scheduler/lease.js +114 -0
  634. package/dist/worker/scheduler/lifecycle.js +348 -0
  635. package/dist/worker/scheduler/lock.js +80 -0
  636. package/dist/worker/scheduler/morning-window.js +191 -0
  637. package/dist/worker/scheduler/night-git-finalizer.js +114 -0
  638. package/dist/worker/scheduler/night-harvest.js +412 -0
  639. package/dist/worker/scheduler/paths.js +92 -0
  640. package/dist/worker/scheduler/prepared-attempt-recovery.js +471 -0
  641. package/dist/worker/scheduler/recovery.js +277 -0
  642. package/dist/worker/scheduler/reservation.js +146 -0
  643. package/dist/worker/scheduler/retry.js +199 -0
  644. package/dist/worker/scheduler/scheduler-loop.js +272 -0
  645. package/dist/worker/scheduler/store.js +275 -0
  646. package/dist/worker/scheduler/traceability.js +54 -0
  647. package/dist/worker/scheduler/trigger.js +258 -0
  648. package/dist/worker/scheduler/types.js +369 -0
  649. package/dist/worker/scheduler/workspace-adapter.js +128 -0
  650. package/dist/worker/task-graph/acceptance-schema.js +40 -0
  651. package/dist/worker/task-graph/ready-planner.js +267 -0
  652. package/dist/worker/task-graph/ready-queue.js +23 -0
  653. package/dist/worker/task-graph/task-graph-schema.js +59 -0
  654. package/dist/worker/task-graph/types.js +1 -0
  655. package/dist/worker/task-graph/validate.js +293 -0
  656. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  657. package/dist/worker/task-spec/schema.js +125 -0
  658. package/dist/worker/task-spec/types.js +1 -0
  659. package/dist/worker/task-spec/validate.js +391 -0
  660. package/dist/worker/task-spec/workflow-routing.js +149 -0
  661. package/dist/workflows/dag/authoring.js +8 -0
  662. package/dist/workflows/dag/authority-surface.js +138 -0
  663. package/dist/workflows/dag/backend-test-analysis-contract.js +507 -0
  664. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +1985 -0
  665. package/dist/workflows/dag/backend-test-case-manifest.js +638 -0
  666. package/dist/workflows/dag/backend-test-classification-contract.js +38 -0
  667. package/dist/workflows/dag/backend-test-contract-envelope.js +205 -0
  668. package/dist/workflows/dag/backend-test-coverage-contract.js +202 -0
  669. package/dist/workflows/dag/backend-test-execution-contract.js +715 -0
  670. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  671. package/dist/workflows/dag/backend-test-intake-context.js +178 -0
  672. package/dist/workflows/dag/backend-test-layout.js +133 -0
  673. package/dist/workflows/dag/backend-test-markdown-workflow.js +1984 -0
  674. package/dist/workflows/dag/backend-test-module-stem.js +26 -0
  675. package/dist/workflows/dag/backend-test-pytest-collection.js +859 -0
  676. package/dist/workflows/dag/backend-test-result-contract.js +1030 -0
  677. package/dist/workflows/dag/backend-test-scenario-param.js +1670 -0
  678. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  679. package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
  680. package/dist/workflows/dag/backend-test-stability-contract.js +57 -0
  681. package/dist/workflows/dag/backend-test-writer-completeness.js +1058 -0
  682. package/dist/workflows/dag/budget-enforcement.js +67 -0
  683. package/dist/workflows/dag/canvas-observer.js +474 -0
  684. package/dist/workflows/dag/context-policy.js +137 -0
  685. package/dist/workflows/dag/contract-output-registry.js +14 -0
  686. package/dist/workflows/dag/contract-validator-registrations.js +10 -0
  687. package/dist/workflows/dag/controller-identity.js +104 -0
  688. package/dist/workflows/dag/convergence/controller.js +596 -0
  689. package/dist/workflows/dag/decision-envelope.js +557 -0
  690. package/dist/workflows/dag/decision-evidence.js +153 -0
  691. package/dist/workflows/dag/decision-gates.js +1 -0
  692. package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
  693. package/dist/workflows/dag/dynamic-runtime/loop-until.js +161 -0
  694. package/dist/workflows/dag/dynamic-runtime/map.js +435 -0
  695. package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
  696. package/dist/workflows/dag/dynamic-runtime/shared.js +203 -0
  697. package/dist/workflows/dag/event-observer.js +132 -0
  698. package/dist/workflows/dag/executor-registry.js +23 -0
  699. package/dist/workflows/dag/facts.js +4 -0
  700. package/dist/workflows/dag/failure-category.js +118 -0
  701. package/dist/workflows/dag/failure-routing.js +105 -0
  702. package/dist/workflows/dag/final-verification.js +180 -0
  703. package/dist/workflows/dag/frontend-implementation-contract.js +1911 -0
  704. package/dist/workflows/dag/frontend-lint-baseline.js +474 -0
  705. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  706. package/dist/workflows/dag/frontend-prewrite-gate.js +1011 -0
  707. package/dist/workflows/dag/frontend-project-capability.js +1 -0
  708. package/dist/workflows/dag/frontend-recovery-plan.js +73 -0
  709. package/dist/workflows/dag/frontend-recovery-root-manifest.js +123 -0
  710. package/dist/workflows/dag/frontend-recovery-run.js +539 -0
  711. package/dist/workflows/dag/frontend-repair.js +541 -0
  712. package/dist/workflows/dag/frontend-review-context.js +116 -0
  713. package/dist/workflows/dag/frontend-risk.js +161 -0
  714. package/dist/workflows/dag/frontend-test-case-checklist.js +343 -0
  715. package/dist/workflows/dag/frontend-test-case-manifest.js +104 -0
  716. package/dist/workflows/dag/frontend-test-case-quality.js +105 -0
  717. package/dist/workflows/dag/frontend-test-html-report.js +174 -0
  718. package/dist/workflows/dag/frontend-test-l5-report.js +177 -0
  719. package/dist/workflows/dag/frontend-test-result-contract.js +487 -0
  720. package/dist/workflows/dag/frontend-verification-trace.js +275 -0
  721. package/dist/workflows/dag/frontend-worktree-diff.js +201 -0
  722. package/dist/workflows/dag/frontend-writer-recovery.js +106 -0
  723. package/dist/workflows/dag/frontend-writer-rollback.js +821 -0
  724. package/dist/workflows/dag/governance-constants.js +5 -0
  725. package/dist/workflows/dag/governance-profile.js +413 -0
  726. package/dist/workflows/dag/index.js +6 -0
  727. package/dist/workflows/dag/init-hybrid.js +7345 -0
  728. package/dist/workflows/dag/interrupt-request.js +559 -0
  729. package/dist/workflows/dag/knowledge-curator.js +165 -0
  730. package/dist/workflows/dag/l5-report-metrics.js +33 -0
  731. package/dist/workflows/dag/lifecycle.js +855 -0
  732. package/dist/workflows/dag/liveness-policy.js +251 -0
  733. package/dist/workflows/dag/node-execution.js +1081 -0
  734. package/dist/workflows/dag/observer-compose.js +52 -0
  735. package/dist/workflows/dag/output-protocol.js +356 -0
  736. package/dist/workflows/dag/project-governance-context.js +514 -0
  737. package/dist/workflows/dag/prompt-source.js +88 -0
  738. package/dist/workflows/dag/prompt.js +348 -0
  739. package/dist/workflows/dag/reconcile-run.js +145 -0
  740. package/dist/workflows/dag/reconcile-tasks.js +404 -0
  741. package/dist/workflows/dag/recovery-recommendation.js +329 -0
  742. package/dist/workflows/dag/repair-artifact.js +363 -0
  743. package/dist/workflows/dag/report.js +1229 -0
  744. package/dist/workflows/dag/rerun-plan.js +614 -0
  745. package/dist/workflows/dag/rerun-run.js +556 -0
  746. package/dist/workflows/dag/rerun-task.js +370 -0
  747. package/dist/workflows/dag/retry-policy.js +263 -0
  748. package/dist/workflows/dag/run-store.js +41 -0
  749. package/dist/workflows/dag/runner.js +1069 -0
  750. package/dist/workflows/dag/runtime-contract.js +87 -0
  751. package/dist/workflows/dag/runtime.js +5 -0
  752. package/dist/workflows/dag/scheduler.js +497 -0
  753. package/dist/workflows/dag/sdd-embedded.js +128 -0
  754. package/dist/workflows/dag/skill-instructions.js +478 -0
  755. package/dist/workflows/dag/skill-snapshot.js +553 -0
  756. package/dist/workflows/dag/skills.js +41 -0
  757. package/dist/workflows/dag/spec.js +3 -0
  758. package/dist/workflows/dag/task-contract-binding.js +138 -0
  759. package/dist/workflows/dag/task-demand-routing.js +1 -0
  760. package/dist/workflows/dag/topo.js +30 -0
  761. package/dist/workflows/dag/types.js +1145 -0
  762. package/dist/workflows/dag/upstream-artifacts.js +98 -0
  763. package/dist/workflows/dag/validate.js +998 -0
  764. package/dist/workflows/dag/workspace-checkpoint.js +186 -0
  765. package/dist/workflows/dynamic/artifacts.js +65 -0
  766. package/dist/workflows/dynamic/compile.js +371 -0
  767. package/dist/workflows/dynamic/compileTypes.js +1 -0
  768. package/dist/workflows/dynamic/errors.js +5 -0
  769. package/dist/workflows/dynamic/index.js +7 -0
  770. package/dist/workflows/dynamic/profiles.js +156 -0
  771. package/dist/workflows/dynamic/spec.js +115 -0
  772. package/dist/workflows/dynamic/validate.js +275 -0
  773. package/dist/workflows/loop/actions/dag-action.js +129 -0
  774. package/dist/workflows/loop/actions/pi-review.js +267 -0
  775. package/dist/workflows/loop/actions/shared.js +157 -0
  776. package/dist/workflows/loop/actions/shell-verify.js +82 -0
  777. package/dist/workflows/loop/actions/types.js +1 -0
  778. package/dist/workflows/loop/actions/workflow-action.js +255 -0
  779. package/dist/workflows/loop/actions.js +160 -0
  780. package/dist/workflows/loop/benchmark.js +510 -0
  781. package/dist/workflows/loop/closeout.js +135 -0
  782. package/dist/workflows/loop/context.js +47 -0
  783. package/dist/workflows/loop/events.js +26 -0
  784. package/dist/workflows/loop/hash.js +32 -0
  785. package/dist/workflows/loop/index.js +8 -0
  786. package/dist/workflows/loop/paths.js +17 -0
  787. package/dist/workflows/loop/policy/auto-policy.js +112 -0
  788. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  789. package/dist/workflows/loop/rounds.js +81 -0
  790. package/dist/workflows/loop/signals.js +52 -0
  791. package/dist/workflows/loop/state.js +116 -0
  792. package/dist/workflows/loop/templates.js +54 -0
  793. package/dist/workflows/loop/types.js +28 -0
  794. package/docs/README.md +82 -0
  795. package/docs/architecture/README.md +35 -0
  796. package/docs/architecture/dag-execution.md +165 -0
  797. package/docs/architecture/evolution.md +108 -0
  798. package/docs/architecture/facts-and-state.md +74 -0
  799. package/docs/architecture/runtime-boundaries.md +226 -0
  800. package/docs/architecture/system-overview.md +100 -0
  801. package/docs/architecture/worker-and-feature.md +122 -0
  802. package/docs/governance/README.md +18 -0
  803. package/docs/governance/harness-methodology-debugging.md +177 -0
  804. package/docs/governance/harness-methodology-tdd.md +130 -0
  805. package/docs/governance/harness-methodology-verification.md +27 -0
  806. package/docs/init-surface.manifest.json +358 -0
  807. package/docs/operations/README.md +15 -0
  808. package/docs/operations/local-development-environment.md +77 -0
  809. package/docs/skills/README.md +7 -0
  810. package/docs/skills/vetted-skill-registry.md +49 -0
  811. package/docs/templates/README.md +67 -0
  812. package/docs/templates/adr.md +60 -0
  813. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
  814. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
  815. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
  816. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
  817. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
  818. package/docs/templates/agent-dag-report.schema.json +473 -0
  819. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
  820. package/docs/templates/agent-dag.base.json +190 -0
  821. package/docs/templates/agent-dag.final-verification.json +185 -0
  822. package/docs/templates/agent-dag.schema.json +590 -0
  823. package/docs/templates/agent-dag.supervised-implementation.json +640 -0
  824. package/docs/templates/agent-worker-production-readiness-checklist.md +47 -0
  825. package/docs/templates/backend-test-analysis.schema.json +37 -0
  826. package/docs/templates/backend-test-case-manifest.schema.json +223 -0
  827. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  828. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +41 -0
  829. package/docs/templates/backend-test-dag.json +671 -0
  830. package/docs/templates/backend-test-dag.retrospect.prompt.md +165 -0
  831. package/docs/templates/backend-test-dag.review-cases.prompt.md +36 -0
  832. package/docs/templates/backend-test-execution.schema.json +138 -0
  833. package/docs/templates/backend-test-result.schema.json +99 -0
  834. package/docs/templates/branch-merge-report.md +115 -0
  835. package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
  836. package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
  837. package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
  838. package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
  839. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  840. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  841. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  842. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  843. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  844. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  845. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  846. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  847. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  848. package/docs/templates/exec-plan.md +66 -0
  849. package/docs/templates/feature-spec.md +53 -0
  850. package/docs/templates/frontend-design-contract.md +68 -0
  851. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  852. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  853. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  854. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  855. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  856. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  857. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  858. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  859. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  860. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  861. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  862. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  863. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  864. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  865. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  866. package/docs/templates/frontend-eval/metrics.md +138 -0
  867. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  868. package/docs/templates/frontend-implementation-contract.schema.json +35 -0
  869. package/docs/templates/frontend-task-constraints.md +62 -0
  870. package/docs/templates/frontend-task-requirement.md +104 -0
  871. package/docs/templates/frontend-test-case-checklist.md +44 -0
  872. package/docs/templates/frontend-test-case.example.md +143 -0
  873. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +63 -0
  874. package/docs/templates/frontend-test-dag.json +409 -0
  875. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +21 -0
  876. package/docs/templates/frontend-test-dag.retrospect.prompt.md +5 -0
  877. package/docs/templates/frontend-test-dag.review-cases.prompt.md +5 -0
  878. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  879. package/docs/templates/frontend-test-standard-scenarios.v1.json +159 -0
  880. package/docs/templates/harness.schema.json +351 -0
  881. package/docs/templates/hybrid-dag.json +188 -0
  882. package/docs/templates/init-evolution-review.md +35 -0
  883. package/docs/templates/init-managed-agents.md +157 -0
  884. package/docs/templates/interactive-ui-round2-experiment.md +66 -0
  885. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
  886. package/docs/templates/knowledge-sync-dag.json +178 -0
  887. package/docs/templates/knowledge-sync-draft.schema.json +71 -0
  888. package/docs/templates/product-line/AGENTS.md +9 -0
  889. package/docs/templates/product-line/README.md +42 -0
  890. package/docs/templates/product-line/acceptance.yaml +23 -0
  891. package/docs/templates/product-line/closeout.yaml +9 -0
  892. package/docs/templates/product-line/design.md +13 -0
  893. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  894. package/docs/templates/product-line/feature.yaml +11 -0
  895. package/docs/templates/product-line/links.md +10 -0
  896. package/docs/templates/product-line/requirement.md +17 -0
  897. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  898. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +16 -0
  899. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  900. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +14 -0
  901. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  902. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  903. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  904. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +18 -0
  905. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  906. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +14 -0
  907. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  908. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  909. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  910. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  911. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  912. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +16 -0
  913. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  914. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +14 -0
  915. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  916. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  917. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  918. package/docs/templates/product-line/task-graph.yaml +23 -0
  919. package/docs/templates/product-line/task.yaml +68 -0
  920. package/docs/templates/product-line/test-plan.md +7 -0
  921. package/docs/templates/production-readiness-checklist.md +57 -0
  922. package/docs/templates/progress-log.md +27 -0
  923. package/docs/templates/project-start-checklist.md +9 -0
  924. package/docs/templates/qa-report.md +48 -0
  925. package/docs/templates/sprint-contract.md +29 -0
  926. package/docs/templates/worker-dogfood-evidence.md +80 -0
  927. package/docs/templates/worker-dogfood-setup.md +68 -0
  928. package/examples/decision-gate-agent-dag.json +177 -0
  929. package/examples/example-dag.json +46 -0
  930. package/examples/hybrid-loop-agent-dag.json +189 -0
  931. package/examples/l5-report-coms-process-definition.html +322 -0
  932. package/examples/l5-report-demo.html +79 -0
  933. package/harness.json +90 -0
  934. package/package.json +98 -0
  935. package/scripts/kb-bootstrap-init-skeleton.sh +241 -0
  936. package/scripts/kb-graph-incremental-prepare.mjs +386 -0
  937. package/scripts/kb-graph-materialize.mjs +105 -0
  938. package/scripts/kb-graph-promote.mjs +164 -0
  939. package/scripts/kb-query.mjs +554 -0
  940. package/skills/agent-worker/SKILL.md +48 -0
  941. package/skills/agent-worker/references/agent-worker-operator.md +159 -0
  942. package/skills/ai-engineering-context/SKILL.md +48 -0
  943. package/skills/analyze-product-dependencies/SKILL.md +108 -0
  944. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +35 -0
  945. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +38 -0
  946. package/skills/analyze-product-dependencies/references/example.md +76 -0
  947. package/skills/analyze-product-dependencies/references/forward-test-cases.md +110 -0
  948. package/skills/analyze-product-dependencies/references/input-contract.md +34 -0
  949. package/skills/analyze-product-dependencies/references/kb-integration.md +64 -0
  950. package/skills/analyze-product-dependencies/references/scouting-rules.md +76 -0
  951. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +460 -0
  952. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +194 -0
  953. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +200 -0
  954. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +90 -0
  955. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +138 -0
  956. package/skills/analyze-product-requirements/SKILL.md +145 -0
  957. package/skills/analyze-product-requirements/references/acceptance-criteria.md +87 -0
  958. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +70 -0
  959. package/skills/analyze-product-requirements/references/example.md +104 -0
  960. package/skills/analyze-product-requirements/references/forward-test-cases.md +144 -0
  961. package/skills/analyze-product-requirements/references/kb-integration.md +56 -0
  962. package/skills/analyze-product-requirements/references/product-analysis-schema.md +41 -0
  963. package/skills/analyze-product-requirements/references/product-requirement-schema.md +42 -0
  964. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +66 -0
  965. package/skills/analyze-product-requirements/scripts/compute-source-identity.mjs +35 -0
  966. package/skills/analyze-product-requirements/scripts/test-validators.mjs +501 -0
  967. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +100 -0
  968. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +109 -0
  969. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +109 -0
  970. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +175 -0
  971. package/skills/browser-tools/SKILL.md +196 -0
  972. package/skills/browser-tools/browser-content.js +103 -0
  973. package/skills/browser-tools/browser-cookies.js +35 -0
  974. package/skills/browser-tools/browser-eval.js +53 -0
  975. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  976. package/skills/browser-tools/browser-nav.js +44 -0
  977. package/skills/browser-tools/browser-pick.js +162 -0
  978. package/skills/browser-tools/browser-screenshot.js +34 -0
  979. package/skills/browser-tools/browser-start.js +86 -0
  980. package/skills/browser-tools/package-lock.json +2556 -0
  981. package/skills/browser-tools/package.json +19 -0
  982. package/skills/code-review-core/SKILL.md +20 -0
  983. package/skills/codebase-scout/SKILL.md +19 -0
  984. package/skills/frontend-bounded-implement/SKILL.md +53 -0
  985. package/skills/frontend-design-review/SKILL.md +79 -0
  986. package/skills/frontend-design-review/references/review-checklist.md +43 -0
  987. package/skills/frontend-implementation/SKILL.md +52 -0
  988. package/skills/frontend-implementation/references/code-standards.md +33 -0
  989. package/skills/frontend-implementation/references/design-spec.md +56 -0
  990. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  991. package/skills/frontend-review/SKILL.md +55 -0
  992. package/skills/frontend-review/references/review-findings.md +50 -0
  993. package/skills/frontend-verification/SKILL.md +61 -0
  994. package/skills/frontend-verification/references/verification-checklist.md +48 -0
  995. package/skills/grill-me/SKILL.md +10 -0
  996. package/skills/grill-with-docs/SKILL.md +80 -0
  997. package/skills/grill-with-docs/adr-format.md +58 -0
  998. package/skills/grill-with-docs/context-format.md +52 -0
  999. package/skills/init-capability-evolution/SKILL.md +70 -0
  1000. package/skills/local-jacoco-coverage/SKILL.md +281 -0
  1001. package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
  1002. package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
  1003. package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
  1004. package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
  1005. package/skills/loop-agent/SKILL.md +68 -0
  1006. package/skills/loop-agent/references/README.md +67 -0
  1007. package/skills/loop-agent/references/command-reference.md +645 -0
  1008. package/skills/loop-agent/references/docs-converge.md +126 -0
  1009. package/skills/loop-agent/references/harness-policy.md +289 -0
  1010. package/skills/loop-agent/references/hybrid-dag.md +245 -0
  1011. package/skills/loop-agent/references/learned/README.md +21 -0
  1012. package/skills/loop-agent/references/long-running-loop.md +57 -0
  1013. package/skills/loop-agent/references/model-routing.md +45 -0
  1014. package/skills/loop-agent/references/multi-worktree.md +54 -0
  1015. package/skills/loop-agent/references/one-shot-runs.md +84 -0
  1016. package/skills/loop-agent/references/orchestrator-and-interventions.md +175 -0
  1017. package/skills/loop-agent/references/pi-prompt.md +23 -0
  1018. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -0
  1019. package/skills/loop-agent/references/post-implementation-and-patterns.md +54 -0
  1020. package/skills/loop-agent/references/source-and-plan-practice.md +169 -0
  1021. package/skills/loop-agent/references/task-workflow.md +104 -0
  1022. package/skills/loop-agent/references/verification-and-failure-handling.md +148 -0
  1023. package/skills/playwright-cli/SKILL.md +93 -0
  1024. package/skills/playwright-cli/references/element-attributes.md +23 -0
  1025. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  1026. package/skills/playwright-cli/references/request-mocking.md +87 -0
  1027. package/skills/playwright-cli/references/running-code.md +241 -0
  1028. package/skills/playwright-cli/references/session-management.md +225 -0
  1029. package/skills/playwright-cli/references/storage-state.md +275 -0
  1030. package/skills/playwright-cli/references/test-generation.md +433 -0
  1031. package/skills/playwright-cli/references/tracing.md +5 -0
  1032. package/skills/playwright-cli/references/video-recording.md +5 -0
  1033. package/skills/playwright-cli-case-generator/SKILL.md +108 -0
  1034. package/skills/requesting-code-review/SKILL.md +101 -0
  1035. package/skills/requesting-code-review/code-reviewer.md +168 -0
  1036. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  1037. package/skills/systematic-debugging/SKILL.md +312 -0
  1038. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  1039. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  1040. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  1041. package/skills/systematic-debugging/find-polluter.sh +63 -0
  1042. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  1043. package/skills/systematic-debugging/test-academic.md +14 -0
  1044. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  1045. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  1046. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  1047. package/skills/test-driven-development/SKILL.md +27 -0
  1048. package/skills/using-git-worktrees/SKILL.md +215 -0
  1049. package/skills/verification-before-completion/SKILL.md +154 -0
  1050. package/skills/webapp-testing/SKILL.md +19 -0
@@ -0,0 +1,3137 @@
1
+ /**
2
+ * Operator Chat — HTTP route handlers (plan W7 / W8 backend).
3
+ *
4
+ * Routes (all under /api/operator/v1/chat):
5
+ * POST /sessions — create a new Chat session
6
+ * GET /sessions/:id — fetch session metadata + messages
7
+ * POST /sessions/:id/prompt — SSE stream: send a prompt, stream events
8
+ * GET /sessions/:id/events — SSE stream: (re)connect to live events
9
+ * GET /capabilities — Chat tool surface
10
+ * DELETE /sessions/:id — dispose a Chat session
11
+ *
12
+ * Mutation routes go through the Console mutation gate (boot token + Origin).
13
+ * Read routes (GET capabilities / sessions) are open to loopback.
14
+ */
15
+ import { parseAssistantContent } from "./assistant-content.js";
16
+ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
17
+ import { buildModelCallableToolSchemas } from "./tool-adapter.js";
18
+ import { cleanChatTitle, acceptedOperationIdFromResult } from "./session-store.js";
19
+ import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
20
+ import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
21
+ import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
22
+ import { ComposerDraftStore } from "./composer-draft-store.js";
23
+ import { walkRepoFiles } from "./repo-walk.js";
24
+ import { isSensitivePath, scrubSecrets } from "./explore-tools.js";
25
+ import { listRepoDirectory, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
26
+ import { projectRuntimeContext } from "./runtime-context.js";
27
+ import { patchModelsConfig, readModelsConfig, readPackageInventory, } from "./pi-console-config.js";
28
+ import { ChatInterviewAdapter, projectChatInterviewState, } from "./interview-adapter.js";
29
+ import { classifyChatCompactionFailure } from "./compaction-errors.js";
30
+ import { evaluateMutationGate, isMutationMethod } from "../security.js";
31
+ import { dispatchOperatorAction } from "../operator-actions.js";
32
+ import { makeHumanGateCard, } from "./human-gate-card.js";
33
+ import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
34
+ import { projectTaskContext } from "./context-panel.js";
35
+ import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
36
+ import { sendJson } from "../routes.js";
37
+ import { openSseResponse, writeSseEvent } from "../operation-sse.js";
38
+ function turnSummary(turn) {
39
+ return {
40
+ turnId: turn.turnId,
41
+ clientRequestId: turn.clientRequestId ?? null,
42
+ state: turn.state,
43
+ ordinal: turn.ordinal,
44
+ createdAt: turn.createdAt,
45
+ startedAt: turn.startedAt ?? null,
46
+ finishedAt: turn.finishedAt ?? null,
47
+ error: turn.error ?? null,
48
+ abortRequestedAt: turn.abortRequestedAt ?? null,
49
+ };
50
+ }
51
+ function isTerminalTurnState(state) {
52
+ return state === "settled" || state === "aborted" || state === "failed";
53
+ }
54
+ const DEFAULT_ABORT_WAIT_MS = 2_500;
55
+ /** Parse Chat's `<epoch>:<seq>` SSE cursor. Legacy `<sessionId>:<seq>` has
56
+ * the same wire shape; its prefix intentionally fails the current process
57
+ * epoch comparison and triggers safe snapshot recovery (M5.1). */
58
+ function parseChatEventCursor(header) {
59
+ if (!header?.trim())
60
+ return { epoch: null, seq: 0 };
61
+ const raw = header.trim();
62
+ const colon = raw.lastIndexOf(":");
63
+ const seqPart = colon >= 0 ? raw.slice(colon + 1) : raw;
64
+ const seq = Number.parseInt(seqPart, 10);
65
+ return {
66
+ epoch: colon > 0 ? raw.slice(0, colon) : null,
67
+ seq: Number.isFinite(seq) && seq >= 0 ? seq : 0,
68
+ };
69
+ }
70
+ function writeChatSse(res, event) {
71
+ // Persisted Chat events carry their own eventId (`<epoch>:<seq>`); write
72
+ // it as the SSE `id:` frame so the browser tracks Last-Event-ID for reconnect.
73
+ if ("seq" in event && event.eventId) {
74
+ res.write(`id: ${event.eventId}\n`);
75
+ }
76
+ res.write(`event: ${event.kind}\n`);
77
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
78
+ }
79
+ /** Map a runtime ChatTurnEvent to the persisted event-store partial.
80
+ *
81
+ * Returns undefined for heartbeat (not persisted — keep-alive only). */
82
+ function turnEventToStorePartial(event) {
83
+ switch (event.type) {
84
+ case "agent_start":
85
+ return { kind: "agent_start", data: {} };
86
+ case "message_update":
87
+ case "message_end": {
88
+ const content = parseAssistantContent(event.content);
89
+ return {
90
+ kind: event.type,
91
+ data: {
92
+ text: String(event.text ?? ""),
93
+ ...(content ? { content } : {}),
94
+ },
95
+ };
96
+ }
97
+ case "usage":
98
+ return { kind: "usage", data: (event.usage ?? {}) };
99
+ case "tool_call":
100
+ return {
101
+ kind: "tool_call",
102
+ data: projectChatToolEventPayload({
103
+ kind: "tool_call",
104
+ toolCallId: String(event.toolCallId ?? ""),
105
+ toolName: String(event.toolName ?? ""),
106
+ value: event.args,
107
+ }),
108
+ };
109
+ case "tool_result": {
110
+ // Plan A (2026-08-16): surface the accepted-operation link on the live
111
+ // SSE frame so the frontend semantic event can resolve operation:<id>.
112
+ const resultOperationId = acceptedOperationIdFromResult(String(event.toolName ?? ""), event.result);
113
+ return {
114
+ kind: "tool_result",
115
+ data: projectChatToolEventPayload({
116
+ kind: "tool_result",
117
+ toolCallId: String(event.toolCallId ?? ""),
118
+ toolName: String(event.toolName ?? ""),
119
+ value: event.result,
120
+ isError: Boolean(event.isError),
121
+ ...(resultOperationId ? { operationId: resultOperationId } : {}),
122
+ }),
123
+ };
124
+ }
125
+ case "agent_end":
126
+ return {
127
+ kind: "agent_end",
128
+ data: { willRetry: Boolean(event.willRetry) },
129
+ };
130
+ case "agent_settled":
131
+ return { kind: "agent_settled", data: {} };
132
+ case "error":
133
+ return { kind: "error", data: { message: String(event.message ?? "") } };
134
+ default:
135
+ return undefined;
136
+ }
137
+ }
138
+ function emitSessionTitle(deps, sessionId, turnId, title, automatic = false) {
139
+ return deps.events.append(sessionId, turnId, {
140
+ kind: "session_title",
141
+ data: { title, ...(automatic ? { automatic: true } : {}) },
142
+ });
143
+ }
144
+ function shouldEmitPlaceholder(record) {
145
+ return !record.title &&
146
+ record.messages.filter((message) => message.role === "user").length === 1
147
+ ? record.displayTitle
148
+ : undefined;
149
+ }
150
+ function scheduleAutomaticTitle(deps, input) {
151
+ void (async () => {
152
+ try {
153
+ const generated = await deps.runtime.generateTitle(input.sessionId, input.firstUserText);
154
+ const title = generated ? cleanChatTitle(generated) : undefined;
155
+ if (!title)
156
+ return;
157
+ const stored = await deps.store.setAutomaticTitle(input.sessionId, title);
158
+ if (!stored)
159
+ return;
160
+ emitSessionTitle(deps, input.sessionId, input.turnId, stored.title, true);
161
+ // Keep the best-effort Pi mirror inside this detached task's catch
162
+ // boundary: both synchronous throws and rejected Promises are absorbed
163
+ // only after durable title/event persistence has completed.
164
+ await deps.runtime.setSessionName(input.sessionId, stored.title);
165
+ }
166
+ catch {
167
+ // Background title failures must never affect the settled Chat turn.
168
+ }
169
+ })();
170
+ }
171
+ /** Guarantee durable terminal events for a turn that ended without the SDK
172
+ * emitting them (client abort, pre-prompt persistence failure, runtime
173
+ * error). Without `agent_end`, reconnect replay stops mid-turn; without
174
+ * `agent_settled`, the single-stream UI keeps idle-grace streaming forever
175
+ * (M4.4). Idempotent for normal settled turns that already have both. */
176
+ function ensureAgentEndEvent(deps, sessionId, turnId) {
177
+ const snap = deps.events.snapshot(sessionId);
178
+ const hasAgentEnd = snap.some((event) => event.turnId === turnId && event.kind === "agent_end");
179
+ if (!hasAgentEnd) {
180
+ deps.events.append(sessionId, turnId, {
181
+ kind: "agent_end",
182
+ data: { willRetry: false },
183
+ });
184
+ }
185
+ // Error/abort paths historically only forced agent_end. After the M4 single
186
+ // stream keeps UI streaming through agent_end for idle grace, a matching
187
+ // agent_settled is required so state reconcile and the applier both finish.
188
+ const hasSettled = deps.events
189
+ .snapshot(sessionId)
190
+ .some((event) => event.turnId === turnId && event.kind === "agent_settled");
191
+ if (!hasSettled) {
192
+ deps.events.append(sessionId, turnId, {
193
+ kind: "agent_settled",
194
+ data: {},
195
+ });
196
+ }
197
+ }
198
+ async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
199
+ const chunks = [];
200
+ let total = 0;
201
+ for await (const chunk of req) {
202
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
203
+ total += buf.length;
204
+ if (total > maxBytes) {
205
+ throw new Error(`request body exceeds ${maxBytes} bytes`);
206
+ }
207
+ chunks.push(buf);
208
+ }
209
+ if (chunks.length === 0)
210
+ return {};
211
+ const raw = Buffer.concat(chunks).toString("utf8");
212
+ if (!raw.trim())
213
+ return {};
214
+ try {
215
+ return JSON.parse(raw);
216
+ }
217
+ catch (error) {
218
+ // Preserve throw semantics: callers map parse failures to
219
+ // INVALID_INPUT / 400. The explicit catch keeps malformed JSON from
220
+ // surfacing as an unhandled SyntaxError.
221
+ throw error;
222
+ }
223
+ }
224
+ const IMAGE_MIME_TYPES = new Set([
225
+ "image/png",
226
+ "image/jpeg",
227
+ "image/gif",
228
+ "image/webp",
229
+ ]);
230
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
231
+ const MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024;
232
+ const MAX_IMAGES = 4;
233
+ export function parseChatImages(value) {
234
+ if (value === undefined)
235
+ return [];
236
+ if (!Array.isArray(value) || value.length > MAX_IMAGES)
237
+ throw new Error(`images must be an array with at most ${MAX_IMAGES} items`);
238
+ let total = 0;
239
+ return value.map((item, index) => {
240
+ if (!item || typeof item !== "object")
241
+ throw new Error(`images[${index}] is invalid`);
242
+ const record = item;
243
+ if (record.type !== "image" ||
244
+ typeof record.mimeType !== "string" ||
245
+ !IMAGE_MIME_TYPES.has(record.mimeType))
246
+ throw new Error(`images[${index}] must be a supported raster image`);
247
+ if (typeof record.data !== "string" ||
248
+ !record.data ||
249
+ !/^[A-Za-z0-9+/]+={0,2}$/.test(record.data) ||
250
+ record.data.length % 4 !== 0)
251
+ throw new Error(`images[${index}] data must be valid base64`);
252
+ const bytes = Buffer.from(record.data, "base64");
253
+ const textProbe = bytes
254
+ .subarray(0, Math.min(bytes.length, 64 * 1024))
255
+ .toString("utf8");
256
+ if (scrubSecrets(textProbe).redactions.length > 0)
257
+ throw new Error(`images[${index}] contains secret-shaped content`);
258
+ if (bytes.length > MAX_IMAGE_BYTES)
259
+ throw new Error(`images[${index}] exceeds ${MAX_IMAGE_BYTES} bytes`);
260
+ total += bytes.length;
261
+ if (total > MAX_TOTAL_IMAGE_BYTES)
262
+ throw new Error(`images exceed ${MAX_TOTAL_IMAGE_BYTES} total bytes`);
263
+ return {
264
+ type: "image",
265
+ mimeType: record.mimeType,
266
+ data: bytes.toString("base64"),
267
+ };
268
+ });
269
+ }
270
+ function gateMutation(req, deps) {
271
+ const failure = evaluateMutationGate(req, {
272
+ bootToken: deps.bootToken.value,
273
+ confirmationToken: deps.bootToken.confirmationToken,
274
+ consoleOrigin: deps.getConsoleOrigin(),
275
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
276
+ });
277
+ if (failure) {
278
+ return {
279
+ ok: false,
280
+ status: failure.status,
281
+ code: failure.code,
282
+ message: failure.message,
283
+ };
284
+ }
285
+ return { ok: true };
286
+ }
287
+ /**
288
+ * RF-02: map a RuntimeSnapshotUnavailableError (critical snapshot projection
289
+ * failure — model enumeration / system prompt) to 503 PI_RUNTIME_UNAVAILABLE.
290
+ * Returns true when the response was sent; other errors keep their existing
291
+ * 409/500 branches so the mutation/reload semantics never regress.
292
+ */
293
+ function sendRuntimeUnavailable(res, error) {
294
+ if (!(error instanceof RuntimeSnapshotUnavailableError))
295
+ return false;
296
+ sendJson(res, 503, {
297
+ ok: false,
298
+ error: { code: error.code, message: error.message },
299
+ });
300
+ return true;
301
+ }
302
+ /**
303
+ * Map a failed `reopenSession()` result to HTTP status + response error code.
304
+ * Must stay aligned with `ensureActiveSession` reopen-failure branches so
305
+ * lazy-reopen routes (prompt/turns/reopen/compact/branch) and readiness
306
+ * endpoints share one semantic table.
307
+ *
308
+ * Business conflicts (TURN_ACTIVE, PI_SESSION_BUSY, model switch, …) are NOT
309
+ * handled here — only reopen/init readiness failures.
310
+ */
311
+ function mapReopenSessionFailure(reopen) {
312
+ if (reopen.code === "NO_SESSION_FILE") {
313
+ return {
314
+ status: 404,
315
+ code: "PI_SESSION_NOT_FOUND",
316
+ message: reopen.message,
317
+ };
318
+ }
319
+ if (reopen.code === "PI_SESSION_INIT_FAILED") {
320
+ return {
321
+ status: 503,
322
+ code: "PI_SESSION_INIT_FAILED",
323
+ message: reopen.message,
324
+ };
325
+ }
326
+ return {
327
+ status: 503,
328
+ code: "PI_RUNTIME_UNAVAILABLE",
329
+ message: reopen.message,
330
+ };
331
+ }
332
+ export async function handleChatCapabilities(_req, res, deps) {
333
+ const toolSchemas = buildModelCallableToolSchemas();
334
+ sendJson(res, 200, {
335
+ ok: true,
336
+ schemaVersion: 1,
337
+ allowedTools: OPERATOR_CHAT_ALLOWED_TOOLS,
338
+ deniedTools: OPERATOR_CHAT_DENIED_TOOLS,
339
+ deniedOperatorActions: OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS,
340
+ tools: toolSchemas,
341
+ // ADR 0011: full Pi tools including bash are default-active.
342
+ hasBash: true,
343
+ });
344
+ }
345
+ export async function handleCreateChatSession(req, res, deps) {
346
+ const gate = gateMutation(req, deps);
347
+ if (!gate.ok) {
348
+ sendJson(res, gate.status, {
349
+ ok: false,
350
+ error: { code: gate.code, message: gate.message },
351
+ });
352
+ return;
353
+ }
354
+ let body;
355
+ try {
356
+ body = await readJsonBody(req);
357
+ }
358
+ catch (error) {
359
+ sendJson(res, 400, {
360
+ ok: false,
361
+ error: {
362
+ code: "INVALID_INPUT",
363
+ message: error instanceof Error ? error.message : String(error),
364
+ },
365
+ });
366
+ return;
367
+ }
368
+ if ("clientRequestId" in body || "requestIndex" in body) {
369
+ sendJson(res, 400, {
370
+ ok: false,
371
+ error: {
372
+ code: "INVALID_INPUT",
373
+ message: "operation idempotency ownership cannot be copied",
374
+ },
375
+ });
376
+ return;
377
+ }
378
+ const provider = typeof body.modelProvider === "string" ? body.modelProvider : undefined;
379
+ const modelId = typeof body.modelId === "string" ? body.modelId : undefined;
380
+ const snapshotInput = body.snapshot;
381
+ if (snapshotInput !== undefined &&
382
+ (!snapshotInput ||
383
+ typeof snapshotInput !== "object" ||
384
+ snapshotInput.schemaVersion !== 1 ||
385
+ typeof snapshotInput.summary !== "string" ||
386
+ !snapshotInput.summary)) {
387
+ sendJson(res, 400, {
388
+ ok: false,
389
+ error: {
390
+ code: "INVALID_SNAPSHOT",
391
+ message: "valid OperatorContextSnapshotV1 is required",
392
+ },
393
+ });
394
+ return;
395
+ }
396
+ try {
397
+ const snapshot = snapshotInput === undefined ? undefined : projectCompactSnapshot(snapshotInput);
398
+ // Fast path: durable shell + Console record before full AgentSession ready.
399
+ const handle = await deps.runtime.createSession({
400
+ ...(provider && modelId ? { model: { provider, modelId } } : {}),
401
+ ...(snapshot
402
+ ? {
403
+ systemPromptSuffix: `Read-only initial Operator context snapshot; this is not runtime rollback.\n${snapshot.summary}`,
404
+ }
405
+ : {}),
406
+ });
407
+ if (!handle.sessionFile) {
408
+ deps.runtime.dispose(handle.sessionId);
409
+ sendJson(res, 500, {
410
+ ok: false,
411
+ error: {
412
+ code: "CHAT_SESSION_CREATE_FAILED",
413
+ message: "chat session shell missing sessionFile",
414
+ },
415
+ });
416
+ return;
417
+ }
418
+ const session = await deps.store.create({
419
+ sessionId: handle.sessionId,
420
+ sessionFile: handle.sessionFile,
421
+ modelProvider: handle.model?.provider,
422
+ modelId: handle.model?.modelId,
423
+ });
424
+ if (snapshot)
425
+ deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
426
+ kind: "compact",
427
+ data: snapshot,
428
+ });
429
+ // Best-effort: after materialization, patch resolved default model onto the
430
+ // durable record. Failures stay on the init fence (PI_SESSION_INIT_FAILED).
431
+ if (typeof deps.runtime.ensureSessionReady === "function") {
432
+ void deps.runtime
433
+ .ensureSessionReady(handle.sessionId)
434
+ .then(async () => {
435
+ if (typeof deps.runtime.getRuntimeSelection !== "function")
436
+ return;
437
+ const selection = deps.runtime.getRuntimeSelection(handle.sessionId);
438
+ if (!selection?.model)
439
+ return;
440
+ await deps.store.setRuntimeSelection(handle.sessionId, {
441
+ modelProvider: selection.model.provider,
442
+ modelId: selection.model.modelId,
443
+ });
444
+ })
445
+ .catch(() => undefined);
446
+ }
447
+ sendJson(res, 201, {
448
+ ok: true,
449
+ sessionId: handle.sessionId,
450
+ sessionFile: handle.sessionFile,
451
+ model: handle.model,
452
+ session,
453
+ activeTools: handle.activeTools,
454
+ hasBash: true,
455
+ ...(handle.initializing ? { initializing: true } : {}),
456
+ });
457
+ }
458
+ catch (error) {
459
+ const message = error instanceof Error ? error.message : String(error);
460
+ sendJson(res, 500, {
461
+ ok: false,
462
+ error: { code: "CHAT_SESSION_CREATE_FAILED", message },
463
+ });
464
+ }
465
+ }
466
+ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
467
+ const gate = gateMutation(req, deps);
468
+ if (!gate.ok) {
469
+ sendJson(res, gate.status, {
470
+ ok: false,
471
+ error: { code: gate.code, message: gate.message },
472
+ });
473
+ return;
474
+ }
475
+ if (deps.events.getActiveTurn(sourceSessionId)) {
476
+ sendJson(res, 409, {
477
+ ok: false,
478
+ error: { code: "TURN_ACTIVE", message: "cannot fork an active turn" },
479
+ });
480
+ return;
481
+ }
482
+ const source = await deps.store.get(sourceSessionId);
483
+ if (!source) {
484
+ sendJson(res, 404, {
485
+ ok: false,
486
+ error: {
487
+ code: "NOT_FOUND",
488
+ message: `chat session not found: ${sourceSessionId}`,
489
+ },
490
+ });
491
+ return;
492
+ }
493
+ try {
494
+ const handle = await deps.runtime.forkSession({
495
+ sourceSessionFile: source.sessionFile ?? "",
496
+ ...(source.modelProvider && source.modelId
497
+ ? { model: { provider: source.modelProvider, modelId: source.modelId } }
498
+ : {}),
499
+ });
500
+ await deps.store.forkFrom({
501
+ sourceSessionId,
502
+ targetSessionId: handle.sessionId,
503
+ targetSessionFile: handle.sessionFile,
504
+ });
505
+ const copied = deps.events.forkContextRefs({
506
+ sourceSessionId,
507
+ targetSessionId: handle.sessionId,
508
+ targetTurnId: `${handle.sessionId}:fork`,
509
+ });
510
+ await deps.operationLinker.recoverSession(handle.sessionId);
511
+ sendJson(res, 201, {
512
+ ok: true,
513
+ sessionId: handle.sessionId,
514
+ forkedFrom: sourceSessionId,
515
+ copiedContextRefs: copied.copiedCount,
516
+ warnings: [
517
+ "已发生的 task/DAG/Worker mutation 不会撤销或重放。",
518
+ "operation idempotency / clientRequestId ownership 不会复制到新会话。",
519
+ ],
520
+ });
521
+ }
522
+ catch (error) {
523
+ sendJson(res, 409, {
524
+ ok: false,
525
+ error: {
526
+ code: "SESSION_FORK_FAILED",
527
+ message: error instanceof Error ? error.message : String(error),
528
+ },
529
+ });
530
+ }
531
+ }
532
+ export async function handleBranchChatSession(req, res, deps, sessionId) {
533
+ const gate = gateMutation(req, deps);
534
+ if (!gate.ok) {
535
+ sendJson(res, gate.status, {
536
+ ok: false,
537
+ error: { code: gate.code, message: gate.message },
538
+ });
539
+ return;
540
+ }
541
+ if (deps.events.getActiveTurn(sessionId)) {
542
+ sendJson(res, 409, {
543
+ ok: false,
544
+ error: { code: "TURN_ACTIVE", message: "cannot branch an active turn" },
545
+ });
546
+ return;
547
+ }
548
+ const record = await deps.store.get(sessionId);
549
+ if (!record) {
550
+ sendJson(res, 404, {
551
+ ok: false,
552
+ error: {
553
+ code: "NOT_FOUND",
554
+ message: `chat session not found: ${sessionId}`,
555
+ },
556
+ });
557
+ return;
558
+ }
559
+ try {
560
+ if (!deps.runtime.hasSession(sessionId)) {
561
+ const reopened = await deps.runtime.reopenSession({
562
+ sessionId,
563
+ sessionFile: record.sessionFile,
564
+ });
565
+ if (!reopened.ok) {
566
+ const mapped = mapReopenSessionFailure({
567
+ code: reopened.code,
568
+ message: reopened.message,
569
+ });
570
+ sendJson(res, mapped.status, {
571
+ ok: false,
572
+ error: { code: mapped.code, message: mapped.message },
573
+ });
574
+ return;
575
+ }
576
+ }
577
+ const body = await readJsonBody(req);
578
+ if ("clientRequestId" in body || "requestIndex" in body)
579
+ throw new Error("operation idempotency ownership cannot be copied");
580
+ const current = deps.runtime.getBranchContext(sessionId);
581
+ const branchFromEntryId = typeof body.branchFromEntryId === "string"
582
+ ? body.branchFromEntryId
583
+ : current.leafId;
584
+ if (!branchFromEntryId)
585
+ throw new Error("branchFromEntryId is required");
586
+ const result = await deps.runtime.branchSession({
587
+ sessionId,
588
+ branchFromEntryId,
589
+ ...(typeof body.summary === "string" && body.summary
590
+ ? { summary: body.summary }
591
+ : {}),
592
+ });
593
+ sendJson(res, 200, {
594
+ ok: true,
595
+ ...result,
596
+ note: "仅影响后续 LLM 上下文,未持久化第二历史",
597
+ });
598
+ }
599
+ catch (error) {
600
+ sendJson(res, 400, {
601
+ ok: false,
602
+ error: {
603
+ code: "SESSION_BRANCH_FAILED",
604
+ message: error instanceof Error ? error.message : String(error),
605
+ },
606
+ });
607
+ }
608
+ }
609
+ export async function handleSwitchMainlineChatSession(req, res, deps, sessionId) {
610
+ const gate = gateMutation(req, deps);
611
+ if (!gate.ok) {
612
+ sendJson(res, gate.status, {
613
+ ok: false,
614
+ error: { code: gate.code, message: gate.message },
615
+ });
616
+ return;
617
+ }
618
+ try {
619
+ deps.runtime.switchToMainline(sessionId);
620
+ sendJson(res, 200, { ok: true, sessionId, active: "mainline" });
621
+ }
622
+ catch (error) {
623
+ sendJson(res, 409, {
624
+ ok: false,
625
+ error: {
626
+ code: "MAINLINE_SWITCH_FAILED",
627
+ message: error instanceof Error ? error.message : String(error),
628
+ },
629
+ });
630
+ }
631
+ }
632
+ export async function handleGetChatSession(_req, res, deps, sessionId) {
633
+ const record = await deps.store.get(sessionId);
634
+ if (!record) {
635
+ sendJson(res, 404, {
636
+ ok: false,
637
+ error: {
638
+ code: "NOT_FOUND",
639
+ message: `chat session not found: ${sessionId}`,
640
+ },
641
+ });
642
+ return;
643
+ }
644
+ await deps.operationLinker.recoverSession(sessionId);
645
+ const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
646
+ const snapshot = deps.events.snapshot(sessionId);
647
+ const lastEvent = snapshot.at(-1);
648
+ sendJson(res, 200, {
649
+ ok: true,
650
+ eventEpoch: deps.events.epoch(sessionId),
651
+ session: {
652
+ ...record,
653
+ composerDraft,
654
+ activeTurnId: deps.events.getActiveTurn(sessionId)?.turnId,
655
+ },
656
+ hasBash: true,
657
+ lastEventId: lastEvent?.eventId ?? null,
658
+ lastEventSeq: lastEvent?.seq ?? 0,
659
+ });
660
+ }
661
+ /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
662
+ * get_state equivalent): exposes the active turn + the latest turn's durable
663
+ * outcome WITHOUT the full message payload, so the UI can poll it on an
664
+ * interval / visibility / online without paying for session detail. */
665
+ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
666
+ const record = await deps.store.get(sessionId);
667
+ if (!record) {
668
+ sendJson(res, 404, {
669
+ ok: false,
670
+ error: {
671
+ code: "NOT_FOUND",
672
+ message: `chat session not found: ${sessionId}`,
673
+ },
674
+ });
675
+ return;
676
+ }
677
+ const active = deps.events.getActiveTurn(sessionId);
678
+ const latest = deps.events.latestTurn(sessionId);
679
+ const runtimeFacts = typeof deps.runtime.getSessionBusyFacts === "function"
680
+ ? deps.runtime.getSessionBusyFacts(sessionId)
681
+ : {
682
+ isStreaming: false,
683
+ isPromptRunning: false,
684
+ isCompacting: false,
685
+ isBashRunning: false,
686
+ };
687
+ sendJson(res, 200, {
688
+ ok: true,
689
+ state: {
690
+ // Legacy flat fields retained for older clients.
691
+ activeTurnId: active?.turnId ?? null,
692
+ activeTurnState: active?.state ?? null,
693
+ latestTurnId: latest?.turnId ?? null,
694
+ latestTurnState: latest?.state ?? null,
695
+ activeTurn: active ? turnSummary(active) : null,
696
+ latestTurn: latest ? turnSummary(latest) : null,
697
+ runtime: runtimeFacts,
698
+ },
699
+ });
700
+ }
701
+ export async function handleListChatSessions(_req, res, deps) {
702
+ const sessions = await deps.store.list({ includeArchived: true });
703
+ sendJson(res, 200, {
704
+ ok: true,
705
+ schemaVersion: 2,
706
+ sessions: sessions.map((session) => ({
707
+ ...session,
708
+ activeTurnId: deps.events.getActiveTurn(session.sessionId)?.turnId,
709
+ })),
710
+ hasBash: true,
711
+ });
712
+ }
713
+ export async function handlePatchChatSession(req, res, deps, sessionId) {
714
+ const gate = gateMutation(req, deps);
715
+ if (!gate.ok) {
716
+ sendJson(res, gate.status, {
717
+ ok: false,
718
+ error: { code: gate.code, message: gate.message },
719
+ });
720
+ return;
721
+ }
722
+ try {
723
+ const body = await readJsonBody(req);
724
+ let record;
725
+ if (typeof body.title === "string")
726
+ record = await deps.store.rename(sessionId, body.title);
727
+ else if (body.state === "active" || body.state === "archived") {
728
+ record = await deps.store.setState(sessionId, body.state);
729
+ }
730
+ else {
731
+ sendJson(res, 400, {
732
+ ok: false,
733
+ error: {
734
+ code: "INVALID_INPUT",
735
+ message: "title or state(active|archived) is required",
736
+ },
737
+ });
738
+ return;
739
+ }
740
+ sendJson(res, 200, { ok: true, session: record });
741
+ }
742
+ catch (error) {
743
+ sendJson(res, 404, {
744
+ ok: false,
745
+ error: {
746
+ code: "NOT_FOUND",
747
+ message: error instanceof Error ? error.message : String(error),
748
+ },
749
+ });
750
+ }
751
+ }
752
+ export async function handleReopenChatSession(req, res, deps, sessionId) {
753
+ const gate = gateMutation(req, deps);
754
+ if (!gate.ok) {
755
+ sendJson(res, gate.status, {
756
+ ok: false,
757
+ error: { code: gate.code, message: gate.message },
758
+ });
759
+ return;
760
+ }
761
+ const record = await deps.store.get(sessionId);
762
+ if (!record) {
763
+ sendJson(res, 404, {
764
+ ok: false,
765
+ error: {
766
+ code: "NOT_FOUND",
767
+ message: `chat session not found: ${sessionId}`,
768
+ },
769
+ });
770
+ return;
771
+ }
772
+ if (deps.runtime.hasSession(sessionId)) {
773
+ sendJson(res, 200, {
774
+ ok: true,
775
+ sessionId,
776
+ reopened: false,
777
+ alreadyActive: true,
778
+ hasBash: true,
779
+ });
780
+ return;
781
+ }
782
+ const result = await deps.runtime.reopenSession({
783
+ sessionId,
784
+ sessionFile: record.sessionFile,
785
+ ...(record.modelProvider && record.modelId
786
+ ? { model: { provider: record.modelProvider, modelId: record.modelId } }
787
+ : {}),
788
+ });
789
+ if (!result.ok) {
790
+ const mapped = mapReopenSessionFailure({
791
+ code: result.code,
792
+ message: result.message,
793
+ });
794
+ sendJson(res, mapped.status, {
795
+ ok: false,
796
+ error: { code: mapped.code, message: mapped.message },
797
+ });
798
+ return;
799
+ }
800
+ sendJson(res, 200, {
801
+ ok: true,
802
+ sessionId,
803
+ reopened: true,
804
+ sessionFile: result.handle.sessionFile,
805
+ model: result.handle.model,
806
+ activeTools: result.handle.activeTools,
807
+ hasBash: true,
808
+ });
809
+ }
810
+ export async function handleChatPrompt(req, res, deps, sessionId) {
811
+ const gate = gateMutation(req, deps);
812
+ if (!gate.ok) {
813
+ sendJson(res, gate.status, {
814
+ ok: false,
815
+ error: { code: gate.code, message: gate.message },
816
+ });
817
+ return;
818
+ }
819
+ let body;
820
+ try {
821
+ body = await readJsonBody(req);
822
+ }
823
+ catch (error) {
824
+ sendJson(res, 400, {
825
+ ok: false,
826
+ error: {
827
+ code: "INVALID_INPUT",
828
+ message: error instanceof Error ? error.message : String(error),
829
+ },
830
+ });
831
+ return;
832
+ }
833
+ const text = typeof body.text === "string" ? body.text.trim() : "";
834
+ let images;
835
+ try {
836
+ images = parseChatImages(body.images);
837
+ }
838
+ catch (error) {
839
+ sendJson(res, 400, {
840
+ ok: false,
841
+ error: {
842
+ code: "INVALID_ATTACHMENT",
843
+ message: error instanceof Error ? error.message : String(error),
844
+ },
845
+ });
846
+ return;
847
+ }
848
+ if (!text) {
849
+ sendJson(res, 400, {
850
+ ok: false,
851
+ error: { code: "INVALID_INPUT", message: "text is required" },
852
+ });
853
+ return;
854
+ }
855
+ if (!deps.runtime.hasSession(sessionId)) {
856
+ // M1-S04/S05: lazily reopen a persisted session that is no longer in
857
+ // memory (Console restart / browser refresh). Fail-closed when there is
858
+ // no durable record or no Pi session file — we never silently create a
859
+ // fresh session under the same id, which would lose prior context.
860
+ const record = await deps.store.get(sessionId);
861
+ if (!record) {
862
+ sendJson(res, 404, {
863
+ ok: false,
864
+ error: {
865
+ code: "NOT_FOUND",
866
+ message: `chat session not found: ${sessionId}`,
867
+ },
868
+ });
869
+ return;
870
+ }
871
+ const reopen = await deps.runtime.reopenSession({
872
+ sessionId,
873
+ sessionFile: record.sessionFile,
874
+ ...(record.modelProvider && record.modelId
875
+ ? { model: { provider: record.modelProvider, modelId: record.modelId } }
876
+ : {}),
877
+ });
878
+ if (!reopen.ok) {
879
+ const mapped = mapReopenSessionFailure({
880
+ code: reopen.code,
881
+ message: reopen.message,
882
+ });
883
+ sendJson(res, mapped.status, {
884
+ ok: false,
885
+ error: { code: mapped.code, message: mapped.message },
886
+ });
887
+ return;
888
+ }
889
+ }
890
+ const actionContext = deps.runtime.actionContext;
891
+ if (!actionContext) {
892
+ sendJson(res, 503, {
893
+ ok: false,
894
+ error: {
895
+ code: "CHAT_NO_ACTION_CONTEXT",
896
+ message: "Chat runtime has no operator action context wired",
897
+ },
898
+ });
899
+ return;
900
+ }
901
+ // Acquire the per-session active-turn lease (T01/T16). The compatibility
902
+ // prompt SSE route uses the same durable Turn resource as POST /turns.
903
+ const createdTurn = deps.events.createTurn(sessionId);
904
+ if (!createdTurn.ok) {
905
+ if (createdTurn.code === "REQUEST_ID_REUSE_CONFLICT") {
906
+ sendJson(res, 409, {
907
+ ok: false,
908
+ accepted: false,
909
+ error: {
910
+ code: createdTurn.code,
911
+ message: createdTurn.message,
912
+ },
913
+ });
914
+ return;
915
+ }
916
+ sendJson(res, 409, {
917
+ ok: false,
918
+ accepted: false,
919
+ error: {
920
+ code: "TURN_ACTIVE",
921
+ message: "上一轮仍在运行,已返回当前 activeTurn;请重新连接或稍后再试。",
922
+ },
923
+ activeTurn: turnSummary(createdTurn.activeTurn),
924
+ });
925
+ return;
926
+ }
927
+ const turnId = createdTurn.turn.turnId;
928
+ deps.events.setTurnState(sessionId, turnId, "running");
929
+ // Stream events via SSE. Each event is FIRST persisted to the chat event
930
+ // ring (so a reconnect via GET /events can resume by Last-Event-ID), THEN
931
+ // framed with its eventId as the SSE `id:` line.
932
+ openSseResponse(res);
933
+ const ac = new AbortController();
934
+ let closed = false;
935
+ let abortedByClient = false;
936
+ // Prefer response close: after the request body is fully read, browser abort
937
+ // of an SSE response typically emits res.close, not req.close.
938
+ res.once("close", () => {
939
+ if (res.writableEnded)
940
+ return;
941
+ closed = true;
942
+ abortedByClient = true;
943
+ ac.abort();
944
+ });
945
+ // Lazily import to avoid circular module load.
946
+ const { runChatTurn } = await import("./session-store.js");
947
+ let firstUserText;
948
+ try {
949
+ const outcome = await runChatTurn({
950
+ runtime: deps.runtime,
951
+ store: deps.store,
952
+ actionContext,
953
+ sessionId,
954
+ text,
955
+ images,
956
+ signal: ac.signal,
957
+ onUserMessagePersisted: (record) => {
958
+ const users = record.messages.filter((message) => message.role === "user");
959
+ if (users.length !== 1)
960
+ return;
961
+ firstUserText = users[0]?.text;
962
+ const placeholder = shouldEmitPlaceholder(record);
963
+ if (!placeholder)
964
+ return;
965
+ const persisted = emitSessionTitle(deps, sessionId, turnId, placeholder);
966
+ if (!res.writableEnded)
967
+ writeChatSse(res, persisted);
968
+ },
969
+ onOperationAccepted: (accepted) => deps.operationLinker.link({
970
+ sessionId,
971
+ turnId,
972
+ toolCallId: accepted.toolCallId,
973
+ operationId: accepted.operationId,
974
+ }),
975
+ onEvent: (event) => {
976
+ if (closed)
977
+ return;
978
+ if (event.type === "tool_result" &&
979
+ event.toolName === "prepareDagConfirmation" &&
980
+ !event.isError) {
981
+ const result = event.result;
982
+ const confirmationId = result?.confirmation?.confirmationId;
983
+ if (confirmationId)
984
+ void ctxConfirmationCard(deps, sessionId, turnId, confirmationId);
985
+ }
986
+ if (event.type === "tool_result" &&
987
+ event.toolName === "prepareMutationGate" &&
988
+ !event.isError) {
989
+ const result = event.result;
990
+ if (result?.receiptId)
991
+ void ctxMutationGateCard(deps, sessionId, result.receiptId);
992
+ }
993
+ // heartbeat is a keep-alive: write it as an SSE comment frame (":\n\n")
994
+ // which proxies/browsers treat as traffic but clients ignore as data.
995
+ // It is NOT persisted (no real state change).
996
+ if (event.type === "heartbeat") {
997
+ if (!res.writableEnded)
998
+ res.write(": heartbeat\n\n");
999
+ return;
1000
+ }
1001
+ const partial = turnEventToStorePartial(event);
1002
+ if (!partial)
1003
+ return;
1004
+ const persisted = deps.events.append(sessionId, turnId, partial);
1005
+ if (!res.writableEnded)
1006
+ writeChatSse(res, persisted);
1007
+ },
1008
+ });
1009
+ if (abortedByClient || ac.signal.aborted) {
1010
+ ensureAgentEndEvent(deps, sessionId, turnId);
1011
+ deps.events.setTurnState(sessionId, turnId, "aborted", {
1012
+ code: "CLIENT_ABORTED",
1013
+ message: "client disconnected before turn settled",
1014
+ });
1015
+ }
1016
+ else {
1017
+ ensureAgentEndEvent(deps, sessionId, turnId);
1018
+ deps.events.setTurnState(sessionId, turnId, outcome.ok ? "settled" : "failed", outcome.error);
1019
+ if (outcome.ok && firstUserText)
1020
+ scheduleAutomaticTitle(deps, { sessionId, turnId, firstUserText });
1021
+ }
1022
+ }
1023
+ catch (error) {
1024
+ // runChatTurn should not reject (it catches runtime.prompt failures
1025
+ // internally and returns {ok:false}), but a pre-prompt persistence failure
1026
+ // (e.g. store.appendMessage for the user message) CAN throw after the SSE
1027
+ // stream is already open. Persist + emit an error event so a reconnecting
1028
+ // client also sees the failure (not just the original requester).
1029
+ const message = error instanceof Error ? error.message : String(error);
1030
+ ensureAgentEndEvent(deps, sessionId, turnId);
1031
+ deps.events.setTurnState(sessionId, turnId, abortedByClient || ac.signal.aborted ? "aborted" : "failed", {
1032
+ code: abortedByClient || ac.signal.aborted
1033
+ ? "CLIENT_ABORTED"
1034
+ : "CHAT_TURN_FAILED",
1035
+ message,
1036
+ });
1037
+ if (!closed) {
1038
+ const persisted = deps.events.append(sessionId, turnId, {
1039
+ kind: "error",
1040
+ data: { message },
1041
+ });
1042
+ if (!res.writableEnded)
1043
+ writeChatSse(res, persisted);
1044
+ }
1045
+ }
1046
+ finally {
1047
+ // Guarantee the SSE stream is terminated regardless of outcome so the
1048
+ // connection never hangs (regression: previously a rejected promise left
1049
+ // res open and the client waited indefinitely).
1050
+ if (!closed && !res.writableEnded) {
1051
+ res.end();
1052
+ }
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Resource-oriented turn start (M1-T02/T15): acknowledge quickly, while the
1057
+ * Pi prompt continues independently of this HTTP response and publishes to the
1058
+ * persisted event ring consumed by GET /events.
1059
+ */
1060
+ export async function handleCreateChatTurn(req, res, deps, sessionId) {
1061
+ const gate = gateMutation(req, deps);
1062
+ if (!gate.ok) {
1063
+ sendJson(res, gate.status, {
1064
+ ok: false,
1065
+ accepted: false,
1066
+ error: { code: gate.code, message: gate.message },
1067
+ });
1068
+ return;
1069
+ }
1070
+ let body;
1071
+ try {
1072
+ body = await readJsonBody(req);
1073
+ }
1074
+ catch (error) {
1075
+ sendJson(res, 400, {
1076
+ ok: false,
1077
+ accepted: false,
1078
+ error: {
1079
+ code: "INVALID_INPUT",
1080
+ message: error instanceof Error ? error.message : String(error),
1081
+ },
1082
+ });
1083
+ return;
1084
+ }
1085
+ const text = typeof body.text === "string" ? body.text.trim() : "";
1086
+ const clientRequestId = typeof body.clientRequestId === "string" && body.clientRequestId.trim()
1087
+ ? body.clientRequestId.trim()
1088
+ : undefined;
1089
+ let images;
1090
+ try {
1091
+ images = parseChatImages(body.images);
1092
+ }
1093
+ catch (error) {
1094
+ sendJson(res, 400, {
1095
+ ok: false,
1096
+ accepted: false,
1097
+ error: {
1098
+ code: "INVALID_ATTACHMENT",
1099
+ message: error instanceof Error ? error.message : String(error),
1100
+ },
1101
+ });
1102
+ return;
1103
+ }
1104
+ if (!text) {
1105
+ sendJson(res, 400, {
1106
+ ok: false,
1107
+ accepted: false,
1108
+ error: { code: "INVALID_INPUT", message: "text is required" },
1109
+ });
1110
+ return;
1111
+ }
1112
+ if (!deps.runtime.hasSession(sessionId)) {
1113
+ const record = await deps.store.get(sessionId);
1114
+ if (!record) {
1115
+ sendJson(res, 404, {
1116
+ ok: false,
1117
+ accepted: false,
1118
+ error: {
1119
+ code: "NOT_FOUND",
1120
+ message: `chat session not found: ${sessionId}`,
1121
+ },
1122
+ });
1123
+ return;
1124
+ }
1125
+ const reopen = await deps.runtime.reopenSession({
1126
+ sessionId,
1127
+ sessionFile: record.sessionFile,
1128
+ ...(record.modelProvider && record.modelId
1129
+ ? { model: { provider: record.modelProvider, modelId: record.modelId } }
1130
+ : {}),
1131
+ });
1132
+ if (!reopen.ok) {
1133
+ const mapped = mapReopenSessionFailure({
1134
+ code: reopen.code,
1135
+ message: reopen.message,
1136
+ });
1137
+ sendJson(res, mapped.status, {
1138
+ ok: false,
1139
+ accepted: false,
1140
+ error: { code: mapped.code, message: mapped.message },
1141
+ });
1142
+ return;
1143
+ }
1144
+ }
1145
+ const actionContext = deps.runtime.actionContext;
1146
+ if (!actionContext) {
1147
+ sendJson(res, 503, {
1148
+ ok: false,
1149
+ accepted: false,
1150
+ error: {
1151
+ code: "CHAT_NO_ACTION_CONTEXT",
1152
+ message: "Chat runtime has no operator action context wired",
1153
+ },
1154
+ });
1155
+ return;
1156
+ }
1157
+ const payloadHash = hashChatTurnPayload({ text, images });
1158
+ const created = deps.events.createTurn(sessionId, {
1159
+ clientRequestId,
1160
+ payloadHash,
1161
+ });
1162
+ if (!created.ok) {
1163
+ if (created.code === "REQUEST_ID_REUSE_CONFLICT") {
1164
+ sendJson(res, 409, {
1165
+ ok: false,
1166
+ accepted: false,
1167
+ error: {
1168
+ code: created.code,
1169
+ message: created.message,
1170
+ },
1171
+ });
1172
+ return;
1173
+ }
1174
+ sendJson(res, 409, {
1175
+ ok: false,
1176
+ accepted: false,
1177
+ error: {
1178
+ code: "TURN_ACTIVE",
1179
+ message: "上一轮仍在运行,已返回当前 activeTurn;请重新连接或稍后再试。",
1180
+ },
1181
+ activeTurn: turnSummary(created.activeTurn),
1182
+ });
1183
+ return;
1184
+ }
1185
+ const turn = created.turn;
1186
+ // Idempotent retry of an already-accepted request: return the same Turn and
1187
+ // never re-run the prompt (even if it already finished).
1188
+ if (created.idempotent) {
1189
+ sendJson(res, 202, {
1190
+ ok: true,
1191
+ accepted: true,
1192
+ idempotent: true,
1193
+ turnId: turn.turnId,
1194
+ ordinal: turn.ordinal,
1195
+ state: turn.state,
1196
+ turn: turnSummary(turn),
1197
+ });
1198
+ return;
1199
+ }
1200
+ const controller = new AbortController();
1201
+ const execution = deps.turnExecutions?.register({
1202
+ sessionId,
1203
+ turnId: turn.turnId,
1204
+ clientRequestId,
1205
+ controller,
1206
+ });
1207
+ deps.events.setTurnState(sessionId, turn.turnId, "running");
1208
+ const running = deps.events.getTurn(sessionId, turn.turnId) ?? {
1209
+ ...turn,
1210
+ state: "running",
1211
+ };
1212
+ sendJson(res, 202, {
1213
+ ok: true,
1214
+ accepted: true,
1215
+ idempotent: false,
1216
+ turnId: running.turnId,
1217
+ ordinal: running.ordinal,
1218
+ state: running.state,
1219
+ turn: turnSummary(running),
1220
+ });
1221
+ // Deliberately detached from the request lifecycle: closing the POST
1222
+ // response cannot abort the prompt; clients follow GET /events instead.
1223
+ // Abort ownership lives in the execution registry (real Stop path).
1224
+ void (async () => {
1225
+ const { runChatTurn } = await import("./session-store.js");
1226
+ let firstUserText;
1227
+ let terminalState = "failed";
1228
+ let terminalError;
1229
+ try {
1230
+ const outcome = await runChatTurn({
1231
+ runtime: deps.runtime,
1232
+ store: deps.store,
1233
+ actionContext,
1234
+ sessionId,
1235
+ text,
1236
+ images,
1237
+ signal: controller.signal,
1238
+ onUserMessagePersisted: (record) => {
1239
+ const users = record.messages.filter((message) => message.role === "user");
1240
+ if (users.length !== 1)
1241
+ return;
1242
+ firstUserText = users[0]?.text;
1243
+ const placeholder = shouldEmitPlaceholder(record);
1244
+ if (placeholder)
1245
+ emitSessionTitle(deps, sessionId, turn.turnId, placeholder);
1246
+ },
1247
+ onOperationAccepted: (accepted) => deps.operationLinker.link({
1248
+ sessionId,
1249
+ turnId: turn.turnId,
1250
+ toolCallId: accepted.toolCallId,
1251
+ operationId: accepted.operationId,
1252
+ }),
1253
+ onEvent: (event) => {
1254
+ if (event.type === "heartbeat")
1255
+ return;
1256
+ const partial = turnEventToStorePartial(event);
1257
+ if (partial)
1258
+ deps.events.append(sessionId, turn.turnId, partial);
1259
+ },
1260
+ });
1261
+ ensureAgentEndEvent(deps, sessionId, turn.turnId);
1262
+ if (controller.signal.aborted) {
1263
+ terminalState = "aborted";
1264
+ terminalError = {
1265
+ code: "CLIENT_ABORTED",
1266
+ message: "turn aborted by client",
1267
+ };
1268
+ deps.events.setTurnState(sessionId, turn.turnId, "aborted", terminalError);
1269
+ }
1270
+ else if (outcome.ok) {
1271
+ terminalState = "settled";
1272
+ deps.events.setTurnState(sessionId, turn.turnId, "settled");
1273
+ if (firstUserText)
1274
+ scheduleAutomaticTitle(deps, {
1275
+ sessionId,
1276
+ turnId: turn.turnId,
1277
+ firstUserText,
1278
+ });
1279
+ }
1280
+ else {
1281
+ terminalState = "failed";
1282
+ terminalError = outcome.error;
1283
+ deps.events.setTurnState(sessionId, turn.turnId, "failed", outcome.error);
1284
+ }
1285
+ }
1286
+ catch (error) {
1287
+ const message = error instanceof Error ? error.message : String(error);
1288
+ if (controller.signal.aborted) {
1289
+ terminalState = "aborted";
1290
+ terminalError = {
1291
+ code: "CLIENT_ABORTED",
1292
+ message: "turn aborted by client",
1293
+ };
1294
+ ensureAgentEndEvent(deps, sessionId, turn.turnId);
1295
+ deps.events.setTurnState(sessionId, turn.turnId, "aborted", terminalError);
1296
+ }
1297
+ else {
1298
+ terminalState = "failed";
1299
+ terminalError = { code: "CHAT_TURN_FAILED", message };
1300
+ deps.events.append(sessionId, turn.turnId, {
1301
+ kind: "error",
1302
+ data: { message },
1303
+ });
1304
+ ensureAgentEndEvent(deps, sessionId, turn.turnId);
1305
+ deps.events.setTurnState(sessionId, turn.turnId, "failed", terminalError);
1306
+ }
1307
+ }
1308
+ finally {
1309
+ execution?.resolveCompletion({
1310
+ ok: terminalState === "settled",
1311
+ state: terminalState,
1312
+ error: terminalError,
1313
+ });
1314
+ deps.turnExecutions?.clear(sessionId, turn.turnId);
1315
+ }
1316
+ })();
1317
+ }
1318
+ /** Real server-side abort: invokes Pi session.abort via the execution registry. */
1319
+ export async function handleAbortChatTurn(req, res, deps, sessionId, turnId) {
1320
+ const gate = gateMutation(req, deps);
1321
+ if (!gate.ok) {
1322
+ sendJson(res, gate.status, {
1323
+ ok: false,
1324
+ accepted: false,
1325
+ error: { code: gate.code, message: gate.message },
1326
+ });
1327
+ return;
1328
+ }
1329
+ const turn = deps.events.getTurn(sessionId, turnId);
1330
+ if (!turn) {
1331
+ sendJson(res, 404, {
1332
+ ok: false,
1333
+ accepted: false,
1334
+ error: {
1335
+ code: "NOT_FOUND",
1336
+ message: `chat turn not found: ${turnId}`,
1337
+ },
1338
+ });
1339
+ return;
1340
+ }
1341
+ if (isTerminalTurnState(turn.state)) {
1342
+ sendJson(res, 200, {
1343
+ ok: true,
1344
+ accepted: true,
1345
+ idempotent: true,
1346
+ turn: turnSummary(turn),
1347
+ });
1348
+ return;
1349
+ }
1350
+ const execution = deps.turnExecutions?.get(sessionId, turnId);
1351
+ if (!execution) {
1352
+ // Active durable Turn without process ownership (restart orphan / detached).
1353
+ const closed = deps.events.setTurnState(sessionId, turnId, "failed", {
1354
+ code: "CHAT_TURN_OWNER_LOST",
1355
+ message: "turn ownership lost; cannot abort a turn this process does not own",
1356
+ });
1357
+ sendJson(res, 409, {
1358
+ ok: false,
1359
+ accepted: false,
1360
+ error: {
1361
+ code: "CHAT_TURN_OWNER_LOST",
1362
+ message: "turn ownership lost; cannot abort a turn this process does not own",
1363
+ },
1364
+ turn: closed ? turnSummary(closed) : turnSummary(turn),
1365
+ });
1366
+ return;
1367
+ }
1368
+ deps.events.markAbortRequested?.(sessionId, turnId);
1369
+ if (!execution.controller.signal.aborted)
1370
+ execution.controller.abort();
1371
+ const waited = await (deps.turnExecutions?.waitForCompletion(sessionId, turnId, DEFAULT_ABORT_WAIT_MS) ?? Promise.resolve({ status: "missing" }));
1372
+ if (waited.status === "completed") {
1373
+ const finalTurn = deps.events.getTurn(sessionId, turnId) ??
1374
+ {
1375
+ ...turn,
1376
+ state: waited.outcome.state,
1377
+ };
1378
+ sendJson(res, 200, {
1379
+ ok: true,
1380
+ accepted: true,
1381
+ idempotent: false,
1382
+ turn: turnSummary(finalTurn),
1383
+ });
1384
+ return;
1385
+ }
1386
+ // Timeout or missing completion: keep non-terminal; client reconciles via /state.
1387
+ const pending = deps.events.getTurn(sessionId, turnId) ?? turn;
1388
+ sendJson(res, 202, {
1389
+ ok: false,
1390
+ accepted: false,
1391
+ error: {
1392
+ code: "TURN_ABORT_PENDING",
1393
+ message: "abort requested; turn has not yet reached a terminal state",
1394
+ },
1395
+ turn: turnSummary(pending),
1396
+ });
1397
+ }
1398
+ export async function handleGetChatTurn(_req, res, deps, sessionId, turnId) {
1399
+ const turn = deps.events.getTurn(sessionId, turnId);
1400
+ if (!turn) {
1401
+ sendJson(res, 404, {
1402
+ ok: false,
1403
+ error: { code: "NOT_FOUND", message: `chat turn not found: ${turnId}` },
1404
+ });
1405
+ return;
1406
+ }
1407
+ sendJson(res, 200, { ok: true, turn });
1408
+ }
1409
+ export async function handleChatEventsStream(req, res, deps, sessionId) {
1410
+ // Read routes (capabilities/sessions GET) are open to loopback; the events
1411
+ // stream is a READ and does NOT require the mutation gate, but we still need
1412
+ // the boot cookie so only the Console browser can poll a live session.
1413
+ const record = await deps.store.get(sessionId);
1414
+ if (!record) {
1415
+ sendJson(res, 404, {
1416
+ ok: false,
1417
+ error: {
1418
+ code: "NOT_FOUND",
1419
+ message: `chat session not found: ${sessionId}`,
1420
+ },
1421
+ });
1422
+ return;
1423
+ }
1424
+ await deps.operationLinker.recoverSession(sessionId);
1425
+ openSseResponse(res);
1426
+ // Resume from the client's last seen eventId (`<epoch>:<seq>`). A previous
1427
+ // Console boot/legacy cursor has a different prefix and receives snapshot.
1428
+ const lastEventIdHeader = req.headers["last-event-id"];
1429
+ const cursor = parseChatEventCursor(Array.isArray(lastEventIdHeader) ? lastEventIdHeader[0] : lastEventIdHeader);
1430
+ const replay = deps.events.listFrom(sessionId, cursor);
1431
+ let lastSeq = cursor.seq;
1432
+ if ("error" in replay) {
1433
+ // Cursor aged out of the bounded ring (T07 fence): tell the client to
1434
+ // reconcile from a fresh snapshot rather than render a gap as if current.
1435
+ // The frame carries the ring tail (latestEventId) so the client can resume
1436
+ // the replayed snapshot with a fresh Last-Event-ID (recovery cursor).
1437
+ const retained = deps.events.snapshot(sessionId);
1438
+ writeChatSse(res, {
1439
+ kind: "reconcile",
1440
+ sessionId,
1441
+ reason: replay.error === "CURSOR_EPOCH_MISMATCH"
1442
+ ? "epoch_mismatch"
1443
+ : "cursor_expired",
1444
+ minSeq: replay.minSeq,
1445
+ epoch: replay.epoch,
1446
+ latestTurnId: deps.events.latestTurnId(sessionId),
1447
+ latestEventId: retained.at(-1)?.eventId ?? null,
1448
+ });
1449
+ // Fall back to full retained snapshot so the client can re-render.
1450
+ for (const ev of retained) {
1451
+ writeChatSse(res, ev);
1452
+ lastSeq = Math.max(lastSeq, ev.seq);
1453
+ }
1454
+ }
1455
+ else {
1456
+ for (const ev of replay.events) {
1457
+ writeChatSse(res, ev);
1458
+ lastSeq = Math.max(lastSeq, ev.seq);
1459
+ }
1460
+ }
1461
+ // Delimit retained replay from follow mode. The client uses this marker to
1462
+ // ensure its sole GET /events consumer is ready before POST /turns (M4.1).
1463
+ const retainedTail = deps.events.snapshot(sessionId).at(-1);
1464
+ writeChatSse(res, {
1465
+ kind: "stream_ready",
1466
+ sessionId,
1467
+ epoch: deps.events.epoch(sessionId),
1468
+ latestEventId: retainedTail?.eventId ?? null,
1469
+ });
1470
+ let closed = false;
1471
+ res.once("close", () => {
1472
+ closed = true;
1473
+ });
1474
+ // Follow live appends for a short window so a reconnect that lands while a
1475
+ // turn is still streaming continues to receive new events (T05). If nothing
1476
+ // arrives within the follow window we close the stream idempotently; the
1477
+ // client re-opens GET /events and resumes from its tracked Last-Event-ID.
1478
+ const FOLLOW_WINDOW_MS = 25_000;
1479
+ const IDLE_CLOSE_MS = 12_000;
1480
+ const unsub = deps.events.subscribe(sessionId, (ev) => {
1481
+ if (closed || res.writableEnded)
1482
+ return;
1483
+ if (ev.seq <= lastSeq)
1484
+ return; // already sent during replay
1485
+ writeChatSse(res, ev);
1486
+ lastSeq = ev.seq;
1487
+ });
1488
+ const startedAt = Date.now();
1489
+ const tick = setInterval(() => {
1490
+ if (closed || res.writableEnded) {
1491
+ clearInterval(tick);
1492
+ return;
1493
+ }
1494
+ const elapsed = Date.now() - startedAt;
1495
+ // Emit a keep-alive comment so proxies/browsers don't drop the idle stream.
1496
+ res.write(": keepalive\n\n");
1497
+ if (elapsed >= FOLLOW_WINDOW_MS) {
1498
+ clearInterval(tick);
1499
+ unsub();
1500
+ if (!res.writableEnded)
1501
+ res.end();
1502
+ }
1503
+ }, IDLE_CLOSE_MS);
1504
+ // If the client disconnects before the window elapses, clean up.
1505
+ res.once("close", () => {
1506
+ clearInterval(tick);
1507
+ unsub();
1508
+ });
1509
+ }
1510
+ export async function handleDeleteChatSession(req, res, deps, sessionId) {
1511
+ const gate = gateMutation(req, deps);
1512
+ if (!gate.ok) {
1513
+ sendJson(res, gate.status, {
1514
+ ok: false,
1515
+ error: { code: gate.code, message: gate.message },
1516
+ });
1517
+ return;
1518
+ }
1519
+ deps.runtime.dispose(sessionId);
1520
+ deps.operationLinker.clearSession(sessionId);
1521
+ deps.events.clear(sessionId);
1522
+ const removed = await deps.store.remove(sessionId);
1523
+ await new ComposerDraftStore(deps.appData).remove(sessionId);
1524
+ if (!removed) {
1525
+ sendJson(res, 404, {
1526
+ ok: false,
1527
+ error: {
1528
+ code: "NOT_FOUND",
1529
+ message: `chat session not found: ${sessionId}`,
1530
+ },
1531
+ });
1532
+ return;
1533
+ }
1534
+ sendJson(res, 200, { ok: true, sessionId, disposed: true, removed: true });
1535
+ }
1536
+ export async function handleGetTaskContext(_res, res, deps, sessionId) {
1537
+ if (!(await requireChatSession(res, deps, sessionId)))
1538
+ return;
1539
+ const events = deps.events.snapshot(sessionId);
1540
+ const context = projectTaskContext(events);
1541
+ const latest = events.at(-1);
1542
+ sendJson(res, 200, {
1543
+ ok: true,
1544
+ context,
1545
+ lastSeq: latest?.seq ?? 0,
1546
+ lastEventId: latest?.eventId,
1547
+ });
1548
+ }
1549
+ async function handleComposerDraft(req, res, deps, sessionId) {
1550
+ const gate = gateMutation(req, deps);
1551
+ if (!gate.ok) {
1552
+ sendJson(res, gate.status, {
1553
+ ok: false,
1554
+ error: { code: gate.code, message: gate.message },
1555
+ });
1556
+ return;
1557
+ }
1558
+ if (!(await requireChatSession(res, deps, sessionId)))
1559
+ return;
1560
+ try {
1561
+ const body = await readJsonBody(req, 70 * 1024);
1562
+ const text = typeof body.text === "string" ? body.text : "";
1563
+ await new ComposerDraftStore(deps.appData).save(sessionId, text);
1564
+ sendJson(res, 200, { ok: true });
1565
+ }
1566
+ catch (error) {
1567
+ sendJson(res, 400, {
1568
+ ok: false,
1569
+ error: {
1570
+ code: "INVALID_INPUT",
1571
+ message: error instanceof Error ? error.message : String(error),
1572
+ },
1573
+ });
1574
+ }
1575
+ }
1576
+ async function handleRepoBrowser(res, deps, kind, rawPath) {
1577
+ try {
1578
+ const data = kind === "tree"
1579
+ ? await listRepoDirectory(deps.appData.repoRoot, rawPath)
1580
+ : await readRepoPreview(deps.appData.repoRoot, rawPath);
1581
+ sendJson(res, 200, { ok: true, data });
1582
+ }
1583
+ catch (error) {
1584
+ const failure = error instanceof RepoBrowserError
1585
+ ? error
1586
+ : new RepoBrowserError(500, "READ_FAILED", error instanceof Error ? error.message : String(error));
1587
+ sendJson(res, failure.status, {
1588
+ ok: false,
1589
+ error: { code: failure.code, message: failure.message },
1590
+ });
1591
+ }
1592
+ }
1593
+ async function handlePiConfig(req, res, deps, resource) {
1594
+ try {
1595
+ if (resource === "models") {
1596
+ const agentDir = await deps.runtime.getAgentDir();
1597
+ if (req.method === "GET")
1598
+ sendJson(res, 200, {
1599
+ ok: true,
1600
+ data: await readModelsConfig(agentDir),
1601
+ });
1602
+ else {
1603
+ const gate = gateMutation(req, deps);
1604
+ if (!gate.ok) {
1605
+ sendJson(res, gate.status, {
1606
+ ok: false,
1607
+ error: { code: gate.code, message: gate.message },
1608
+ });
1609
+ return;
1610
+ }
1611
+ const body = await readJsonBody(req);
1612
+ sendJson(res, 200, {
1613
+ ok: true,
1614
+ data: await patchModelsConfig(agentDir, body),
1615
+ });
1616
+ }
1617
+ return;
1618
+ }
1619
+ if (resource === "packages") {
1620
+ sendJson(res, 200, {
1621
+ ok: true,
1622
+ data: {
1623
+ packages: await readPackageInventory(await deps.runtime.getAgentDir(), deps.appData.repoRoot),
1624
+ mutationSupported: false,
1625
+ },
1626
+ });
1627
+ return;
1628
+ }
1629
+ }
1630
+ catch (error) {
1631
+ const failure = error;
1632
+ sendJson(res, failure.status ?? 400, {
1633
+ ok: false,
1634
+ error: {
1635
+ code: failure.code ?? "INVALID_INPUT",
1636
+ message: failure.message,
1637
+ },
1638
+ });
1639
+ }
1640
+ }
1641
+ async function handleChatFiles(res, deps, sessionId, query) {
1642
+ if (!(await requireChatSession(res, deps, sessionId)))
1643
+ return;
1644
+ const q = query.trim().toLowerCase();
1645
+ const walked = await walkRepoFiles(deps.appData.repoRoot, {
1646
+ skipSensitive: true,
1647
+ maxFiles: 5000,
1648
+ includeDirs: true,
1649
+ });
1650
+ const entries = walked
1651
+ .filter((entry) => !isSensitivePath(entry.repoRelative) &&
1652
+ (!q || entry.repoRelative.toLowerCase().includes(q)))
1653
+ .sort((a, b) => {
1654
+ const aDir = a.repoRelative.endsWith("/");
1655
+ const bDir = b.repoRelative.endsWith("/");
1656
+ if (aDir !== bDir)
1657
+ return aDir ? -1 : 1;
1658
+ return a.repoRelative.localeCompare(b.repoRelative);
1659
+ })
1660
+ .slice(0, 50)
1661
+ .map((entry) => ({
1662
+ path: entry.repoRelative,
1663
+ isDir: entry.repoRelative.endsWith("/"),
1664
+ }));
1665
+ sendJson(res, 200, { ok: true, files: entries });
1666
+ }
1667
+ export async function handleChatCommands(res, deps, sessionId) {
1668
+ try {
1669
+ const listed = typeof deps.runtime.listSlashCommands === "function"
1670
+ ? await deps.runtime.listSlashCommands(sessionId)
1671
+ : { commands: [] };
1672
+ // Partial single-source failures keep successful commands + compact warning (AC-3).
1673
+ const commands = listed.commands ?? [];
1674
+ const warning = typeof listed.warning === "string" && listed.warning.trim()
1675
+ ? listed.warning.trim().slice(0, 160)
1676
+ : undefined;
1677
+ res.statusCode = 200;
1678
+ res.setHeader("content-type", "application/json; charset=utf-8");
1679
+ res.end(JSON.stringify({
1680
+ ok: true,
1681
+ commands,
1682
+ ...(warning ? { warning } : {}),
1683
+ }));
1684
+ }
1685
+ catch (error) {
1686
+ // Whole-list / readiness failures stay fail-soft with empty commands + warning.
1687
+ res.statusCode = 200;
1688
+ res.setHeader("content-type", "application/json; charset=utf-8");
1689
+ res.end(JSON.stringify({
1690
+ ok: true,
1691
+ commands: [],
1692
+ warning: error instanceof Error ? error.message : "commands unavailable",
1693
+ }));
1694
+ }
1695
+ }
1696
+ export async function handleRuntimeContext(res, deps, sessionId) {
1697
+ const ensured = await ensureActiveSession(deps, sessionId);
1698
+ if (!ensured.ok) {
1699
+ sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
1700
+ return;
1701
+ }
1702
+ // AC-03: system prompt comes from the REAL Pi session (agent.state.systemPrompt
1703
+ // fallback chain); resources come from that session's ResourceLoader. The raw
1704
+ // prompt stays server-side; the browser only ever receives the scrubbed +
1705
+ // length-limited projection. RF-02: a critical projection failure is 503
1706
+ // PI_RUNTIME_UNAVAILABLE — never a 500 leak or a fake-empty context.
1707
+ let snapshot;
1708
+ try {
1709
+ snapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
1710
+ includeRaw: true,
1711
+ });
1712
+ }
1713
+ catch (error) {
1714
+ if (sendRuntimeUnavailable(res, error))
1715
+ return;
1716
+ throw error;
1717
+ }
1718
+ if (!snapshot) {
1719
+ sendJson(res, 503, {
1720
+ ok: false,
1721
+ error: {
1722
+ code: "PI_RUNTIME_UNAVAILABLE",
1723
+ message: `chat session not active: ${sessionId}`,
1724
+ },
1725
+ });
1726
+ return;
1727
+ }
1728
+ sendJson(res, 200, {
1729
+ ok: true,
1730
+ context: projectRuntimeContext({
1731
+ systemPrompt: snapshot.systemPrompt.raw ?? "",
1732
+ model: snapshot.selection.model
1733
+ ? {
1734
+ provider: snapshot.selection.model.provider,
1735
+ modelId: snapshot.selection.model.id,
1736
+ }
1737
+ : undefined,
1738
+ thinkingLevel: snapshot.selection.thinkingSelection === "auto"
1739
+ ? snapshot.selection.effectiveThinkingLevel
1740
+ : snapshot.selection.thinkingSelection,
1741
+ activeTools: snapshot.activeTools,
1742
+ contextFiles: snapshot.contextFiles,
1743
+ skills: snapshot.skills,
1744
+ extensions: snapshot.extensions,
1745
+ packages: snapshot.packages.map((pkg) => ({
1746
+ name: pkg.spec,
1747
+ source: pkg.spec,
1748
+ scope: pkg.scope,
1749
+ enabled: pkg.enabled,
1750
+ ...(pkg.version ? { version: pkg.version } : {}),
1751
+ ...(pkg.path ? { path: pkg.path } : {}),
1752
+ ...(pkg.cwd ? { cwd: pkg.cwd } : {}),
1753
+ resolved: pkg.resolved,
1754
+ diagnostics: pkg.diagnostics,
1755
+ })),
1756
+ diagnostics: snapshot.diagnostics,
1757
+ }),
1758
+ });
1759
+ }
1760
+ // Exported for the direct handler-boundary tests (trace binding for
1761
+ // vt-route-model-errors); router dispatch behavior is unchanged.
1762
+ export async function handleModels(req, res, deps, sessionId) {
1763
+ // AC-01/AC-06: models (and model/thinking mutation) requests reuse the same
1764
+ // activation path. A missing/reopen-failed runtime is a STRUCTURED error —
1765
+ // never a fabricated empty model list.
1766
+ const ensured = await ensureActiveSession(deps, sessionId);
1767
+ if (!ensured.ok) {
1768
+ sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
1769
+ return;
1770
+ }
1771
+ try {
1772
+ if ((req.method ?? "GET").toUpperCase() === "GET") {
1773
+ let models;
1774
+ try {
1775
+ models = await deps.runtime.listModels(sessionId);
1776
+ }
1777
+ catch (error) {
1778
+ // AC-06: a model-list failure is a STRUCTURED runtime error — never
1779
+ // folded into a fake empty list.
1780
+ sendJson(res, 503, {
1781
+ ok: false,
1782
+ error: {
1783
+ code: "PI_RUNTIME_UNAVAILABLE",
1784
+ message: error instanceof Error ? error.message : String(error),
1785
+ },
1786
+ });
1787
+ return;
1788
+ }
1789
+ sendJson(res, 200, {
1790
+ ok: true,
1791
+ models: models.map((model) => ({
1792
+ provider: model.provider,
1793
+ id: model.id ?? model.modelId ?? "",
1794
+ ...(model.name ? { name: model.name } : {}),
1795
+ hasCredentials: model.hasCredentials,
1796
+ })),
1797
+ thinkingLevels: THINKING_LEVELS,
1798
+ });
1799
+ return;
1800
+ }
1801
+ const gate = gateMutation(req, deps);
1802
+ if (!gate.ok) {
1803
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
1804
+ return;
1805
+ }
1806
+ const body = await readJsonBody(req);
1807
+ let updated;
1808
+ if (typeof body.provider === "string" && typeof body.modelId === "string") {
1809
+ const model = await deps.runtime.applyModel(sessionId, { provider: body.provider, modelId: body.modelId });
1810
+ updated = await deps.store.setRuntimeSelection(sessionId, { modelProvider: model.provider, modelId: model.modelId });
1811
+ }
1812
+ else if (typeof body.thinkingLevel === "string" && (body.thinkingLevel === "auto" || THINKING_LEVELS.includes(body.thinkingLevel))) {
1813
+ const thinkingLevel = deps.runtime.applyThinkingLevel(sessionId, body.thinkingLevel);
1814
+ updated = await deps.store.setRuntimeSelection(sessionId, thinkingLevel === "auto" ? {} : { thinkingLevel });
1815
+ }
1816
+ else {
1817
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "provider/modelId or thinkingLevel is required" } });
1818
+ return;
1819
+ }
1820
+ // Design §5: mutations return the fresh snapshot so the browser can
1821
+ // atomically replace its state (revision increments server-side).
1822
+ // RF-02: the mutation may already be applied server-side, but a failed
1823
+ // snapshot read must surface as 503 PI_RUNTIME_UNAVAILABLE — NOT be
1824
+ // folded into a 409 MODEL_SWITCH_FAILED (the frontend refresh path
1825
+ // re-reads the snapshot to converge on the applied selection).
1826
+ const snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
1827
+ sendJson(res, 200, { ok: true, session: updated, snapshot });
1828
+ }
1829
+ catch (error) {
1830
+ if (error instanceof RuntimeSnapshotUnavailableError) {
1831
+ sendJson(res, 503, {
1832
+ ok: false,
1833
+ error: { code: error.code, message: error.message },
1834
+ });
1835
+ return;
1836
+ }
1837
+ sendJson(res, 409, { ok: false, error: { code: "MODEL_SWITCH_FAILED", message: error instanceof Error ? error.message : String(error) } });
1838
+ }
1839
+ }
1840
+ async function requireChatSession(res, deps, sessionId) {
1841
+ if (await deps.store.get(sessionId))
1842
+ return true;
1843
+ sendJson(res, 404, {
1844
+ ok: false,
1845
+ error: {
1846
+ code: "NOT_FOUND",
1847
+ message: `chat session not found: ${sessionId}`,
1848
+ },
1849
+ });
1850
+ return false;
1851
+ }
1852
+ /**
1853
+ * Unified session activation (design §2.1 / AC-01): models, runtime snapshot,
1854
+ * reload and model/thinking mutation requests all flow through this path.
1855
+ *
1856
+ * - runtime already holds the session → ok;
1857
+ * - only a persisted record exists → reopen from record.sessionFile;
1858
+ * - no record / missing session file → 404 PI_SESSION_NOT_FOUND;
1859
+ * - SDK reopen failure → 503 PI_RUNTIME_UNAVAILABLE.
1860
+ *
1861
+ * Never fabricates a fresh session or an empty model list.
1862
+ */
1863
+ export async function ensureActiveSession(deps, sessionId) {
1864
+ // Wait for create-path materialization before treating the session as missing.
1865
+ if (typeof deps.runtime.ensureSessionReady === "function") {
1866
+ try {
1867
+ await deps.runtime.ensureSessionReady(sessionId);
1868
+ if (deps.runtime.hasSession(sessionId))
1869
+ return { ok: true };
1870
+ }
1871
+ catch (error) {
1872
+ const code = error && typeof error === "object" && "code" in error
1873
+ ? String(error.code ?? "")
1874
+ : "";
1875
+ if (code === "PI_SESSION_INIT_FAILED") {
1876
+ return {
1877
+ ok: false,
1878
+ status: 503,
1879
+ code: "PI_SESSION_INIT_FAILED",
1880
+ message: error instanceof Error ? error.message : String(error),
1881
+ };
1882
+ }
1883
+ // Not initializing / not found — fall through to reopen path.
1884
+ }
1885
+ }
1886
+ if (deps.runtime.hasSession(sessionId))
1887
+ return { ok: true };
1888
+ const record = await deps.store.get(sessionId);
1889
+ if (!record) {
1890
+ return {
1891
+ ok: false,
1892
+ status: 404,
1893
+ code: "PI_SESSION_NOT_FOUND",
1894
+ message: `chat session not found: ${sessionId}`,
1895
+ };
1896
+ }
1897
+ const reopened = await deps.runtime.reopenSession({
1898
+ sessionId,
1899
+ sessionFile: record.sessionFile,
1900
+ ...(record.modelProvider && record.modelId
1901
+ ? { model: { provider: record.modelProvider, modelId: record.modelId } }
1902
+ : {}),
1903
+ });
1904
+ if (!reopened.ok) {
1905
+ return { ok: false, ...mapReopenSessionFailure(reopened) };
1906
+ }
1907
+ return { ok: true };
1908
+ }
1909
+ /**
1910
+ * PATCH /sessions/:id/skills/:resourceId/auto-invocation
1911
+ * Persist repo-scoped preference, then try immediate reloadSession when idle.
1912
+ * Busy sessions keep preferenceSaved=true and reloadApplied=false (no abort).
1913
+ */
1914
+ export async function handleSkillAutoInvocation(req, res, deps, sessionId, resourceId) {
1915
+ const gate = gateMutation(req, deps);
1916
+ if (!gate.ok) {
1917
+ sendJson(res, gate.status, {
1918
+ ok: false,
1919
+ error: { code: gate.code, message: gate.message },
1920
+ });
1921
+ return;
1922
+ }
1923
+ const ensured = await ensureActiveSession(deps, sessionId);
1924
+ if (!ensured.ok) {
1925
+ sendJson(res, ensured.status, {
1926
+ ok: false,
1927
+ error: { code: ensured.code, message: ensured.message },
1928
+ });
1929
+ return;
1930
+ }
1931
+ const id = resourceId.trim();
1932
+ if (!id) {
1933
+ sendJson(res, 400, {
1934
+ ok: false,
1935
+ error: {
1936
+ code: "INVALID_INPUT",
1937
+ message: "resourceId is required",
1938
+ },
1939
+ });
1940
+ return;
1941
+ }
1942
+ let body;
1943
+ try {
1944
+ body = await readJsonBody(req);
1945
+ }
1946
+ catch (error) {
1947
+ sendJson(res, 400, {
1948
+ ok: false,
1949
+ error: {
1950
+ code: "INVALID_INPUT",
1951
+ message: error instanceof Error ? error.message : String(error),
1952
+ },
1953
+ });
1954
+ return;
1955
+ }
1956
+ if (typeof body.autoInvocationEnabled !== "boolean") {
1957
+ sendJson(res, 400, {
1958
+ ok: false,
1959
+ error: {
1960
+ code: "INVALID_INPUT",
1961
+ message: "autoInvocationEnabled boolean is required",
1962
+ },
1963
+ });
1964
+ return;
1965
+ }
1966
+ const found = await deps.runtime.findSkillByResourceId(sessionId, id);
1967
+ if (!found) {
1968
+ sendJson(res, 404, {
1969
+ ok: false,
1970
+ error: {
1971
+ code: "PI_SKILL_NOT_FOUND",
1972
+ message: `skill resourceId not in current session inventory: ${id}`,
1973
+ },
1974
+ });
1975
+ return;
1976
+ }
1977
+ const prefsPath = deps.appData.resourcePreferences;
1978
+ if (!prefsPath) {
1979
+ sendJson(res, 500, {
1980
+ ok: false,
1981
+ error: {
1982
+ code: "PI_RESOURCE_PREFERENCES_WRITE_FAILED",
1983
+ message: "resourcePreferences path is not configured",
1984
+ },
1985
+ });
1986
+ return;
1987
+ }
1988
+ try {
1989
+ await setSkillAutoInvocationPreference({
1990
+ filePath: prefsPath,
1991
+ resourceId: id,
1992
+ autoInvocationEnabled: body.autoInvocationEnabled,
1993
+ });
1994
+ }
1995
+ catch (error) {
1996
+ if (error instanceof ResourcePreferencesError) {
1997
+ const status = error.code === "PI_RESOURCE_PREFERENCES_MALFORMED" ? 500 : 500;
1998
+ sendJson(res, status, {
1999
+ ok: false,
2000
+ error: { code: error.code, message: error.message },
2001
+ });
2002
+ return;
2003
+ }
2004
+ sendJson(res, 500, {
2005
+ ok: false,
2006
+ error: {
2007
+ code: "PI_RESOURCE_PREFERENCES_WRITE_FAILED",
2008
+ message: error instanceof Error ? error.message : String(error),
2009
+ },
2010
+ });
2011
+ return;
2012
+ }
2013
+ const reload = await deps.runtime.reloadSession(sessionId);
2014
+ if (!reload.ok) {
2015
+ if (reload.code === "PI_SESSION_BUSY") {
2016
+ // Preference is durable; do not abort the busy turn or pretend reload applied.
2017
+ sendJson(res, 200, {
2018
+ ok: true,
2019
+ preferenceSaved: true,
2020
+ reloadApplied: false,
2021
+ error: { code: reload.code, message: reload.message },
2022
+ });
2023
+ return;
2024
+ }
2025
+ sendJson(res, reload.code === "PI_SESSION_NOT_FOUND" ? 404 : 500, {
2026
+ ok: false,
2027
+ preferenceSaved: true,
2028
+ reloadApplied: false,
2029
+ error: { code: reload.code, message: reload.message },
2030
+ });
2031
+ return;
2032
+ }
2033
+ let snapshot;
2034
+ try {
2035
+ snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
2036
+ }
2037
+ catch (error) {
2038
+ if (sendRuntimeUnavailable(res, error))
2039
+ return;
2040
+ throw error;
2041
+ }
2042
+ if (!snapshot) {
2043
+ sendJson(res, 503, {
2044
+ ok: false,
2045
+ preferenceSaved: true,
2046
+ reloadApplied: true,
2047
+ error: {
2048
+ code: "PI_RUNTIME_UNAVAILABLE",
2049
+ message: `chat session not active after reload: ${sessionId}`,
2050
+ },
2051
+ });
2052
+ return;
2053
+ }
2054
+ sendJson(res, 200, {
2055
+ ok: true,
2056
+ preferenceSaved: true,
2057
+ reloadApplied: true,
2058
+ revision: reload.revision,
2059
+ snapshot,
2060
+ });
2061
+ }
2062
+ /**
2063
+ * POST /sessions/:id/reload (AC-05 / AC-06): official session.reload() with
2064
+ * structured busy/failure semantics. Never aborts, never silently queues.
2065
+ */
2066
+ export async function handleReloadSession(req, res, deps, sessionId) {
2067
+ const gate = gateMutation(req, deps);
2068
+ if (!gate.ok) {
2069
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
2070
+ return;
2071
+ }
2072
+ const ensured = await ensureActiveSession(deps, sessionId);
2073
+ if (!ensured.ok) {
2074
+ sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
2075
+ return;
2076
+ }
2077
+ const result = await deps.runtime.reloadSession(sessionId);
2078
+ if (!result.ok) {
2079
+ const status = result.code === "PI_SESSION_NOT_FOUND"
2080
+ ? 404
2081
+ : result.code === "PI_SESSION_BUSY"
2082
+ ? 409
2083
+ : result.code === "PI_SESSION_INIT_FAILED"
2084
+ ? 503
2085
+ : 500;
2086
+ sendJson(res, status, { ok: false, error: { code: result.code, message: result.message } });
2087
+ return;
2088
+ }
2089
+ // RF-02: the reload succeeded, but a failed snapshot read is still a
2090
+ // structured 503 — never a 200 with fake-empty resources.
2091
+ let snapshot;
2092
+ try {
2093
+ snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
2094
+ }
2095
+ catch (error) {
2096
+ if (sendRuntimeUnavailable(res, error))
2097
+ return;
2098
+ throw error;
2099
+ }
2100
+ sendJson(res, 200, {
2101
+ ok: true,
2102
+ revision: result.revision,
2103
+ snapshot,
2104
+ resourceCounts: snapshot
2105
+ ? {
2106
+ skills: snapshot.skills.length,
2107
+ extensions: snapshot.extensions.length,
2108
+ packages: snapshot.packages.length,
2109
+ contextFiles: snapshot.contextFiles.length,
2110
+ }
2111
+ : undefined,
2112
+ });
2113
+ }
2114
+ /**
2115
+ * GET /sessions/:id/runtime-snapshot (AC-01): the unified live snapshot.
2116
+ */
2117
+ export async function handleRuntimeSnapshot(_res, res, deps, sessionId) {
2118
+ const ensured = await ensureActiveSession(deps, sessionId);
2119
+ if (!ensured.ok) {
2120
+ sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
2121
+ return;
2122
+ }
2123
+ // RF-02: a critical snapshot projection failure is 503 PI_RUNTIME_UNAVAILABLE
2124
+ // — never a 200 fake-empty snapshot.
2125
+ let snapshot;
2126
+ try {
2127
+ snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
2128
+ }
2129
+ catch (error) {
2130
+ if (sendRuntimeUnavailable(res, error))
2131
+ return;
2132
+ throw error;
2133
+ }
2134
+ if (!snapshot) {
2135
+ sendJson(res, 503, {
2136
+ ok: false,
2137
+ error: { code: "PI_RUNTIME_UNAVAILABLE", message: `chat session not active: ${sessionId}` },
2138
+ });
2139
+ return;
2140
+ }
2141
+ sendJson(res, 200, { ok: true, snapshot });
2142
+ }
2143
+ async function handleLinkWorkspaceOperation(req, res, deps, sessionId) {
2144
+ const gate = gateMutation(req, deps);
2145
+ if (!gate.ok) {
2146
+ sendJson(res, gate.status, {
2147
+ ok: false,
2148
+ error: { code: gate.code, message: gate.message },
2149
+ });
2150
+ return;
2151
+ }
2152
+ if (!(await requireChatSession(res, deps, sessionId)))
2153
+ return;
2154
+ try {
2155
+ const body = await readJsonBody(req);
2156
+ const operationId = typeof body.operationId === "string" ? body.operationId.trim() : "";
2157
+ if (!operationId) {
2158
+ sendJson(res, 400, {
2159
+ ok: false,
2160
+ error: { code: "INVALID_INPUT", message: "operationId is required" },
2161
+ });
2162
+ return;
2163
+ }
2164
+ const operations = deps.runtime.actionContext?.operations;
2165
+ if (!operations || !(await operations.get(operationId))) {
2166
+ sendJson(res, 404, {
2167
+ ok: false,
2168
+ error: {
2169
+ code: "NOT_FOUND",
2170
+ message: `operation not found: ${operationId}`,
2171
+ },
2172
+ });
2173
+ return;
2174
+ }
2175
+ await deps.operationLinker.link({
2176
+ sessionId,
2177
+ turnId: deps.events.latestTurnId(sessionId) ?? "workspace",
2178
+ toolCallId: `workspace-${operationId}`,
2179
+ operationId,
2180
+ });
2181
+ sendJson(res, 200, { ok: true });
2182
+ }
2183
+ catch (error) {
2184
+ sendJson(res, 400, {
2185
+ ok: false,
2186
+ error: {
2187
+ code: "INVALID_INPUT",
2188
+ message: error instanceof Error ? error.message : String(error),
2189
+ },
2190
+ });
2191
+ }
2192
+ }
2193
+ async function handleStartInterview(req, res, deps, sessionId) {
2194
+ const gate = gateMutation(req, deps);
2195
+ if (!gate.ok) {
2196
+ sendJson(res, gate.status, {
2197
+ ok: false,
2198
+ error: { code: gate.code, message: gate.message },
2199
+ });
2200
+ return;
2201
+ }
2202
+ if (!(await requireChatSession(res, deps, sessionId)))
2203
+ return;
2204
+ try {
2205
+ const body = await readJsonBody(req);
2206
+ const taskId = typeof body.taskId === "string" ? body.taskId.trim() : "";
2207
+ if (!taskId) {
2208
+ sendJson(res, 400, {
2209
+ ok: false,
2210
+ error: { code: "INVALID_INPUT", message: "taskId is required" },
2211
+ });
2212
+ return;
2213
+ }
2214
+ const adapter = new ChatInterviewAdapter(deps.appData, deps.events);
2215
+ const state = await adapter.start({
2216
+ operatorSessionId: sessionId,
2217
+ taskId,
2218
+ ...(typeof body.title === "string" ? { title: body.title } : {}),
2219
+ ...(body.initialDraft && typeof body.initialDraft === "object"
2220
+ ? { initialDraft: body.initialDraft }
2221
+ : {}),
2222
+ });
2223
+ sendJson(res, 201, { ok: true, ...projectChatInterviewState(state) });
2224
+ }
2225
+ catch (error) {
2226
+ sendJson(res, 400, {
2227
+ ok: false,
2228
+ error: {
2229
+ code: "INTERVIEW_START_FAILED",
2230
+ message: error instanceof Error ? error.message : String(error),
2231
+ },
2232
+ });
2233
+ }
2234
+ }
2235
+ async function handleAnswerInterview(req, res, deps, sessionId, interviewSessionId) {
2236
+ const gate = gateMutation(req, deps);
2237
+ if (!gate.ok) {
2238
+ sendJson(res, gate.status, {
2239
+ ok: false,
2240
+ error: { code: gate.code, message: gate.message },
2241
+ });
2242
+ return;
2243
+ }
2244
+ if (!(await requireChatSession(res, deps, sessionId)))
2245
+ return;
2246
+ try {
2247
+ const body = await readJsonBody(req);
2248
+ const questionId = typeof body.questionId === "string" ? body.questionId : "";
2249
+ const response = typeof body.response === "string" ? body.response : "";
2250
+ if (!questionId || !response) {
2251
+ sendJson(res, 400, {
2252
+ ok: false,
2253
+ error: {
2254
+ code: "INVALID_INPUT",
2255
+ message: "questionId and response are required",
2256
+ },
2257
+ });
2258
+ return;
2259
+ }
2260
+ const state = await new ChatInterviewAdapter(deps.appData, deps.events).answer({
2261
+ operatorSessionId: sessionId,
2262
+ interviewSessionId,
2263
+ questionId,
2264
+ response,
2265
+ ...(typeof body.text === "string" ? { text: body.text } : {}),
2266
+ });
2267
+ sendJson(res, 200, { ok: true, ...projectChatInterviewState(state) });
2268
+ }
2269
+ catch (error) {
2270
+ sendJson(res, 400, {
2271
+ ok: false,
2272
+ error: {
2273
+ code: "INTERVIEW_ANSWER_FAILED",
2274
+ message: error instanceof Error ? error.message : String(error),
2275
+ },
2276
+ });
2277
+ }
2278
+ }
2279
+ async function handleGetInterviewFact(res, deps, sessionId, interviewSessionId, fact) {
2280
+ if (!(await requireChatSession(res, deps, sessionId)))
2281
+ return;
2282
+ const state = await new ChatInterviewAdapter(deps.appData, deps.events).get(interviewSessionId);
2283
+ if (!state || state.interview.operatorSessionId !== sessionId) {
2284
+ sendJson(res, 404, {
2285
+ ok: false,
2286
+ error: {
2287
+ code: "NOT_FOUND",
2288
+ message: `interview session not found: ${interviewSessionId}`,
2289
+ },
2290
+ });
2291
+ return;
2292
+ }
2293
+ const projected = projectChatInterviewState(state);
2294
+ sendJson(res, 200, { ok: true, [fact]: projected[fact] });
2295
+ }
2296
+ async function ctxConfirmationCard(deps, sessionId, turnId, confirmationId) {
2297
+ const confirmation = await deps.runtime.actionContext?.confirmations.get(confirmationId);
2298
+ if (!confirmation)
2299
+ return;
2300
+ const taskId = String(confirmation.taskContractBinding.taskId ?? "");
2301
+ appendHumanGateCard(deps, sessionId, makeHumanGateCard({
2302
+ cardId: `dag-${confirmationId}`,
2303
+ gateType: "dag-run",
2304
+ state: confirmation.state === "confirmed" ? "confirmed" : "prepared",
2305
+ taskId,
2306
+ confirmationId,
2307
+ expiresAt: confirmation.expiresAt,
2308
+ inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
2309
+ humanGateToken: confirmation.humanGateToken,
2310
+ displayOnlyChallenges: [
2311
+ "source",
2312
+ "writer-scope",
2313
+ "verification",
2314
+ "human-gates",
2315
+ ],
2316
+ contractRevision: confirmation.taskContractBinding.revision,
2317
+ }));
2318
+ void turnId;
2319
+ }
2320
+ async function ctxMutationGateCard(deps, sessionId, receiptId) {
2321
+ const { MutationGateReceiptStore } = await import("../mutation-gate-receipt-store.js");
2322
+ const store = new MutationGateReceiptStore(deps.appData);
2323
+ const receipt = await store.get(receiptId);
2324
+ if (!receipt)
2325
+ return;
2326
+ appendHumanGateCard(deps, sessionId, makeHumanGateCard({
2327
+ cardId: `mutation-${receipt.receiptId}`,
2328
+ gateType: "mutation",
2329
+ state: "prepared",
2330
+ taskId: receipt.action,
2331
+ receiptId: receipt.receiptId,
2332
+ expiresAt: receipt.expiresAt,
2333
+ humanGateToken: receipt.humanGateToken,
2334
+ inspectHref: `/inspect/#/`,
2335
+ }));
2336
+ }
2337
+ function actionResultBody(result) {
2338
+ const body = result.body;
2339
+ return (body.result && typeof body.result === "object" ? body.result : body);
2340
+ }
2341
+ function appendHumanGateCard(deps, sessionId, card) {
2342
+ deps.events.append(sessionId, deps.events.latestTurnId(sessionId) ?? "human-gate", {
2343
+ kind: "human-gate-card",
2344
+ data: card,
2345
+ });
2346
+ }
2347
+ async function liveContractBinding(ctx, taskId) {
2348
+ const draft = await ctx.drafts.get(taskId);
2349
+ if (!draft)
2350
+ throw new Error(`draft not found: ${taskId}`);
2351
+ const shown = await dispatchOperatorAction(ctx, {
2352
+ action: "contractShow",
2353
+ actionParams: { taskId },
2354
+ });
2355
+ if (shown.kind === "error")
2356
+ throw new Error("contractShow failed");
2357
+ const result = actionResultBody(shown);
2358
+ const state = result.state;
2359
+ const revision = state?.ref?.revision ?? 0;
2360
+ const expectedObservedHash = state?.observedCanonicalHash ?? "0".repeat(64);
2361
+ return {
2362
+ taskId,
2363
+ revision,
2364
+ canonicalHash: state?.ref?.canonicalHash ?? expectedObservedHash,
2365
+ draftSha256: draft.draftSha256,
2366
+ expectedObservedHash,
2367
+ };
2368
+ }
2369
+ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
2370
+ const gate = gateMutation(req, deps);
2371
+ if (!gate.ok) {
2372
+ sendJson(res, gate.status, {
2373
+ ok: false,
2374
+ error: { code: gate.code, message: gate.message },
2375
+ });
2376
+ return;
2377
+ }
2378
+ if (!(await requireChatSession(res, deps, sessionId)))
2379
+ return;
2380
+ const ctx = deps.runtime.actionContext;
2381
+ if (!ctx) {
2382
+ sendJson(res, 503, {
2383
+ ok: false,
2384
+ error: {
2385
+ code: "CHAT_NO_ACTION_CONTEXT",
2386
+ message: "Chat runtime has no operator action context wired",
2387
+ },
2388
+ });
2389
+ return;
2390
+ }
2391
+ try {
2392
+ const body = await readJsonBody(req);
2393
+ const receipts = new ContractApplyReceiptStore(deps.appData);
2394
+ if (step === "prepare") {
2395
+ const taskId = typeof body.taskId === "string" ? body.taskId.trim() : "";
2396
+ if (!taskId)
2397
+ throw new Error("taskId is required");
2398
+ const binding = await liveContractBinding(ctx, taskId);
2399
+ const expiresAt = new Date(Date.now() + 30 * 60_000).toISOString();
2400
+ const receiptId = `apply_${crypto.randomUUID()}`;
2401
+ const humanGateToken = issueHumanGateToken({
2402
+ confirmationId: receiptId,
2403
+ payloadHash: contractApplyPayloadHash(binding),
2404
+ expiresAt,
2405
+ }, deps.bootToken.confirmationToken);
2406
+ const receipt = await receipts.prepare({
2407
+ receiptId,
2408
+ operatorSessionId: sessionId,
2409
+ ...binding,
2410
+ expiresAt,
2411
+ humanGateToken,
2412
+ });
2413
+ const card = makeHumanGateCard({
2414
+ cardId: `contract-${receipt.receiptId}`,
2415
+ gateType: "contract-apply",
2416
+ state: "prepared",
2417
+ taskId,
2418
+ receiptId: receipt.receiptId,
2419
+ expiresAt,
2420
+ humanGateToken,
2421
+ contractRevision: binding.revision,
2422
+ contractDiffSummary: `revision ${binding.revision}; draft ${binding.draftSha256.slice(0, 12)}; observed ${binding.expectedObservedHash.slice(0, 12)}`,
2423
+ inspectHref: `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
2424
+ });
2425
+ appendHumanGateCard(deps, sessionId, card);
2426
+ sendJson(res, 201, { ok: true, card });
2427
+ return;
2428
+ }
2429
+ const receiptId = typeof body.receiptId === "string" ? body.receiptId : "";
2430
+ const receipt = await receipts.get(receiptId);
2431
+ if (!receipt || receipt.operatorSessionId !== sessionId) {
2432
+ sendJson(res, 404, {
2433
+ ok: false,
2434
+ error: {
2435
+ code: "NOT_FOUND",
2436
+ message: "contract apply receipt not found",
2437
+ },
2438
+ });
2439
+ return;
2440
+ }
2441
+ const current = await liveContractBinding(ctx, receipt.taskId);
2442
+ const check = verifyHumanGateToken(body.humanGateToken, {
2443
+ confirmationId: receipt.receiptId,
2444
+ payloadHash: contractApplyPayloadHash(current),
2445
+ }, deps.bootToken.confirmationToken);
2446
+ if (!check.ok) {
2447
+ sendJson(res, 403, {
2448
+ ok: false,
2449
+ error: { code: check.code, message: check.message },
2450
+ });
2451
+ return;
2452
+ }
2453
+ const reserved = await receipts.tryReserve(receiptId);
2454
+ if (!reserved.ok) {
2455
+ sendJson(res, reserved.code === "NOT_FOUND"
2456
+ ? 404
2457
+ : reserved.code === "EXPIRED"
2458
+ ? 410
2459
+ : 409, {
2460
+ ok: false,
2461
+ error: { code: reserved.code, message: reserved.message },
2462
+ });
2463
+ return;
2464
+ }
2465
+ if (!reserved.reserved) {
2466
+ const operationId = reserved.receipt.operationId;
2467
+ const card = makeHumanGateCard({
2468
+ cardId: `contract-${receiptId}`,
2469
+ gateType: "contract-apply",
2470
+ state: "dispatched",
2471
+ taskId: receipt.taskId,
2472
+ receiptId,
2473
+ expiresAt: receipt.expiresAt,
2474
+ contractRevision: receipt.revision,
2475
+ operationId,
2476
+ inspectHref: operationId
2477
+ ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
2478
+ : `/inspect/?taskId=${encodeURIComponent(receipt.taskId)}#/`,
2479
+ });
2480
+ appendHumanGateCard(deps, sessionId, card);
2481
+ sendJson(res, 202, {
2482
+ ok: true,
2483
+ card,
2484
+ result: operationId
2485
+ ? {
2486
+ operationId,
2487
+ state: "accepted",
2488
+ action: "contractApply",
2489
+ clientRequestId: `chat-apply-${receiptId}`,
2490
+ }
2491
+ : { state: reserved.reason },
2492
+ });
2493
+ return;
2494
+ }
2495
+ const clientRequestId = typeof body.clientRequestId === "string" && body.clientRequestId.trim()
2496
+ ? body.clientRequestId.trim()
2497
+ : `chat-apply-${receiptId}`;
2498
+ const result = await dispatchOperatorAction(ctx, {
2499
+ action: "contractApply",
2500
+ actionParams: {
2501
+ taskId: receipt.taskId,
2502
+ expectedRevision: receipt.revision,
2503
+ expectedObservedHash: receipt.expectedObservedHash,
2504
+ },
2505
+ clientRequestId,
2506
+ });
2507
+ if (result.kind === "error") {
2508
+ await receipts.releaseToPrepared(receiptId);
2509
+ sendJson(res, result.status, result.body);
2510
+ return;
2511
+ }
2512
+ const operationId = result.kind === "accepted" ? result.body.operationId : undefined;
2513
+ await receipts.markDispatched(receiptId, operationId);
2514
+ const card = makeHumanGateCard({
2515
+ cardId: `contract-${receiptId}`,
2516
+ gateType: "contract-apply",
2517
+ state: "dispatched",
2518
+ taskId: receipt.taskId,
2519
+ receiptId,
2520
+ expiresAt: receipt.expiresAt,
2521
+ contractRevision: receipt.revision,
2522
+ operationId,
2523
+ inspectHref: operationId
2524
+ ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
2525
+ : `/inspect/?taskId=${encodeURIComponent(receipt.taskId)}#/`,
2526
+ });
2527
+ appendHumanGateCard(deps, sessionId, card);
2528
+ if (operationId)
2529
+ await deps.operationLinker.link({
2530
+ sessionId,
2531
+ turnId: deps.events.latestTurnId(sessionId) ?? "human-gate",
2532
+ toolCallId: "human-contract-apply",
2533
+ operationId,
2534
+ });
2535
+ sendJson(res, result.status, {
2536
+ ok: true,
2537
+ card,
2538
+ result: actionResultBody(result),
2539
+ });
2540
+ }
2541
+ catch (error) {
2542
+ sendJson(res, 400, {
2543
+ ok: false,
2544
+ error: {
2545
+ code: "HUMAN_GATE_FAILED",
2546
+ message: error instanceof Error ? error.message : String(error),
2547
+ },
2548
+ });
2549
+ }
2550
+ }
2551
+ async function handleDagHumanGate(req, res, deps, sessionId, step) {
2552
+ const gate = gateMutation(req, deps);
2553
+ if (!gate.ok) {
2554
+ sendJson(res, gate.status, {
2555
+ ok: false,
2556
+ error: { code: gate.code, message: gate.message },
2557
+ });
2558
+ return;
2559
+ }
2560
+ if (!(await requireChatSession(res, deps, sessionId)))
2561
+ return;
2562
+ const ctx = deps.runtime.actionContext;
2563
+ if (!ctx) {
2564
+ sendJson(res, 503, {
2565
+ ok: false,
2566
+ error: {
2567
+ code: "CHAT_NO_ACTION_CONTEXT",
2568
+ message: "Chat runtime has no operator action context wired",
2569
+ },
2570
+ });
2571
+ return;
2572
+ }
2573
+ try {
2574
+ const body = await readJsonBody(req);
2575
+ const action = step === "prepare"
2576
+ ? "prepareDagConfirmation"
2577
+ : step === "confirm"
2578
+ ? "confirmDagConfirmation"
2579
+ : "runDag";
2580
+ const result = await dispatchOperatorAction(ctx, {
2581
+ action,
2582
+ actionParams: body,
2583
+ clientRequestId: typeof body.clientRequestId === "string"
2584
+ ? body.clientRequestId
2585
+ : `chat-${sessionId}-${Date.now()}`,
2586
+ });
2587
+ if (result.kind === "error") {
2588
+ sendJson(res, result.status, result.body);
2589
+ return;
2590
+ }
2591
+ const projected = actionResultBody(result);
2592
+ const confirmation = (projected.confirmation ??
2593
+ (await ctx.confirmations.get(String(body.confirmationId ?? ""))));
2594
+ const confirmationId = String(confirmation?.confirmationId ?? body.confirmationId ?? "");
2595
+ const taskBinding = confirmation?.taskContractBinding;
2596
+ const taskId = String(taskBinding?.taskId ?? body.taskId ?? "");
2597
+ const state = step === "prepare"
2598
+ ? "prepared"
2599
+ : step === "confirm"
2600
+ ? "confirmed"
2601
+ : "dispatched";
2602
+ const operationId = result.kind === "accepted" ? result.body.operationId : undefined;
2603
+ const dagText = typeof body.dagText === "string"
2604
+ ? body.dagText
2605
+ : typeof body.dagJson === "string"
2606
+ ? body.dagJson
2607
+ : "";
2608
+ let dagSpine;
2609
+ try {
2610
+ const parsed = JSON.parse(dagText);
2611
+ dagSpine = parsed.nodes
2612
+ ?.map((node) => String(node.id ?? ""))
2613
+ .filter(Boolean);
2614
+ }
2615
+ catch {
2616
+ /* display summary is optional */
2617
+ }
2618
+ const card = makeHumanGateCard({
2619
+ cardId: `dag-${confirmationId}`,
2620
+ gateType: "dag-run",
2621
+ state,
2622
+ taskId,
2623
+ confirmationId,
2624
+ expiresAt: String(confirmation?.expiresAt ?? new Date().toISOString()),
2625
+ inspectHref: operationId
2626
+ ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
2627
+ : `/inspect/?taskId=${encodeURIComponent(taskId)}#/`,
2628
+ ...(confirmation?.humanGateToken
2629
+ ? { humanGateToken: confirmation.humanGateToken }
2630
+ : {}),
2631
+ displayOnlyChallenges: Array.isArray(projected.requiredChallenges)
2632
+ ? projected.requiredChallenges.map(String)
2633
+ : undefined,
2634
+ contractRevision: typeof taskBinding?.revision === "number"
2635
+ ? taskBinding.revision
2636
+ : undefined,
2637
+ dagSpine,
2638
+ operationId,
2639
+ });
2640
+ appendHumanGateCard(deps, sessionId, card);
2641
+ if (operationId)
2642
+ await deps.operationLinker.link({
2643
+ sessionId,
2644
+ turnId: deps.events.latestTurnId(sessionId) ?? "human-gate",
2645
+ toolCallId: `human-${step}`,
2646
+ operationId,
2647
+ });
2648
+ sendJson(res, result.status, { ok: true, card, result: projected });
2649
+ }
2650
+ catch (error) {
2651
+ sendJson(res, 400, {
2652
+ ok: false,
2653
+ error: {
2654
+ code: "HUMAN_GATE_FAILED",
2655
+ message: error instanceof Error ? error.message : String(error),
2656
+ },
2657
+ });
2658
+ }
2659
+ }
2660
+ async function handleMutationHumanGate(req, res, deps, sessionId) {
2661
+ const gate = gateMutation(req, deps);
2662
+ if (!gate.ok) {
2663
+ sendJson(res, gate.status, {
2664
+ ok: false,
2665
+ error: { code: gate.code, message: gate.message },
2666
+ });
2667
+ return;
2668
+ }
2669
+ if (!(await requireChatSession(res, deps, sessionId)))
2670
+ return;
2671
+ const ctx = deps.runtime.actionContext;
2672
+ if (!ctx) {
2673
+ sendJson(res, 503, {
2674
+ ok: false,
2675
+ error: {
2676
+ code: "CHAT_NO_ACTION_CONTEXT",
2677
+ message: "Chat runtime has no operator action context wired",
2678
+ },
2679
+ });
2680
+ return;
2681
+ }
2682
+ try {
2683
+ const body = await readJsonBody(req);
2684
+ const receiptId = typeof body.receiptId === "string" ? body.receiptId : "";
2685
+ const { MutationGateReceiptStore } = await import("../mutation-gate-receipt-store.js");
2686
+ const store = new MutationGateReceiptStore(deps.appData);
2687
+ const receipt = await store.get(receiptId);
2688
+ if (!receipt || receipt.operatorSessionId !== ctx.operatorSessionId) {
2689
+ sendJson(res, 404, {
2690
+ ok: false,
2691
+ error: {
2692
+ code: "NOT_FOUND",
2693
+ message: "mutation gate receipt not found",
2694
+ },
2695
+ });
2696
+ return;
2697
+ }
2698
+ const result = await dispatchOperatorAction(ctx, {
2699
+ action: receipt.action,
2700
+ actionParams: {
2701
+ ...receipt.actionParams,
2702
+ confirmationId: receipt.receiptId,
2703
+ humanGateToken: body.humanGateToken ?? receipt.humanGateToken,
2704
+ },
2705
+ clientRequestId: `chat-mutation-${receipt.receiptId}`,
2706
+ });
2707
+ if (result.kind === "error") {
2708
+ sendJson(res, result.status, result.body);
2709
+ return;
2710
+ }
2711
+ const operationId = result.kind === "accepted" ? result.body.operationId : undefined;
2712
+ const card = makeHumanGateCard({
2713
+ cardId: `mutation-${receiptId}`,
2714
+ gateType: "mutation",
2715
+ state: "dispatched",
2716
+ taskId: receipt.action,
2717
+ receiptId,
2718
+ expiresAt: receipt.expiresAt,
2719
+ operationId,
2720
+ inspectHref: operationId
2721
+ ? `/inspect/?operationId=${encodeURIComponent(operationId)}#/`
2722
+ : `/inspect/#/`,
2723
+ });
2724
+ appendHumanGateCard(deps, sessionId, card);
2725
+ if (operationId) {
2726
+ await deps.operationLinker.link({
2727
+ sessionId,
2728
+ turnId: deps.events.latestTurnId(sessionId) ?? "human-gate",
2729
+ toolCallId: "human-mutation-apply",
2730
+ operationId,
2731
+ });
2732
+ }
2733
+ sendJson(res, result.status, {
2734
+ ok: true,
2735
+ card,
2736
+ result: actionResultBody(result),
2737
+ });
2738
+ }
2739
+ catch (error) {
2740
+ sendJson(res, 400, {
2741
+ ok: false,
2742
+ error: {
2743
+ code: "HUMAN_GATE_FAILED",
2744
+ message: error instanceof Error ? error.message : String(error),
2745
+ },
2746
+ });
2747
+ }
2748
+ }
2749
+ async function handleConfirmTaskKind(req, res, deps, sessionId, interviewSessionId) {
2750
+ const gate = gateMutation(req, deps);
2751
+ if (!gate.ok) {
2752
+ sendJson(res, gate.status, {
2753
+ ok: false,
2754
+ error: { code: gate.code, message: gate.message },
2755
+ });
2756
+ return;
2757
+ }
2758
+ if (!(await requireChatSession(res, deps, sessionId)))
2759
+ return;
2760
+ try {
2761
+ const body = await readJsonBody(req);
2762
+ if (typeof body.taskKind !== "string" || !body.taskKind.trim()) {
2763
+ sendJson(res, 400, {
2764
+ ok: false,
2765
+ error: { code: "INVALID_INPUT", message: "taskKind is required" },
2766
+ });
2767
+ return;
2768
+ }
2769
+ const state = await new ChatInterviewAdapter(deps.appData, deps.events).confirmTaskKind({
2770
+ operatorSessionId: sessionId,
2771
+ interviewSessionId,
2772
+ taskKind: body.taskKind,
2773
+ });
2774
+ sendJson(res, 200, { ok: true, ...projectChatInterviewState(state) });
2775
+ }
2776
+ catch (error) {
2777
+ sendJson(res, 400, {
2778
+ ok: false,
2779
+ error: {
2780
+ code: "TASK_KIND_CONFIRM_FAILED",
2781
+ message: error instanceof Error ? error.message : String(error),
2782
+ },
2783
+ });
2784
+ }
2785
+ }
2786
+ export async function handleChatCompact(req, res, deps, sessionId) {
2787
+ const gate = gateMutation(req, deps);
2788
+ if (!gate.ok) {
2789
+ sendJson(res, gate.status, {
2790
+ ok: false,
2791
+ error: { code: gate.code, message: gate.message },
2792
+ });
2793
+ return;
2794
+ }
2795
+ if (deps.events.getActiveTurn(sessionId)) {
2796
+ sendJson(res, 409, {
2797
+ ok: false,
2798
+ error: { code: "TURN_ACTIVE", message: "cannot compact an active turn" },
2799
+ });
2800
+ return;
2801
+ }
2802
+ const record = await deps.store.get(sessionId);
2803
+ if (!record) {
2804
+ sendJson(res, 404, {
2805
+ ok: false,
2806
+ error: {
2807
+ code: "NOT_FOUND",
2808
+ message: `chat session not found: ${sessionId}`,
2809
+ },
2810
+ });
2811
+ return;
2812
+ }
2813
+ if (!deps.runtime.hasSession(sessionId)) {
2814
+ const reopened = await deps.runtime.reopenSession({
2815
+ sessionId,
2816
+ sessionFile: record.sessionFile,
2817
+ ...(record.modelProvider && record.modelId
2818
+ ? { model: { provider: record.modelProvider, modelId: record.modelId } }
2819
+ : {}),
2820
+ });
2821
+ if (!reopened.ok) {
2822
+ const mapped = mapReopenSessionFailure({
2823
+ code: reopened.code,
2824
+ message: reopened.message,
2825
+ });
2826
+ sendJson(res, mapped.status, {
2827
+ ok: false,
2828
+ error: { code: mapped.code, message: mapped.message },
2829
+ });
2830
+ return;
2831
+ }
2832
+ }
2833
+ let body = {};
2834
+ try {
2835
+ body = await readJsonBody(req);
2836
+ }
2837
+ catch {
2838
+ sendJson(res, 400, {
2839
+ ok: false,
2840
+ error: { code: "INVALID_INPUT", message: "invalid json body" },
2841
+ });
2842
+ return;
2843
+ }
2844
+ const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
2845
+ const customInstructions = rawInstructions?.trim() || undefined;
2846
+ try {
2847
+ const snapshot = await deps.runtime.compact(sessionId, customInstructions);
2848
+ const event = deps.events.append(sessionId, deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`, { kind: "compact", data: snapshot });
2849
+ sendJson(res, 200, { ok: true, snapshot, eventId: event.eventId });
2850
+ }
2851
+ catch (error) {
2852
+ const classified = classifyChatCompactionFailure(error);
2853
+ sendJson(res, classified.status, {
2854
+ ok: false,
2855
+ error: { code: classified.code, message: classified.message },
2856
+ });
2857
+ }
2858
+ }
2859
+ export async function handleChatMessages(res, deps, sessionId, url) {
2860
+ try {
2861
+ const limit = Number(url.searchParams.get("limit") ?? 30);
2862
+ const page = await deps.store.listMessages(sessionId, {
2863
+ limit: Number.isFinite(limit) ? limit : 30,
2864
+ beforeId: url.searchParams.get("beforeId") ?? undefined,
2865
+ });
2866
+ sendJson(res, 200, { ok: true, ...page });
2867
+ }
2868
+ catch (error) {
2869
+ sendJson(res, 404, {
2870
+ ok: false,
2871
+ error: {
2872
+ code: "NOT_FOUND",
2873
+ message: error instanceof Error ? error.message : String(error),
2874
+ },
2875
+ });
2876
+ }
2877
+ }
2878
+ /**
2879
+ * Dispatch a chat HTTP request. Returns true if handled.
2880
+ * Mounted under /api/operator/v1/chat/*.
2881
+ */
2882
+ export async function handleChatRequest(req, res, deps, pathname) {
2883
+ if (!deps)
2884
+ return false;
2885
+ if (!pathname.startsWith("/api/operator/v1/chat/") &&
2886
+ !pathname.startsWith("/api/operator/v1/pi/"))
2887
+ return false;
2888
+ const method = (req.method ?? "GET").toUpperCase();
2889
+ const url = new URL(req.url ?? "/", "http://localhost");
2890
+ if (pathname === "/api/operator/v1/pi/models-config/test" &&
2891
+ method === "POST") {
2892
+ const gate = gateMutation(req, deps);
2893
+ if (!gate.ok) {
2894
+ sendJson(res, gate.status, {
2895
+ ok: false,
2896
+ error: { code: gate.code, message: gate.message },
2897
+ });
2898
+ return true;
2899
+ }
2900
+ try {
2901
+ const body = await readJsonBody(req);
2902
+ if (typeof body.provider !== "string" || typeof body.modelId !== "string")
2903
+ throw new Error("provider and modelId are required");
2904
+ sendJson(res, 200, {
2905
+ ok: true,
2906
+ data: await deps.runtime.testConfiguredModel({
2907
+ provider: body.provider,
2908
+ modelId: body.modelId,
2909
+ }),
2910
+ });
2911
+ }
2912
+ catch (error) {
2913
+ sendJson(res, 400, {
2914
+ ok: false,
2915
+ error: {
2916
+ code: "MODEL_TEST_FAILED",
2917
+ message: error instanceof Error ? error.message : String(error),
2918
+ },
2919
+ });
2920
+ }
2921
+ return true;
2922
+ }
2923
+ if (pathname === "/api/operator/v1/pi/models-config" &&
2924
+ (method === "GET" || method === "PATCH")) {
2925
+ await handlePiConfig(req, res, deps, "models");
2926
+ return true;
2927
+ }
2928
+ if (pathname === "/api/operator/v1/pi/packages" && method === "GET") {
2929
+ await handlePiConfig(req, res, deps, "packages");
2930
+ return true;
2931
+ }
2932
+ if (method === "GET" && pathname === "/api/operator/v1/chat/capabilities") {
2933
+ await handleChatCapabilities(req, res, deps);
2934
+ return true;
2935
+ }
2936
+ if (method === "GET" && pathname === "/api/operator/v1/chat/repo/tree") {
2937
+ await handleRepoBrowser(res, deps, "tree", url.searchParams.get("path") ?? "");
2938
+ return true;
2939
+ }
2940
+ if (method === "GET" && pathname === "/api/operator/v1/chat/repo/file") {
2941
+ const filePath = url.searchParams.get("path");
2942
+ if (!filePath)
2943
+ sendJson(res, 400, {
2944
+ ok: false,
2945
+ error: { code: "INVALID_PATH", message: "path is required" },
2946
+ });
2947
+ else
2948
+ await handleRepoBrowser(res, deps, "file", filePath);
2949
+ return true;
2950
+ }
2951
+ if (method === "GET" && pathname === "/api/operator/v1/chat/sessions") {
2952
+ await handleListChatSessions(req, res, deps);
2953
+ return true;
2954
+ }
2955
+ const interviewStart = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/interviews$/);
2956
+ if (method === "POST" && interviewStart) {
2957
+ await handleStartInterview(req, res, deps, decodeURIComponent(interviewStart[1]));
2958
+ return true;
2959
+ }
2960
+ const interviewAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/interviews\/([^/]+)\/answers$/);
2961
+ if (method === "POST" && interviewAnswer) {
2962
+ await handleAnswerInterview(req, res, deps, decodeURIComponent(interviewAnswer[1]), decodeURIComponent(interviewAnswer[2]));
2963
+ return true;
2964
+ }
2965
+ const interviewFact = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/interviews\/([^/]+)\/(draft|assessment)$/);
2966
+ if (method === "GET" && interviewFact) {
2967
+ await handleGetInterviewFact(res, deps, decodeURIComponent(interviewFact[1]), decodeURIComponent(interviewFact[2]), interviewFact[3]);
2968
+ return true;
2969
+ }
2970
+ const taskKindConfirm = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/interviews\/([^/]+)\/task-kind\/confirm$/);
2971
+ if (method === "POST" && taskKindConfirm) {
2972
+ await handleConfirmTaskKind(req, res, deps, decodeURIComponent(taskKindConfirm[1]), decodeURIComponent(taskKindConfirm[2]));
2973
+ return true;
2974
+ }
2975
+ const contractHumanGate = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/human-gate\/contract-apply(?:\/(prepare))?$/);
2976
+ if (method === "POST" && contractHumanGate) {
2977
+ await handleContractApplyHumanGate(req, res, deps, decodeURIComponent(contractHumanGate[1]), contractHumanGate[2] ? "prepare" : "apply");
2978
+ return true;
2979
+ }
2980
+ const dagHumanGate = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/human-gate\/dag-(prepare|confirm|run)$/);
2981
+ if (method === "POST" && dagHumanGate) {
2982
+ await handleDagHumanGate(req, res, deps, decodeURIComponent(dagHumanGate[1]), dagHumanGate[2]);
2983
+ return true;
2984
+ }
2985
+ const mutationHumanGate = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/human-gate\/mutation-apply$/);
2986
+ if (method === "POST" && mutationHumanGate) {
2987
+ await handleMutationHumanGate(req, res, deps, decodeURIComponent(mutationHumanGate[1]));
2988
+ return true;
2989
+ }
2990
+ const skillAutoInvocation = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/skills\/([^/]+)\/auto-invocation$/);
2991
+ if (method === "PATCH" && skillAutoInvocation) {
2992
+ await handleSkillAutoInvocation(req, res, deps, decodeURIComponent(skillAutoInvocation[1]), decodeURIComponent(skillAutoInvocation[2]));
2993
+ return true;
2994
+ }
2995
+ const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|runtime-snapshot|models|model|reload|commands)$/);
2996
+ if (m4Route) {
2997
+ const id = decodeURIComponent(m4Route[1]);
2998
+ const route = m4Route[2];
2999
+ if (route === "draft" && method === "PUT") {
3000
+ await handleComposerDraft(req, res, deps, id);
3001
+ return true;
3002
+ }
3003
+ if (route === "files" && method === "GET") {
3004
+ await handleChatFiles(res, deps, id, url.searchParams.get("q") ?? "");
3005
+ return true;
3006
+ }
3007
+ if (route === "runtime-context" && method === "GET") {
3008
+ await handleRuntimeContext(res, deps, id);
3009
+ return true;
3010
+ }
3011
+ if (route === "runtime-snapshot" && method === "GET") {
3012
+ await handleRuntimeSnapshot(req, res, deps, id);
3013
+ return true;
3014
+ }
3015
+ if (route === "models" && method === "GET") {
3016
+ await handleModels(req, res, deps, id);
3017
+ return true;
3018
+ }
3019
+ if (route === "model" && method === "POST") {
3020
+ await handleModels(req, res, deps, id);
3021
+ return true;
3022
+ }
3023
+ if (route === "reload" && method === "POST") {
3024
+ await handleReloadSession(req, res, deps, id);
3025
+ return true;
3026
+ }
3027
+ if (route === "commands" && method === "GET") {
3028
+ await handleChatCommands(res, deps, id);
3029
+ return true;
3030
+ }
3031
+ }
3032
+ const sessionMainline = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/branch\/mainline$/);
3033
+ if (method === "POST" && sessionMainline) {
3034
+ await handleSwitchMainlineChatSession(req, res, deps, decodeURIComponent(sessionMainline[1]));
3035
+ return true;
3036
+ }
3037
+ const sessionFork = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/fork$/);
3038
+ if (method === "POST" && sessionFork) {
3039
+ await handleForkChatSession(req, res, deps, decodeURIComponent(sessionFork[1]));
3040
+ return true;
3041
+ }
3042
+ const sessionBranch = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/branch$/);
3043
+ if (method === "POST" && sessionBranch) {
3044
+ await handleBranchChatSession(req, res, deps, decodeURIComponent(sessionBranch[1]));
3045
+ return true;
3046
+ }
3047
+ const sessionReopen = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/reopen$/);
3048
+ if (method === "POST" && sessionReopen) {
3049
+ await handleReopenChatSession(req, res, deps, decodeURIComponent(sessionReopen[1]));
3050
+ return true;
3051
+ }
3052
+ const sessionTurn = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/turns$/);
3053
+ if (method === "POST" && sessionTurn) {
3054
+ await handleCreateChatTurn(req, res, deps, decodeURIComponent(sessionTurn[1]));
3055
+ return true;
3056
+ }
3057
+ const sessionTurnAbort = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/turns\/([^/]+)\/abort$/);
3058
+ if (method === "POST" && sessionTurnAbort) {
3059
+ await handleAbortChatTurn(req, res, deps, decodeURIComponent(sessionTurnAbort[1]), decodeURIComponent(sessionTurnAbort[2]));
3060
+ return true;
3061
+ }
3062
+ const sessionTurnDetail = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/turns\/([^/]+)$/);
3063
+ if (method === "GET" && sessionTurnDetail) {
3064
+ await handleGetChatTurn(req, res, deps, decodeURIComponent(sessionTurnDetail[1]), decodeURIComponent(sessionTurnDetail[2]));
3065
+ return true;
3066
+ }
3067
+ const operationLink = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/operation-links$/);
3068
+ if (method === "POST" && operationLink) {
3069
+ await handleLinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLink[1]));
3070
+ return true;
3071
+ }
3072
+ const sessionCompact = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/compact$/);
3073
+ if (method === "POST" && sessionCompact) {
3074
+ await handleChatCompact(req, res, deps, decodeURIComponent(sessionCompact[1]));
3075
+ return true;
3076
+ }
3077
+ const sessionMessages = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/messages$/);
3078
+ if (method === "GET" && sessionMessages) {
3079
+ await handleChatMessages(res, deps, decodeURIComponent(sessionMessages[1]), url);
3080
+ return true;
3081
+ }
3082
+ const sessionPrompt = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/prompt$/);
3083
+ if (method === "POST" && sessionPrompt) {
3084
+ await handleChatPrompt(req, res, deps, decodeURIComponent(sessionPrompt[1]));
3085
+ return true;
3086
+ }
3087
+ const sessionContext = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/context$/);
3088
+ if (method === "GET" && sessionContext) {
3089
+ await handleGetTaskContext(req, res, deps, decodeURIComponent(sessionContext[1]));
3090
+ return true;
3091
+ }
3092
+ const sessionState = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/state$/);
3093
+ if (method === "GET" && sessionState) {
3094
+ await handleGetChatSessionState(req, res, deps, decodeURIComponent(sessionState[1]));
3095
+ return true;
3096
+ }
3097
+ const sessionEvents = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/events$/);
3098
+ if (method === "GET" && sessionEvents) {
3099
+ await handleChatEventsStream(req, res, deps, decodeURIComponent(sessionEvents[1]));
3100
+ return true;
3101
+ }
3102
+ const sessionMatch = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)$/);
3103
+ if (sessionMatch) {
3104
+ const id = decodeURIComponent(sessionMatch[1]);
3105
+ if (method === "GET") {
3106
+ await handleGetChatSession(req, res, deps, id);
3107
+ return true;
3108
+ }
3109
+ if (method === "PATCH") {
3110
+ await handlePatchChatSession(req, res, deps, id);
3111
+ return true;
3112
+ }
3113
+ if (method === "DELETE") {
3114
+ await handleDeleteChatSession(req, res, deps, id);
3115
+ return true;
3116
+ }
3117
+ }
3118
+ if (method === "POST" && pathname === "/api/operator/v1/chat/sessions") {
3119
+ await handleCreateChatSession(req, res, deps);
3120
+ return true;
3121
+ }
3122
+ if (isMutationMethod(method)) {
3123
+ // Already gated above for known routes; unknown mutation routes 404.
3124
+ sendJson(res, 404, {
3125
+ ok: false,
3126
+ error: { code: "NOT_FOUND", message: `unknown chat route: ${pathname}` },
3127
+ });
3128
+ return true;
3129
+ }
3130
+ sendJson(res, 404, {
3131
+ ok: false,
3132
+ error: { code: "NOT_FOUND", message: `unknown chat route: ${pathname}` },
3133
+ });
3134
+ return true;
3135
+ }
3136
+ // Re-export for route wiring; writeSseEvent kept for parity with operation-sse.
3137
+ void writeSseEvent;