@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3

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 (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1645 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1150 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +42 -16
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +347 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +572 -500
  164. package/dist/workflows/dag/node-execution.js +307 -70
  165. package/dist/workflows/dag/prompt.js +10 -18
  166. package/dist/workflows/dag/rerun-plan.js +144 -5
  167. package/dist/workflows/dag/rerun-run.js +58 -6
  168. package/dist/workflows/dag/retry-policy.js +228 -4
  169. package/dist/workflows/dag/runner.js +314 -119
  170. package/dist/workflows/dag/scheduler.js +114 -16
  171. package/dist/workflows/dag/structured-output-repair.js +712 -0
  172. package/dist/workflows/dag/types.js +235 -23
  173. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  174. package/dist/workflows/dag/validate.js +60 -10
  175. package/docs/init-surface.manifest.json +5 -0
  176. package/docs/operations/README.md +1 -1
  177. package/docs/templates/agent-dag.schema.json +44 -0
  178. package/docs/templates/frontend-test-dag.json +8 -10
  179. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  180. package/docs/templates/init-managed-agents.md +13 -3
  181. package/harness.json +3 -3
  182. package/package.json +5 -1
  183. package/scripts/next-info.mjs +356 -0
  184. package/scripts/next-publish-gate.mjs +238 -0
  185. package/scripts/release-source-binding.mjs +251 -0
  186. package/skills/codebase-scout/SKILL.md +1 -1
  187. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  188. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  189. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  190. package/skills/frontend-implementation/SKILL.md +4 -4
  191. package/skills/frontend-implementation/references/design-spec.md +11 -7
  192. package/skills/frontend-implementation/references/node-contracts.md +20 -11
  193. package/skills/loop-agent/references/command-reference.md +11 -4
  194. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  195. package/skills/playwright-cli/SKILL.md +22 -0
  196. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  197. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  198. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  199. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  200. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  201. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  202. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -1,5 +1,9 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import path from "node:path";
3
+ import { assertFrontendSplitProjectionSuccessor, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
4
+ import { buildFrontendHumanDecisionProjection, consumeFrontendHumanDecision, materializeFrontendHumanDecision, parseFrontendHumanDecisionProjection, parseFrontendHumanDecisionReceipt, } from "../../workflows/dag/frontend-human-decision.js";
5
+ import { buildFrontendSplitAggregateCloseout, computeFrontendSplitDigest, finalizeFrontendSplitAggregate, frontendCommittedFactRefSchema, frontendSplitChildCloseoutFactsSchema, initializeFrontendSplitParent, } from "../../workflows/dag/frontend-split-orchestrator.js";
6
+ import { frontendCloseoutFactsSchema, frontendTestHandoffSchema, } from "../../workflows/dag/frontend-closeout.js";
3
7
  import { listJsonFiles, readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
4
8
  export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out"]);
5
9
  function operationFile(appData, operationId) {
@@ -15,16 +19,21 @@ export function computeActionPayloadHash(action, actionParams) {
15
19
  export function newOperationId() {
16
20
  return `op_${randomBytes(12).toString("hex")}`;
17
21
  }
22
+ const sharedWriteChains = new Map();
18
23
  export class OperationStore {
19
24
  appData;
20
- /**
21
- * Per-key write lock. Concurrent update/save/create on the same operation
22
- * or clientRequestId must not race rename() into the same JSON path
23
- * (Windows EPERM) or last-write-wins lost patches.
24
- */
25
- writeChains = new Map();
25
+ /** Shared per-store-root, per-key write lock. Multiple OperationStore instances
26
+ * in one worker process must observe the same CAS critical section. */
27
+ writeChains;
26
28
  constructor(appData) {
27
29
  this.appData = appData;
30
+ const root = path.resolve(appData.operations);
31
+ let chains = sharedWriteChains.get(root);
32
+ if (!chains) {
33
+ chains = new Map();
34
+ sharedWriteChains.set(root, chains);
35
+ }
36
+ this.writeChains = chains;
28
37
  }
29
38
  serialize(key, op) {
30
39
  const prev = this.writeChains.get(key) ?? Promise.resolve();
@@ -66,6 +75,7 @@ export class OperationStore {
66
75
  payloadHash,
67
76
  state: "queued",
68
77
  createdAt: now,
78
+ revision: 1,
69
79
  taskId: input.taskId,
70
80
  cliArgs: input.cliArgs,
71
81
  ...(input.externalCommand
@@ -77,11 +87,61 @@ export class OperationStore {
77
87
  return { kind: "created", operation };
78
88
  });
79
89
  }
90
+ /**
91
+ * Idempotently materialize a split child/integration operation with a
92
+ * caller-specified deterministic operationId (from the split manifest).
93
+ * Unlike create() (random op_<hex> id) and save/update (no create), this is
94
+ * the durable cross-restart idempotency boundary for the production
95
+ * FrontendSplitTaskMaterializer backend: an existing operation file with the
96
+ * same taskId + payloadHash is returned as {created:false}; any drift fails
97
+ * closed instead of overwriting or silently diverging.
98
+ */
99
+ async ensureSplitMaterializedOperation(input) {
100
+ return this.serialize(input.operationId, async () => {
101
+ const existing = await this.get(input.operationId);
102
+ if (existing) {
103
+ if (existing.taskId !== input.taskId ||
104
+ existing.payloadHash !== input.payloadHash) {
105
+ throw new Error("frontend split operation materialization payload drift");
106
+ }
107
+ return { operation: existing, created: false };
108
+ }
109
+ const now = new Date().toISOString();
110
+ const operation = {
111
+ schemaVersion: 1,
112
+ operationId: input.operationId,
113
+ clientRequestId: `split:${input.operationId}`,
114
+ action: input.action,
115
+ actionParams: input.actionParams,
116
+ payloadHash: input.payloadHash,
117
+ state: "queued",
118
+ createdAt: now,
119
+ revision: 1,
120
+ taskId: input.taskId,
121
+ ...(input.cliArgs ? { cliArgs: input.cliArgs } : {}),
122
+ };
123
+ await writeSecureJson(operationFile(this.appData, input.operationId), operation);
124
+ return { operation, created: true };
125
+ });
126
+ }
80
127
  async get(operationId) {
81
128
  return readJsonIfExists(operationFile(this.appData, operationId));
82
129
  }
83
130
  async save(operation) {
84
131
  return this.serialize(operation.operationId, async () => {
132
+ const current = await this.get(operation.operationId);
133
+ if (current?.revision !== undefined && operation.revision !== current.revision)
134
+ throw new Error("operation revision may not be changed by save");
135
+ if (operation.frontendHumanDecision !== undefined || current?.frontendHumanDecision !== undefined) {
136
+ if (!current || JSON.stringify(operation) !== JSON.stringify(current))
137
+ throw new Error("human-decision projection may only be mutated by canonical CAS");
138
+ return current;
139
+ }
140
+ if (operation.frontendSplit !== undefined || current?.frontendSplit !== undefined) {
141
+ if (!current || JSON.stringify(operation) !== JSON.stringify(current))
142
+ throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
143
+ return current;
144
+ }
85
145
  await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
86
146
  return operation;
87
147
  });
@@ -92,6 +152,10 @@ export class OperationStore {
92
152
  if (!current) {
93
153
  throw new Error(`operation not found: ${operationId}`);
94
154
  }
155
+ if ("frontendHumanDecision" in patch || (current.frontendHumanDecision !== undefined && ("state" in patch || "frontendExecution" in patch)))
156
+ throw new Error("human-decision projection may only be mutated by canonical CAS");
157
+ if ("frontendSplit" in patch || "revision" in patch || (current.frontendSplit !== undefined && ("state" in patch || "frontendExecution" in patch)))
158
+ throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
95
159
  const next = {
96
160
  ...current,
97
161
  ...patch,
@@ -106,6 +170,162 @@ export class OperationStore {
106
170
  return next;
107
171
  });
108
172
  }
173
+ async materializeFrontendHumanDecision(input) {
174
+ return this.serialize(input.operationId, async () => {
175
+ const current = await this.get(input.operationId);
176
+ if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision) {
177
+ const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.envelope.runId, revision: Math.max(1, input.expectedRecordRevision) });
178
+ return { outcome: "rejected", reason: "record revision conflict or legacy record", projection: fallback };
179
+ }
180
+ const projection = current.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: current.operationId, runId: input.envelope.runId, revision: current.revision });
181
+ const result = materializeFrontendHumanDecision({ projection, envelope: input.envelope, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest });
182
+ if (result.outcome !== "applied")
183
+ return result;
184
+ const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection, state: "running" };
185
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
186
+ return result;
187
+ });
188
+ }
189
+ async consumeFrontendHumanDecision(input) {
190
+ return this.serialize(input.operationId, async () => {
191
+ const current = await this.get(input.operationId);
192
+ if (!current || current.revision === undefined || !current.frontendHumanDecision) {
193
+ const fallback = buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
194
+ return { outcome: "rejected", reason: "record has no mutable human-decision projection", projection: fallback };
195
+ }
196
+ const projection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
197
+ if (current.revision !== input.expectedRecordRevision)
198
+ return { outcome: "rejected", reason: "record revision conflict", projection };
199
+ const result = consumeFrontendHumanDecision({ projection, receipt: input.receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
200
+ if (result.outcome !== "applied")
201
+ return result;
202
+ const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection };
203
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
204
+ return result;
205
+ });
206
+ }
207
+ async initializeFrontendHumanApprovedSplit(input) {
208
+ return this.serialize(input.operationId, async () => {
209
+ const current = await this.get(input.operationId);
210
+ const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
211
+ if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision || !current.frontendHumanDecision || !current.frontendExecution || current.frontendSplit)
212
+ return { outcome: "rejected", reason: "record is not an authoritative pending single-task split", projection: fallback };
213
+ const humanProjection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
214
+ if (humanProjection.status !== "blocked-human" || humanProjection.envelope.kind !== "split-materialization-approval" || humanProjection.digest !== input.expectedProjectionDigest)
215
+ return { outcome: "rejected", reason: "pending split envelope conflict", projection: humanProjection };
216
+ const receipt = parseFrontendHumanDecisionReceipt(input.receipt);
217
+ if (receipt.kind !== "split-materialization-approval")
218
+ return { outcome: "rejected", reason: "receipt is not a split approval", projection: humanProjection };
219
+ if (JSON.stringify(current.frontendExecution) !== JSON.stringify(input.execution) || input.execution.kind !== "single-task")
220
+ return { outcome: "rejected", reason: "single-task execution conflict", projection: humanProjection };
221
+ const consumed = consumeFrontendHumanDecision({ projection: humanProjection, receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
222
+ if (consumed.outcome !== "applied")
223
+ return consumed;
224
+ const initialized = initializeFrontendSplitParent({
225
+ execution: input.execution,
226
+ manifest: input.manifest,
227
+ expectedOperationRevision: input.execution.operationRevision,
228
+ successorOperationRevision: consumed.projection.revision,
229
+ envelopeTargetDigest: humanProjection.envelope.targetDigest,
230
+ activeWriterOperationId: input.activeWriterOperationId,
231
+ activeProviderOperationId: input.activeProviderOperationId,
232
+ authorization: { mode: "human", envelope: humanProjection.envelope, receipt },
233
+ });
234
+ if (initialized.outcome !== "applied")
235
+ return { outcome: "rejected", reason: initialized.reason, projection: humanProjection };
236
+ const splitProjection = frontendSplitProjectionSchema.parse({ revision: consumed.projection.revision, execution: initialized.execution, manifest: input.manifest, parent: initialized.parent });
237
+ const { frontendExecution: _singleTask, ...withoutSingleTask } = current;
238
+ const record = { ...withoutSingleTask, revision: consumed.projection.revision, frontendHumanDecision: consumed.projection, frontendSplit: splitProjection, state: "running" };
239
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
240
+ return { outcome: "applied", projection: splitProjection, humanDecision: consumed.projection };
241
+ });
242
+ }
243
+ async compareAndSwapFrontendSplit(input) {
244
+ return this.serialize(input.operationId, async () => {
245
+ const current = await this.get(input.operationId);
246
+ if (!current || current.revision === undefined || current.revision !== input.expectedRevision)
247
+ return { outcome: "conflict" };
248
+ const currentProjection = current.frontendSplit;
249
+ if (currentProjection) {
250
+ if (currentProjection.execution.digest !== input.expectedExecutionDigest || currentProjection.parent.digest !== input.expectedParentDigest)
251
+ return { outcome: "conflict" };
252
+ }
253
+ else if (input.expectedExecutionDigest || input.expectedParentDigest)
254
+ return { outcome: "conflict" };
255
+ const next = currentProjection
256
+ ? assertFrontendSplitProjectionSuccessor(currentProjection, input.next)
257
+ : frontendSplitProjectionSchema.parse(input.next);
258
+ if (next.revision !== current.revision + 1 || next.parent.operationId !== input.operationId)
259
+ throw new Error("split CAS next revision or operation binding mismatch");
260
+ const state = next.parent.operationState === "success" ? "succeeded" : next.parent.operationTerminal !== undefined ? "failed" : current.state;
261
+ const record = { ...current, revision: next.revision, frontendSplit: next, state };
262
+ await writeSecureJson(operationFile(this.appData, input.operationId), record);
263
+ return { outcome: "applied", projection: next };
264
+ });
265
+ }
266
+ async loadChildCloseoutFacts(input) {
267
+ const factsDir = input.closeoutRef.ref;
268
+ const facts = await readJsonIfExists(path.join(factsDir, "frontend-closeout-facts.json"));
269
+ if (facts === undefined)
270
+ return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: [], frontendTestHandoffRef: undefined });
271
+ const parsedFacts = frontendCloseoutFactsSchema.parse(facts);
272
+ if (computeFrontendSplitDigest(parsedFacts) !== input.closeoutRef.digest)
273
+ throw new Error(`child closeout facts digest mismatch: ${input.childId}`);
274
+ const handoff = await readJsonIfExists(path.join(factsDir, "frontend-test-handoff.json"));
275
+ const frontendTestHandoffRef = handoff === undefined ? undefined : computeFrontendSplitDigest(frontendTestHandoffSchema.parse(handoff));
276
+ return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: parsedFacts.residualRisks ?? [], frontendTestHandoffRef });
277
+ }
278
+ async settleFrontendSplitAggregate(input) {
279
+ return this.serialize(input.operationId, async () => {
280
+ const record = await this.get(input.operationId);
281
+ if (!record || record.revision === undefined || record.revision !== input.expectedRevision || !record.frontendSplit)
282
+ return { outcome: "conflict" };
283
+ const authoritative = frontendSplitProjectionSchema.parse(record.frontendSplit);
284
+ if (authoritative.parent.operationId !== input.operationId
285
+ || authoritative.revision !== input.expectedRevision
286
+ || authoritative.execution.digest !== input.expectedExecutionDigest
287
+ || authoritative.parent.digest !== input.expectedParentDigest
288
+ || authoritative.manifest.digest !== input.expectedManifestDigest
289
+ || authoritative.candidateBinding?.digest !== input.expectedCandidateBindingDigest
290
+ || authoritative.integrationBinding?.digest !== input.expectedIntegrationBindingDigest)
291
+ return { outcome: "conflict" };
292
+ if (!authoritative.candidateBinding)
293
+ return { outcome: "conflict" };
294
+ const expectedAggregate = buildFrontendSplitAggregateCloseout(authoritative.parent, authoritative.manifest, authoritative.candidateBinding, authoritative.integrationBinding, input.childCloseoutFacts);
295
+ if (JSON.stringify(expectedAggregate) !== JSON.stringify(input.aggregate))
296
+ return { outcome: "conflict" };
297
+ const transaction = await input.prepareAggregate({
298
+ operationId: authoritative.parent.operationId,
299
+ aggregate: expectedAggregate,
300
+ });
301
+ try {
302
+ const committedRef = frontendCommittedFactRefSchema.parse(transaction.committedRef);
303
+ const finalized = finalizeFrontendSplitAggregate(authoritative.parent, expectedAggregate, committedRef);
304
+ const next = assertFrontendSplitProjectionSuccessor(authoritative, { ...authoritative, revision: authoritative.revision + 1, parent: finalized.parent, execution: finalized.execution, aggregateCloseout: { value: expectedAggregate, committedRef } });
305
+ const nextRecord = { ...record, revision: next.revision, frontendSplit: next, state: "succeeded" };
306
+ await transaction.commit({
307
+ parentRecordPath: operationFile(this.appData, record.operationId),
308
+ parentRecord: nextRecord,
309
+ });
310
+ return { outcome: "applied", projection: next };
311
+ }
312
+ catch (error) {
313
+ await transaction.rollback();
314
+ throw error;
315
+ }
316
+ });
317
+ }
318
+ async initializeFrontendSplit(expectedRevision, projectionValue) {
319
+ const projection = frontendSplitProjectionSchema.parse(projectionValue);
320
+ return this.serialize(projection.parent.operationId, async () => {
321
+ const current = await this.get(projection.parent.operationId);
322
+ if (!current || current.revision === undefined || current.revision !== expectedRevision || current.frontendSplit !== undefined || projection.revision !== expectedRevision + 1)
323
+ return { outcome: "conflict" };
324
+ const record = { ...current, revision: projection.revision, frontendSplit: projection };
325
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
326
+ return { outcome: "applied", projection };
327
+ });
328
+ }
109
329
  async list() {
110
330
  const files = await listJsonFiles(this.appData.operations);
111
331
  const ops = [];
@@ -1,5 +1,6 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { buildOperatorCapabilitiesDocument, OFFICIAL_TAIL_ACTION_IDS, } from "../../shared/operator/capabilities.js";
3
+ import { DAG_RERUN_PROMPT_OVERRIDE_ENV, isDagRerunPromptOverrideEnabled, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
3
4
  import { buildOperatorResult, operatorFailed, operatorSucceeded, } from "../../shared/operator/envelope.js";
4
5
  import { CANONICAL_TASK_ID_PATTERN, formatLocalBusinessDate, getTaskPaths, loadTaskConfig, } from "../../task/runtime.js";
5
6
  import { diffTaskContract, validateDraftForTask, } from "../../task/contract/index.js";
@@ -401,6 +402,85 @@ async function runReadCli(ctx, args, command) {
401
402
  });
402
403
  }
403
404
  }
405
+ /**
406
+ * Parse and bound the experimental promptOverrides action param. The env
407
+ * switch is checked separately so disabled deployments fail closed before any
408
+ * staging happens. `mode` defaults to "replace"; "append" keeps the frozen
409
+ * base prompt and only appends the operator text as a marked addendum.
410
+ */
411
+ function parsePromptOverrideParams(raw) {
412
+ if (raw === undefined || raw === null)
413
+ return { kind: "ok", overrides: [] };
414
+ if (!Array.isArray(raw)) {
415
+ return {
416
+ kind: "error",
417
+ message: "promptOverrides must be an array of {nodeId, prompt, mode?}",
418
+ };
419
+ }
420
+ if (raw.length === 0)
421
+ return { kind: "ok", overrides: [] };
422
+ if (raw.length > 8) {
423
+ return { kind: "error", message: "promptOverrides supports at most 8 nodes" };
424
+ }
425
+ const overrides = [];
426
+ const seen = new Set();
427
+ for (const item of raw) {
428
+ if (!item || typeof item !== "object") {
429
+ return {
430
+ kind: "error",
431
+ message: "promptOverrides entries must be objects {nodeId, prompt, mode?}",
432
+ };
433
+ }
434
+ const record = item;
435
+ const nodeId = typeof record.nodeId === "string" ? record.nodeId.trim() : "";
436
+ const prompt = typeof record.prompt === "string" ? record.prompt : "";
437
+ const mode = record.mode === undefined || record.mode === null
438
+ ? undefined
439
+ : typeof record.mode === "string"
440
+ ? record.mode
441
+ : "invalid";
442
+ if (!nodeId) {
443
+ return { kind: "error", message: "promptOverrides nodeId is required" };
444
+ }
445
+ if (seen.has(nodeId)) {
446
+ return {
447
+ kind: "error",
448
+ message: `promptOverrides contains duplicate nodeId: ${nodeId}`,
449
+ };
450
+ }
451
+ seen.add(nodeId);
452
+ if (mode !== undefined && mode !== "append" && mode !== "replace") {
453
+ return {
454
+ kind: "error",
455
+ message: `promptOverrides mode for ${nodeId} must be append or replace`,
456
+ };
457
+ }
458
+ if (!prompt.trim() || prompt.length > MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS) {
459
+ return {
460
+ kind: "error",
461
+ message: `promptOverrides prompt for ${nodeId} must be 1-${MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS} characters`,
462
+ };
463
+ }
464
+ overrides.push(mode === undefined ? { nodeId, prompt } : { nodeId, prompt, mode });
465
+ }
466
+ return { kind: "ok", overrides };
467
+ }
468
+ /** Stage override bodies under app-data and build `--prompt-override` CLI
469
+ * args. The flag carries the mode: `--prompt-override-append` for append. */
470
+ async function buildPromptOverrideCliArgs(ctx, overrides) {
471
+ const args = [];
472
+ for (const override of overrides) {
473
+ const staged = await stageUtf8Text(ctx.appData, override.prompt, {
474
+ extension: ".md",
475
+ label: `prompt-override-${override.nodeId}`,
476
+ });
477
+ const flag = override.mode === "append"
478
+ ? "--prompt-override-append"
479
+ : "--prompt-override";
480
+ args.push(flag, `${override.nodeId}@${staged.path}`);
481
+ }
482
+ return args;
483
+ }
404
484
  async function acceptOperation(ctx, input) {
405
485
  const created = await ctx.operations.create({
406
486
  clientRequestId: input.clientRequestId,
@@ -456,6 +536,84 @@ async function acceptOperation(ctx, input) {
456
536
  },
457
537
  };
458
538
  }
539
+ function splitRuntimeMissing(action) {
540
+ return {
541
+ kind: "error",
542
+ status: 503,
543
+ body: operatorFailed({
544
+ command: action,
545
+ outcome: "blocked",
546
+ code: "INTERNAL_ERROR",
547
+ message: `${action} is unavailable: frontend split runtime is not wired`,
548
+ }),
549
+ };
550
+ }
551
+ function numOf(v) {
552
+ return typeof v === "number" ? v : Number(v);
553
+ }
554
+ function splitCommandBase(p) {
555
+ return {
556
+ operationId: str(p.operationId) ?? "",
557
+ expectedRevision: numOf(p.expectedRevision),
558
+ expectedParentDigest: str(p.expectedParentDigest) ?? "",
559
+ expectedManifestDigest: str(p.expectedManifestDigest) ?? "",
560
+ };
561
+ }
562
+ function splitCommandDispatch(action, result) {
563
+ if (result.outcome === "conflict") {
564
+ return {
565
+ kind: "error",
566
+ status: 409,
567
+ body: operatorFailed({
568
+ command: action,
569
+ outcome: "conflict",
570
+ code: "CONFLICT",
571
+ message: "frontend split CAS conflict (stale revision or binding)",
572
+ }),
573
+ };
574
+ }
575
+ return { kind: "sync", status: 200, body: operatorSucceeded(action, result) };
576
+ }
577
+ async function runSplitAction(action, run) {
578
+ try {
579
+ const result = await run();
580
+ if (result && typeof result === "object" && "outcome" in result) {
581
+ return splitCommandDispatch(action, result);
582
+ }
583
+ return {
584
+ kind: "sync",
585
+ status: 200,
586
+ body: operatorSucceeded(action, result),
587
+ };
588
+ }
589
+ catch (error) {
590
+ return invalid(action, error instanceof Error ? error.message : String(error));
591
+ }
592
+ }
593
+ function humanDecisionDispatch(action, result) {
594
+ if (result.outcome === "rejected") {
595
+ return {
596
+ kind: "error",
597
+ status: 409,
598
+ body: operatorFailed({
599
+ command: action,
600
+ outcome: "conflict",
601
+ code: "CONFLICT",
602
+ message: result.reason,
603
+ details: { projection: result.projection },
604
+ }),
605
+ };
606
+ }
607
+ return { kind: "sync", status: 200, body: operatorSucceeded(action, result) };
608
+ }
609
+ async function runHumanDecisionAction(action, run) {
610
+ try {
611
+ return humanDecisionDispatch(action, await run());
612
+ }
613
+ catch (error) {
614
+ return invalid(action, error instanceof Error ? error.message : String(error));
615
+ }
616
+ }
459
617
  export async function dispatchOperatorAction(ctx, req) {
460
618
  const action = req.action?.trim();
461
619
  if (!action) {
@@ -2078,12 +2236,35 @@ export async function dispatchOperatorAction(ctx, req) {
2078
2236
  }
2079
2237
  return accepted;
2080
2238
  }
2239
+ case "dagPromptOverrideConfig": {
2240
+ // Experimental internal switch; the browser UI hides the prompt-edit
2241
+ // entry unless this reports enabled=true.
2242
+ return {
2243
+ kind: "sync",
2244
+ status: 200,
2245
+ body: operatorSucceeded(action, {
2246
+ enabled: isDagRerunPromptOverrideEnabled(),
2247
+ envVar: DAG_RERUN_PROMPT_OVERRIDE_ENV,
2248
+ }),
2249
+ };
2250
+ }
2081
2251
  case "dagRerunPlan": {
2082
2252
  const runId = str(p.runId);
2083
2253
  const fromNode = str(p.fromNode);
2084
2254
  if (!runId || !fromNode) {
2085
2255
  return invalid(action, "runId and fromNode are required");
2086
2256
  }
2257
+ const parsedOverrides = parsePromptOverrideParams(p.promptOverrides);
2258
+ if (parsedOverrides.kind === "error") {
2259
+ return invalid(action, parsedOverrides.message);
2260
+ }
2261
+ if (parsedOverrides.overrides.length > 0 &&
2262
+ !isDagRerunPromptOverrideEnabled()) {
2263
+ return invalid(action, `prompt override rerun is disabled; set ${DAG_RERUN_PROMPT_OVERRIDE_ENV}=1 to enable this experimental capability`);
2264
+ }
2265
+ const overrideArgs = parsedOverrides.overrides.length > 0
2266
+ ? await buildPromptOverrideCliArgs(ctx, parsedOverrides.overrides)
2267
+ : [];
2087
2268
  const body = await runReadCli(ctx, [
2088
2269
  "dag",
2089
2270
  "rerun",
@@ -2092,6 +2273,7 @@ export async function dispatchOperatorAction(ctx, req) {
2092
2273
  "--from-node",
2093
2274
  fromNode,
2094
2275
  "--plan",
2276
+ ...overrideArgs,
2095
2277
  "--json",
2096
2278
  ], "dag rerun plan");
2097
2279
  return { kind: "sync", status: body.ok ? 200 : 400, body };
@@ -2105,6 +2287,17 @@ export async function dispatchOperatorAction(ctx, req) {
2105
2287
  if (!runId || !fromNode || !planHash || !reason || !clientRequestId) {
2106
2288
  return invalid(action, "runId, fromNode, planHash, reason, and clientRequestId are required");
2107
2289
  }
2290
+ const parsedOverrides = parsePromptOverrideParams(p.promptOverrides);
2291
+ if (parsedOverrides.kind === "error") {
2292
+ return invalid(action, parsedOverrides.message);
2293
+ }
2294
+ if (parsedOverrides.overrides.length > 0 &&
2295
+ !isDagRerunPromptOverrideEnabled()) {
2296
+ return invalid(action, `prompt override rerun is disabled; set ${DAG_RERUN_PROMPT_OVERRIDE_ENV}=1 to enable this experimental capability`);
2297
+ }
2298
+ const overrideArgs = parsedOverrides.overrides.length > 0
2299
+ ? await buildPromptOverrideCliArgs(ctx, parsedOverrides.overrides)
2300
+ : [];
2108
2301
  return acceptOperation(ctx, {
2109
2302
  action,
2110
2303
  actionParams: p,
@@ -2122,6 +2315,7 @@ export async function dispatchOperatorAction(ctx, req) {
2122
2315
  clientRequestId,
2123
2316
  "--reason",
2124
2317
  reason,
2318
+ ...overrideArgs,
2125
2319
  "--json",
2126
2320
  ],
2127
2321
  });
@@ -2606,6 +2800,75 @@ export async function dispatchOperatorAction(ctx, req) {
2606
2800
  }),
2607
2801
  };
2608
2802
  }
2803
+ case "frontendSplitWakeup": {
2804
+ if (!ctx.splitOrchestrator)
2805
+ return splitRuntimeMissing(action);
2806
+ return runSplitAction(action, () => ctx.splitOrchestrator.wakeupChildren(splitCommandBase(p)));
2807
+ }
2808
+ case "frontendSplitChildCallback": {
2809
+ if (!ctx.splitOrchestrator)
2810
+ return splitRuntimeMissing(action);
2811
+ return runSplitAction(action, () => ctx.splitOrchestrator.childCallback({
2812
+ ...splitCommandBase(p),
2813
+ observation: p.observation,
2814
+ }));
2815
+ }
2816
+ case "frontendSplitWakeupIntegration": {
2817
+ if (!ctx.splitOrchestrator)
2818
+ return splitRuntimeMissing(action);
2819
+ return runSplitAction(action, () => ctx.splitOrchestrator.wakeupIntegration(splitCommandBase(p)));
2820
+ }
2821
+ case "frontendSplitIntegrationCallback": {
2822
+ if (!ctx.splitOrchestrator)
2823
+ return splitRuntimeMissing(action);
2824
+ return runSplitAction(action, () => ctx.splitOrchestrator.integrationCallback({
2825
+ ...splitCommandBase(p),
2826
+ completion: p.completion,
2827
+ }));
2828
+ }
2829
+ case "frontendSplitFinalize": {
2830
+ if (!ctx.splitOrchestrator)
2831
+ return splitRuntimeMissing(action);
2832
+ return runSplitAction(action, () => ctx.splitOrchestrator.finalize(splitCommandBase(p)));
2833
+ }
2834
+ case "frontendHumanDecisionMaterialize": {
2835
+ if (!ctx.humanDecisionAdapter)
2836
+ return splitRuntimeMissing(action);
2837
+ return runHumanDecisionAction(action, () => ctx.humanDecisionAdapter.materialize({
2838
+ operationId: str(p.operationId) ?? "",
2839
+ expectedRecordRevision: numOf(p.expectedRecordRevision),
2840
+ expectedProjectionDigest: str(p.expectedProjectionDigest) ?? "",
2841
+ envelope: p.envelope,
2842
+ }));
2843
+ }
2844
+ case "frontendHumanDecisionApprove": {
2845
+ if (!ctx.humanDecisionAdapter)
2846
+ return splitRuntimeMissing(action);
2847
+ return runHumanDecisionAction(action, () => ctx.humanDecisionAdapter.consume({
2848
+ operationId: str(p.operationId) ?? "",
2849
+ expectedRecordRevision: numOf(p.expectedRecordRevision),
2850
+ expectedProjectionDigest: str(p.expectedProjectionDigest) ?? "",
2851
+ receipt: p.receipt,
2852
+ currentTargetDigest: str(p.currentTargetDigest) ?? "",
2853
+ now: str(p.now) ?? new Date().toISOString(),
2854
+ }));
2855
+ }
2856
+ case "frontendSplitInitialize": {
2857
+ if (!ctx.humanDecisionAdapter)
2858
+ return splitRuntimeMissing(action);
2859
+ return runHumanDecisionAction(action, () => ctx.humanDecisionAdapter.initializeSplit({
2860
+ operationId: str(p.operationId) ?? "",
2861
+ expectedRecordRevision: numOf(p.expectedRecordRevision),
2862
+ expectedProjectionDigest: str(p.expectedProjectionDigest) ?? "",
2863
+ execution: p.execution,
2864
+ manifest: p.manifest,
2865
+ receipt: p.receipt,
2866
+ currentTargetDigest: str(p.currentTargetDigest) ?? "",
2867
+ now: str(p.now) ?? new Date().toISOString(),
2868
+ activeWriterOperationId: str(p.activeWriterOperationId) ?? null,
2869
+ activeProviderOperationId: str(p.activeProviderOperationId) ?? null,
2870
+ }));
2871
+ }
2609
2872
  default: {
2610
2873
  // M5 official-command coverage uses one generic tail for newly registered
2611
2874
  // CLI actions. It remains inside dispatchOperatorAction (the sole dispatch
@@ -3,7 +3,7 @@ import { rewriteRecoveryOperatorError } from "./recovery-error-copy.js";
3
3
  const MUTATION_GATE_COPY = [
4
4
  {
5
5
  pattern: /invalid (boot capability|confirmation header) token/i,
6
- copy: "页面已过期(控制台已重启),请刷新页面(F5)后重试。",
6
+ copy: "页面凭证已失效(控制台可能已重启),请刷新页面(F5)后重试。",
7
7
  },
8
8
  {
9
9
  pattern: /mutation requires boot capability cookie|mutation requires confirmation header token/i,
@@ -23,7 +23,7 @@ export function formatOperatorUserError(message) {
23
23
  if (recovery)
24
24
  return recovery;
25
25
  if (/controller identity changed|package fingerprint changed|entry binary sha256 changed/i.test(raw)) {
26
- return "控制器版本已变更(Console 启动后代码被重新编译)。请重启 Console 后再试,一次会话中途不要升级/重建控制器。";
26
+ return "控制器版本已变更(Console 启动后代码被重新编译,或提交了 package.json / bin / dist / skills 等指纹范围文件)。请重启 Console 进程使其重新加载当前构建后再重试本操作;一次会话中途不要重建控制器。";
27
27
  }
28
28
  if (/Pi session file missing or not openable|cannot reopen chat session .* no persisted Pi session file/i.test(raw)) {
29
29
  return "该对话的 Pi 会话文件已丢失,无法恢复上下文。请新建对话继续(历史记录无法重开)。";