@tea-agent/loop-agent 0.39.0-beta.7 → 0.39.0-beta.9

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 (164) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +23 -5
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/command-definitions.js +7 -0
  7. package/dist/cli/program.js +5 -0
  8. package/dist/cli/update/runtime-activity.js +1 -29
  9. package/dist/commands/dag-follow-up.js +138 -0
  10. package/dist/commands/dag-rerun.js +1 -1
  11. package/dist/commands/init.js +26 -0
  12. package/dist/executors/dag-pi-executor.js +326 -8
  13. package/dist/executors/pi-read-budget-policy.js +166 -0
  14. package/dist/executors/shell-executor.js +26 -30
  15. package/dist/shared/dag-prompt-override.js +4 -5
  16. package/dist/shared/update/npm-client.js +33 -11
  17. package/dist/task/config-types.js +15 -0
  18. package/dist/task/contract/apply.js +11 -1
  19. package/dist/task/contract/constants.js +2 -0
  20. package/dist/task/contract/hash.js +3 -0
  21. package/dist/task/contract/observe.js +25 -4
  22. package/dist/task/contract/paths.js +2 -1
  23. package/dist/task/contract/project.js +15 -1
  24. package/dist/task/contract/schema.js +2 -0
  25. package/dist/task/contract/transaction.js +12 -0
  26. package/dist/task/frontend-project-capability.js +123 -4
  27. package/dist/task/source-prepare/completeness.js +188 -0
  28. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  29. package/dist/task/source-prepare/index.js +3 -0
  30. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  31. package/dist/task/source-prepare/ledger-review.js +214 -0
  32. package/dist/task/source-prepare/ledger.js +545 -0
  33. package/dist/task/source-prepare/prepare.js +262 -1
  34. package/dist/task/source-prepare/semantic-intake.js +25 -5
  35. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  36. package/dist/task/source-prepare/types.js +22 -0
  37. package/dist/worker/cli.js +12 -26
  38. package/dist/worker/console/console-handoff.js +82 -0
  39. package/dist/worker/console/console-update-and-init.js +115 -0
  40. package/dist/worker/console/console-update-runtime.js +84 -0
  41. package/dist/worker/console/draft-store.js +49 -0
  42. package/dist/worker/console/operation-store.js +1 -1
  43. package/dist/worker/console/operator-actions.js +129 -4
  44. package/dist/worker/console/operator-surface-health.js +1 -0
  45. package/dist/worker/console/prd-intake-bridge.js +13 -0
  46. package/dist/worker/console/routes.js +3 -0
  47. package/dist/worker/console/server.js +73 -5
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-bPer1waP.js → abnfDiagram-N423BO3Z-D1-Z-BBa.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-BLdkwucW.js → arc-q8qAYm5m.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw4wo4I1.js → architectureDiagram-T3A2C74G-CAu4_lve.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DUUkc0gr.js → blockDiagram-VBNYF7ZC-D5GhQvHm.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BC3O0nob.js → c4Diagram-5PPSVZJV-ClQjeLkO.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-ClAuhSUL.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Dd4g2nti.js → chunk-2GRJ4B5K-DLqWlFbo.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-s2EDm7-n.js → chunk-2Q5K7J3B-DkRT_usL.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-GqtHM2yQ.js → chunk-5RXB4S5H-Cqp7Chi3.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BzRFrN8j.js → chunk-5VM5RSS4-CedEdFB9.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bw9nFjCJ.js → chunk-6Q2QTUOP-BHM8vP5x.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B4n-03Qm.js → chunk-GF5L2VYU-D7EFr377.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Dt75ZdBD.js → chunk-JWPE2WC7-DR38mTWV.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B_afMhHB.js → chunk-KBJHAD2P-BFv2LsR3.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Bd8Rqqg9.js → chunk-RYQCIY6F-BAvpJS5z.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-9boOMji4.js → chunk-XXDRQBXY-eKI9TO72.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-cBgeCpVU.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-cBgeCpVU.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BtCycfCc.js → cose-bilkent-JH36ORCC-BMJXpgo4.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-eTOts9L1.js → cynefin-VYW2F7L2-CKJ2ofZx.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cibd2mjg.js → cynefinDiagram-MW4NZA55-CsKnsazm.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CDjDZtDV.js → dagre-VZM6K2ZE-DVQ46vIw.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Btexn-JJ.js → diagram-7IWD3JNH-D_RuonYS.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cyl8sdpM.js → diagram-B4RE2ZJO-D2Mt0nMu.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Ev-83E1t.js → diagram-LBJQPF4R-DjBDuye6.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-41LqE0Mc.js → diagram-Q27KOJAE-8VZkqpE_.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BC39T5R3.js → diagram-UB23O5K3-D0izYxia.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DzcNRuri.js → ebnfDiagram-BXEA7PRR-CncG2I95.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B5Frvvnz.js → erDiagram-JOGREHBK-DiBUUXR8.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BapxROIM.js → flowDiagram-UKHOOZJN-Dtc0fArb.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cd4d4wQ0.js → ganttDiagram-PKOTCBZU-BPNFP5OZ.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CrL6buFW.js → gitGraphDiagram-DS77QQ5N-CsgpxEEd.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DBh_x_Ek.js → index-Bai8Z25q.js} +81 -81
  81. package/dist/worker/console/static/assets/index-CH0Tr5hY.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BrSiNxyd.js → infoDiagram-6WML65LV-kHGxkJUS.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-gvlvY-n9.js → ishikawaDiagram-WSZJBQD7-CbLe9wrr.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-YaPzRQ.js → journeyDiagram-NVQOT4AX-BOkwxAcP.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BsmDv9E_.js → kanban-definition-27J2QSJJ-BHYQ2rDu.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-UWaoDN3h.js → linear-CHT4ujob.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BlvpGVG8.js → mermaid.core-yL8sfqnY.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CUCIP5CP.js → mindmap-definition-FAOFIHXS-DZx1coV-.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CB1aQUAK.js → pegDiagram-VL7TDLO6-DStWRCI8.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BwehHxhf.js → pieDiagram-7S7Q4E2Y-DSKZkwzK.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CFz30APB.js → quadrantDiagram-CIZ2JOQS-CmoEGOuW.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwNmw1i6.js → railroadDiagram-AXF67PYL-BLqzhKyq.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-jg-tPXlI.js → requirementDiagram-LRYGKXZP-gbHeUabM.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BlyReDKl.js → sankeyDiagram-W5VNT64P-CaKkBCKO.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DBUzW1f3.js → sequenceDiagram-SI44F4Z6-PUbi3Upu.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BYcI2RAr.js → sizeCapture-X5ZJPWSS-dm7S9Qfe.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D7AiyJdo.js → stateDiagram-OKZ733FA-C7WSzv4p.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-sQDv5AP3.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BHGt7Kkv.js → swimlanes-SLNWSIFB-CBKCiQwd.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BXwsmcPs.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-OPjooWcX.js → timeline-definition-Z64GVDOM-CvnUlylk.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-ggsWDvrO.js → vennDiagram-T6HMQDX7-xM0StY1O.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DjNe-BjZ.js → wardleyDiagram-T6FBY63Y-CWtJMagy.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Byp63Hei.js → xychartDiagram-ELKLHX3M-BO_-MeK8.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/app/console-types.js +40 -1
  107. package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
  108. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  109. package/dist/worker/observability/init-runtime-activity.js +26 -0
  110. package/dist/worker/observe/node-input.js +88 -2
  111. package/dist/worker/observe/routes.js +188 -0
  112. package/dist/worker/observe/static/api.js +3 -3
  113. package/dist/worker/observe/static/format.js +37 -0
  114. package/dist/worker/observe/static/index.html +5 -0
  115. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  116. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  117. package/dist/worker/observe/static/state.js +2 -2
  118. package/dist/worker/observe/static/styles.css +331 -35
  119. package/dist/worker/observe/static/views/dag-inspector.js +509 -78
  120. package/dist/worker/observe/static/views/task.js +55 -0
  121. package/dist/workflows/dag/frontend-design-policy.js +19 -9
  122. package/dist/workflows/dag/frontend-implementation-contract.js +117 -50
  123. package/dist/workflows/dag/frontend-plan-render.js +27 -0
  124. package/dist/workflows/dag/frontend-prewrite-gate.js +45 -1
  125. package/dist/workflows/dag/frontend-recovery-controller.js +1 -0
  126. package/dist/workflows/dag/frontend-review-context.js +102 -15
  127. package/dist/workflows/dag/frontend-review-findings.js +42 -10
  128. package/dist/workflows/dag/frontend-shadow-dual-write.js +36 -2
  129. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  130. package/dist/workflows/dag/frontend-worktree-diff.js +154 -0
  131. package/dist/workflows/dag/frontend-writer-admission.js +12 -0
  132. package/dist/workflows/dag/init-hybrid.js +297 -65
  133. package/dist/workflows/dag/interrupt-request.js +7 -0
  134. package/dist/workflows/dag/node-execution.js +111 -1
  135. package/dist/workflows/dag/prompt.js +34 -1
  136. package/dist/workflows/dag/recovery-lease.js +80 -0
  137. package/dist/workflows/dag/rerun-feedback.js +31 -8
  138. package/dist/workflows/dag/rerun-run.js +23 -0
  139. package/dist/workflows/dag/rerun-task.js +29 -0
  140. package/dist/workflows/dag/retry-policy.js +34 -9
  141. package/dist/workflows/dag/runner.js +83 -16
  142. package/dist/workflows/dag/types.js +67 -11
  143. package/docs/README.md +1 -0
  144. package/docs/architecture/evolution.md +45 -0
  145. package/docs/init-surface.manifest.json +4 -0
  146. package/docs/templates/README.md +2 -1
  147. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  148. package/docs/templates/spec-registry.schema.json +45 -0
  149. package/harness.json +3 -3
  150. package/package.json +1 -1
  151. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  152. package/skills/frontend-design-review/SKILL.md +20 -41
  153. package/skills/frontend-implementation/SKILL.md +2 -3
  154. package/skills/frontend-implementation/references/design-spec.md +4 -3
  155. package/skills/frontend-implementation/references/node-contracts.md +4 -3
  156. package/skills/frontend-review/SKILL.md +22 -14
  157. package/skills/frontend-review/references/review-findings.md +6 -7
  158. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  159. package/dist/worker/console/static/assets/channel-AbFtVnFm.js +0 -1
  160. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +0 -1
  161. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +0 -1
  162. package/dist/worker/console/static/assets/index-DwOGauxU.css +0 -1
  163. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +0 -1
  164. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +0 -8
@@ -0,0 +1,384 @@
1
+ /**
2
+ * 生产 Source Fidelity Pi 适配器(AC-PROD-001..006 / AC-001..006)
3
+ *
4
+ * 为真实 `task advance` 的 prepare-contract 路径提供两个独立的 Pi 调用面:
5
+ * - reconcile:仅叙述性/歧义 ledger fragment 的顺序 reconciliation executor
6
+ * (结构化片段由 ledger-reconciliation.ts 的 isStructuredMappableFragment
7
+ * 走代码映射,零 Pi 调用);
8
+ * - review:context-only / duplicate / conflict 的独立 reviewer(与 reconciler
9
+ * 是不同调用阶段,最多一次有界修订,LEDGER_REVIEW_MAX_REVISIONS=1)。
10
+ *
11
+ * 模型队列镜像 semantic-intake.ts(MED→HIGH→LOW harness 档位 + Pi SDK 目录),
12
+ * 单次尝试沿用有界超时(SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS),工具面仅只读
13
+ * read/grep/find/ls(无写工具)。所有模型输出经 validateOutput + zod 严格校验:
14
+ * 无效 JSON/schema、越界 canonical ID、排除类 disposition 缺理由一律抛
15
+ * LedgerReconciliationError / LedgerReviewError,由 prepare.ts 既有 catch 转为
16
+ * 稳定 SOURCE_FIDELITY_* 阻断码,绝不回退确定性 heuristics。队列空 / 执行失败 /
17
+ * 超时同样 fail closed。
18
+ */
19
+ import { z } from "zod";
20
+ import { executePiStep } from "../../executors/pi-executor.js";
21
+ import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
22
+ import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
23
+ import { listPiSdkAvailableModels } from "../../executors/pi-sdk-executor.js";
24
+ import { loadHarnessManifest } from "../../governance/harness.js";
25
+ import { loadTaskConfig } from "../runtime.js";
26
+ import { buildSemanticIntakeModelQueue, extractJsonObject, } from "./semantic-intake.js";
27
+ import { LEDGER_RECONCILIATION_DEFAULT_BUDGET_CHARS, LedgerReconciliationError, } from "./ledger-reconciliation.js";
28
+ import { LedgerReviewError, } from "./ledger-review.js";
29
+ /** 只读工具面(AC-003):source fidelity Pi 调用无任何写工具。 */
30
+ export const SOURCE_FIDELITY_PI_TOOL_NAMES = [
31
+ "read",
32
+ "grep",
33
+ "find",
34
+ "ls",
35
+ ];
36
+ /** 单次 reconciliation/review Pi 尝试的墙钟上限(镜像 SEMANTIC_INTAKE_ATTEMPT_TIMEOUT_MS)。 */
37
+ export const SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS = 300_000;
38
+ /** 接线侧门 taskKind(AC-PROD-005)。 */
39
+ export const SOURCE_FIDELITY_TASK_KIND = "frontend-implementation";
40
+ const FRAGMENT_DISPOSITIONS = [
41
+ "requirement",
42
+ "acceptance",
43
+ "constraint",
44
+ "non-goal",
45
+ "duplicate",
46
+ "context-only",
47
+ "unresolved",
48
+ ];
49
+ const EXCLUSION_DISPOSITIONS = new Set([
50
+ "context-only",
51
+ "duplicate",
52
+ "unresolved",
53
+ ]);
54
+ const reconciliationResultSchema = z.object({
55
+ fragmentId: z.string().min(1),
56
+ mappedIds: z.array(z.string().min(1)).default([]),
57
+ disposition: z.enum(FRAGMENT_DISPOSITIONS),
58
+ reason: z.string().min(1).optional(),
59
+ });
60
+ const reviewResponseSchema = z.object({
61
+ fragmentId: z.string().min(1),
62
+ approved: z.boolean(),
63
+ reason: z.string().min(1).optional(),
64
+ revision: z
65
+ .object({
66
+ disposition: z.enum(FRAGMENT_DISPOSITIONS).optional(),
67
+ reason: z.string().min(1).optional(),
68
+ })
69
+ .optional(),
70
+ });
71
+ /**
72
+ * 模型队列构建镜像 semantic-intake.ts:MED→HIGH→LOW(harness executors.pi)+
73
+ * Pi SDK 目录(已认证优先、非 chat 模型过滤、额外模型上限沿用
74
+ * SEMANTIC_INTAKE_MAX_EXTRA_MODELS)。队列空即 fail closed。
75
+ */
76
+ export function buildSourceFidelityModelQueue(input) {
77
+ return buildSemanticIntakeModelQueue(input);
78
+ }
79
+ async function buildProductionModelQueue(repoRoot, listAvailableModels) {
80
+ const manifest = await loadHarnessManifest(repoRoot);
81
+ const models = resolveExecutorModelMatrices(manifest);
82
+ let available = [];
83
+ try {
84
+ available = listAvailableModels
85
+ ? await listAvailableModels()
86
+ : await listPiSdkAvailableModels();
87
+ }
88
+ catch {
89
+ available = [];
90
+ }
91
+ return buildSourceFidelityModelQueue({ harness: models.pi, available });
92
+ }
93
+ function buildReconciliationPrompt(request) {
94
+ return [
95
+ "You are a read-only source-fidelity-reconcile agent for loop-agent task intake.",
96
+ "Map the source fragment to EXISTING canonical requirement ids only.",
97
+ "Return exactly one JSON object (optional json fence); no prose outside it.",
98
+ "Rules:",
99
+ "- mappedIds: canonical requirement ids from the provided list that this fragment directly supports; empty array when none.",
100
+ "- fragmentId: MUST equal the requested fragment id exactly (echo the `fragment id:` line verbatim); identity mismatch is rejected.",
101
+ "- disposition: exactly one of requirement|acceptance|constraint|non-goal|duplicate|context-only|unresolved.",
102
+ "- reason: REQUIRED (non-empty) when disposition is context-only, duplicate or unresolved; optional otherwise.",
103
+ "- Never invent canonical ids; only use ids present in the provided canonical requirements.",
104
+ "- Never invent requirements or human resolutions.",
105
+ JSON.stringify({
106
+ fragmentId: "REQ-SRC-AB12CD34EF56",
107
+ mappedIds: ["AC-001"],
108
+ disposition: "requirement",
109
+ reason: "optional; required for exclusion dispositions",
110
+ }, null, 2),
111
+ `fragment id: ${request.fragment.id}`,
112
+ `fragment path: ${request.fragment.path}`,
113
+ `fragment headingPath: ${request.fragment.headingPath}`,
114
+ `fragment lineRange: ${JSON.stringify(request.fragment.lineRange)}`,
115
+ `budgetChars: ${request.budgetChars}`,
116
+ "fragment text:",
117
+ "```markdown",
118
+ request.fragment.text,
119
+ "```",
120
+ "canonical requirements:",
121
+ JSON.stringify(request.canonicalRequirements, null, 2),
122
+ ].join("\n\n");
123
+ }
124
+ function buildReviewPrompt(request) {
125
+ return [
126
+ "You are an independent read-only source-fidelity-review agent for loop-agent task intake.",
127
+ "Review whether the given fragment's context-only / duplicate / conflict disposition is justified.",
128
+ "You are a separate stage from reconciliation: do not re-map fragments; only approve, reject, or propose at most one bounded revision.",
129
+ "Return exactly one JSON object (optional json fence); no prose outside it.",
130
+ "Rules:",
131
+ "- approved: true only when the exclusion/conflict handling is justified.",
132
+ "- fragmentId: MUST equal the requested fragment id exactly (echo the `fragment id:` line verbatim); identity mismatch is rejected.",
133
+ "- reason: recommended; explain the verdict.",
134
+ "- revision: optional single bounded revision {disposition, reason}; never invent requirements or human resolutions.",
135
+ JSON.stringify({
136
+ fragmentId: "REQ-SRC-AB12CD34EF56",
137
+ approved: true,
138
+ reason: "verdict rationale",
139
+ revision: {
140
+ disposition: "duplicate",
141
+ reason: "optional bounded revision",
142
+ },
143
+ }, null, 2),
144
+ `review kind: ${request.kind}`,
145
+ `fragment id: ${request.fragment.id}`,
146
+ `fragment disposition: ${request.fragment.disposition}`,
147
+ ...(request.fragment.reason
148
+ ? [`fragment reason: ${request.fragment.reason}`]
149
+ : []),
150
+ `fragment risk: ${request.fragment.risk}`,
151
+ `fragment canonicalRequirementIds: ${JSON.stringify(request.fragment.canonicalRequirementIds)}`,
152
+ ...(request.fragment.text.trim()
153
+ ? [
154
+ "fragment text:",
155
+ "```markdown",
156
+ request.fragment.text,
157
+ "```",
158
+ ]
159
+ : [
160
+ "fragment text: (bounded original text unavailable for this record)",
161
+ `fragment id: ${request.fragment.id}`,
162
+ ]),
163
+ "canonical requirements:",
164
+ JSON.stringify(request.canonicalRequirements, null, 2),
165
+ ...(request.resolutionAvailable
166
+ ? ["resolutionAvailable: true (conflict has a valid human resolution)"]
167
+ : []),
168
+ ].join("\n\n");
169
+ }
170
+ function validateReconciliationOutput(text, request) {
171
+ let raw;
172
+ try {
173
+ raw = extractJsonObject(text);
174
+ }
175
+ catch (error) {
176
+ throw new LedgerReconciliationError(`reconciliation output is not a JSON object: ${error instanceof Error ? error.message : String(error)}`);
177
+ }
178
+ let parsed;
179
+ try {
180
+ parsed = reconciliationResultSchema.parse(raw);
181
+ }
182
+ catch (error) {
183
+ throw new LedgerReconciliationError(`reconciliation output failed schema validation: ${error instanceof Error ? error.message : String(error)}`);
184
+ }
185
+ // 生产 fragmentId 绑定(AC-005 / AC-HARD-005):缺失/错配即 fail closed,
186
+ // 保证 reviewer/reconciler 决策绑定到实际请求的 source fragment。
187
+ if (parsed.fragmentId !== request.fragmentId) {
188
+ throw new LedgerReconciliationError(`reconciliation output fragmentId ${parsed.fragmentId} does not match requested fragment ${request.fragmentId}`);
189
+ }
190
+ const outOfBounds = parsed.mappedIds.filter((id) => !request.canonicalIds.has(id));
191
+ if (outOfBounds.length > 0) {
192
+ throw new LedgerReconciliationError(`reconciliation output contains out-of-bounds canonical ids: ${outOfBounds.join(", ")}`);
193
+ }
194
+ if (EXCLUSION_DISPOSITIONS.has(parsed.disposition) && !parsed.reason?.trim()) {
195
+ throw new LedgerReconciliationError(`reconciliation output omitted the required exclusion reason for disposition ${parsed.disposition}`);
196
+ }
197
+ return {
198
+ mappedIds: parsed.mappedIds,
199
+ disposition: parsed.disposition,
200
+ ...(parsed.reason ? { reason: parsed.reason } : {}),
201
+ };
202
+ }
203
+ function validateReviewOutput(text, request) {
204
+ let raw;
205
+ try {
206
+ raw = extractJsonObject(text);
207
+ }
208
+ catch (error) {
209
+ throw new LedgerReviewError(`review output is not a JSON object: ${error instanceof Error ? error.message : String(error)}`);
210
+ }
211
+ let parsed;
212
+ try {
213
+ parsed = reviewResponseSchema.parse(raw);
214
+ }
215
+ catch (error) {
216
+ throw new LedgerReviewError(`review output failed schema validation: ${error instanceof Error ? error.message : String(error)}`);
217
+ }
218
+ // 生产 fragmentId 绑定(AC-005 / AC-HARD-005):缺失/错配即 fail closed。
219
+ if (parsed.fragmentId !== request.fragmentId) {
220
+ throw new LedgerReviewError(`review output fragmentId ${parsed.fragmentId} does not match requested fragment ${request.fragmentId}`);
221
+ }
222
+ return {
223
+ approved: parsed.approved,
224
+ ...(parsed.reason ? { reason: parsed.reason } : {}),
225
+ ...(parsed.revision ? { revision: parsed.revision } : {}),
226
+ };
227
+ }
228
+ /**
229
+ * 执行一次生产 reconciliation:模型队列串行尝试(默认真实 executePiStep),
230
+ * 每个成功输出都经 validateOutput + zod 严格校验,全部失败抛
231
+ * LedgerReconciliationError(稳定 SOURCE_FIDELITY_RECONCILIATION_FAILED)。
232
+ */
233
+ export async function runProductionLedgerReconciliation(request, options) {
234
+ const canonicalIds = new Set(request.canonicalRequirements.map((requirement) => requirement.id));
235
+ const parseAndValidate = (text) => validateReconciliationOutput(text, {
236
+ fragmentId: request.fragment.id,
237
+ canonicalIds,
238
+ });
239
+ const queue = options.modelQueue ??
240
+ (await buildProductionModelQueue(options.repoRoot, options.listAvailableModels));
241
+ if (queue.length === 0) {
242
+ throw new LedgerReconciliationError("source fidelity reconciliation has no MED/HIGH/LOW or Pi-supported models to try (fail closed)");
243
+ }
244
+ const prompt = buildReconciliationPrompt(request);
245
+ const execute = options.executePi ?? executePiStep;
246
+ const timeoutMs = options.timeoutMs ?? SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS;
247
+ let lastFailure = "";
248
+ for (const modelRef of queue) {
249
+ try {
250
+ const result = await execute({
251
+ attachedFiles: [],
252
+ modelConfig: resolveDagPiModelConfig(modelRef),
253
+ prompt,
254
+ repoRoot: options.repoRoot,
255
+ step: "analyze",
256
+ toolNames: [...SOURCE_FIDELITY_PI_TOOL_NAMES],
257
+ timeoutMs,
258
+ stallTimeoutMs: timeoutMs,
259
+ userMessage: "Reconcile the source fragment into the required JSON only.",
260
+ validateOutput: (text) => {
261
+ try {
262
+ parseAndValidate(text);
263
+ return [];
264
+ }
265
+ catch (error) {
266
+ return [error instanceof Error ? error.message : String(error)];
267
+ }
268
+ },
269
+ });
270
+ if (!result.ok) {
271
+ lastFailure =
272
+ `${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
273
+ continue;
274
+ }
275
+ try {
276
+ return parseAndValidate(result.assistantText);
277
+ }
278
+ catch (error) {
279
+ // 模型返回成功但输出未通过严格校验:视为 invalid-output,尝试下一模型。
280
+ lastFailure =
281
+ `invalid-output: ${error instanceof Error ? error.message : String(error)}`;
282
+ }
283
+ }
284
+ catch (error) {
285
+ lastFailure =
286
+ `unknown: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000);
287
+ }
288
+ }
289
+ throw new LedgerReconciliationError(`source fidelity reconciliation pi failed after ${queue.length} model(s): ${lastFailure || "no attempt produced valid output"}`);
290
+ }
291
+ /**
292
+ * 执行一次生产独立 review:模型队列串行尝试(默认真实 executePiStep),
293
+ * 输出经 validateOutput + zod 严格校验,全部失败抛 LedgerReviewError
294
+ * (稳定 SOURCE_FIDELITY_RECONCILIATION_FAILED,绝不回退默认 reviewer)。
295
+ */
296
+ export async function runProductionLedgerReview(request, options) {
297
+ const parseAndValidate = (text) => validateReviewOutput(text, { fragmentId: request.fragment.id });
298
+ const queue = options.modelQueue ??
299
+ (await buildProductionModelQueue(options.repoRoot, options.listAvailableModels));
300
+ if (queue.length === 0) {
301
+ throw new LedgerReviewError("source fidelity review has no MED/HIGH/LOW or Pi-supported models to try (fail closed)");
302
+ }
303
+ const prompt = buildReviewPrompt(request);
304
+ const execute = options.executePi ?? executePiStep;
305
+ const timeoutMs = options.timeoutMs ?? SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS;
306
+ let lastFailure = "";
307
+ for (const modelRef of queue) {
308
+ try {
309
+ const result = await execute({
310
+ attachedFiles: [],
311
+ modelConfig: resolveDagPiModelConfig(modelRef),
312
+ prompt,
313
+ repoRoot: options.repoRoot,
314
+ step: "analyze",
315
+ toolNames: [...SOURCE_FIDELITY_PI_TOOL_NAMES],
316
+ timeoutMs,
317
+ stallTimeoutMs: timeoutMs,
318
+ userMessage: "Review the fragment disposition into the required JSON only.",
319
+ validateOutput: (text) => {
320
+ try {
321
+ parseAndValidate(text);
322
+ return [];
323
+ }
324
+ catch (error) {
325
+ return [error instanceof Error ? error.message : String(error)];
326
+ }
327
+ },
328
+ });
329
+ if (!result.ok) {
330
+ lastFailure =
331
+ `${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
332
+ continue;
333
+ }
334
+ try {
335
+ return parseAndValidate(result.assistantText);
336
+ }
337
+ catch (error) {
338
+ lastFailure =
339
+ `invalid-output: ${error instanceof Error ? error.message : String(error)}`;
340
+ }
341
+ }
342
+ catch (error) {
343
+ lastFailure =
344
+ `unknown: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000);
345
+ }
346
+ }
347
+ throw new LedgerReviewError(`source fidelity review pi failed after ${queue.length} model(s): ${lastFailure || "no attempt produced valid output"}`);
348
+ }
349
+ /**
350
+ * 生产适配器工厂:返回 prepare 可注入的 reconcile executor(budgetChars=4000)
351
+ * 与独立 reviewer 两个不同 Pi 调用面。
352
+ */
353
+ export function createProductionSourceFidelityPi(options) {
354
+ const reconcileExecutor = (request) => runProductionLedgerReconciliation(request, options);
355
+ const reviewer = (request) => runProductionLedgerReview(request, options);
356
+ return {
357
+ reconcile: {
358
+ executor: reconcileExecutor,
359
+ budgetChars: LEDGER_RECONCILIATION_DEFAULT_BUDGET_CHARS,
360
+ },
361
+ reviewer,
362
+ };
363
+ }
364
+ /**
365
+ * 接线侧门(AC-PROD-005):taskKind === 'frontend-implementation'(input.taskKind
366
+ * 优先,缺省回退 loadTaskConfig(repoRoot, taskId).taskKind)。ineligible 返回
367
+ * undefined 不接线;配合 prepare.ts 的 parseable requirement/acceptance 前置
368
+ * 双重保证非 eligible 路径零 source-fidelity Pi 调用。
369
+ */
370
+ export async function resolveProductionSourceFidelityPi(input) {
371
+ let effectiveTaskKind = input.taskKind?.trim();
372
+ if (!effectiveTaskKind) {
373
+ try {
374
+ const config = await loadTaskConfig(input.repoRoot, input.taskId);
375
+ effectiveTaskKind = config.taskKind?.trim();
376
+ }
377
+ catch {
378
+ effectiveTaskKind = undefined;
379
+ }
380
+ }
381
+ if (effectiveTaskKind !== SOURCE_FIDELITY_TASK_KIND)
382
+ return undefined;
383
+ return createProductionSourceFidelityPi({ repoRoot: input.repoRoot });
384
+ }
@@ -5,3 +5,25 @@ export const PREPARE_DEFAULT_FORBIDDEN_PATHS = [
5
5
  ".harness/**",
6
6
  "node_modules/**",
7
7
  ];
8
+ // ---------------------------------------------------------------------------
9
+ // Source Fidelity Ledger (20260824-frontend-source-fidelity-ledger-v1)
10
+ // ---------------------------------------------------------------------------
11
+ // 原始 requirement/acceptance 文档保持语义权威;派生 source/需求.md 仅作导航视图。
12
+ // ledger 是二者之间确定性、可审计的映射账本:完整性由可审计账本、来源绑定和
13
+ // writer 前确定性门禁证明,摘要遗漏不再静默通过。
14
+ /** 稳定机器可判读错误码(SOURCE_FIDELITY_* 类目),writer 前门禁/修复路由消费。 */
15
+ export const SOURCE_FIDELITY_ERROR_CODES = [
16
+ "SOURCE_FIDELITY_LEDGER_MISSING",
17
+ "SOURCE_FIDELITY_LEDGER_STALE",
18
+ "SOURCE_FIDELITY_UNRESOLVED",
19
+ "SOURCE_FIDELITY_CONFLICT",
20
+ "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED",
21
+ "SOURCE_FIDELITY_CONTEXT_ONLY_UNREVIEWED",
22
+ "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED",
23
+ "SOURCE_FIDELITY_SOURCE_REF_INVALID",
24
+ "SOURCE_FIDELITY_FRAGMENT_INVALID",
25
+ "SOURCE_FIDELITY_RECONCILIATION_FAILED",
26
+ ];
27
+ export const LEDGER_SCHEMA_VERSION = 1;
28
+ export const LEDGER_SCHEMA_RECONCILER_VERSION = "ledger-reconciler-v1";
29
+ export const REQUIREMENT_LEDGER_FILE_NAME = "requirement-ledger.json";
@@ -1,11 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from "node:fs/promises";
3
- import os from "node:os";
4
3
  import path from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
5
  import { Command } from "commander";
7
- import { createNpmUpdateClient } from "../shared/update/npm-client.js";
8
- import { runConsoleDailyUpdateCheck } from "../shared/update/console-notifier.js";
9
6
  import { readPackageVersion } from "../shared/package-metadata.js";
10
7
  import YAML from "yaml";
11
8
  import { LoopAgentClient } from "./loop-agent/loop-agent-client.js";
@@ -844,37 +841,26 @@ function parseOptionalTimeoutMs(raw) {
844
841
  export async function runConsoleServe(options) {
845
842
  const { createConsoleServer } = await import("./console/server.js");
846
843
  const { maybeOpenConsoleBrowser } = await import("./console/open-browser.js");
844
+ const { readConsoleHandoff, waitForOldConsoleExit } = await import("./console/console-handoff.js");
847
845
  try {
848
- try {
849
- const packageRoot = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
850
- const currentVersion = readPackageVersion(packageRoot) ?? "0.0.0";
851
- await runConsoleDailyUpdateCheck({
852
- currentVersion,
853
- packageRoot,
854
- homeDir: os.homedir(),
855
- cwd: options.repoRoot,
856
- env: process.env,
857
- stdinIsTTY: process.stdin.isTTY === true,
858
- stderrIsTTY: process.stderr.isTTY === true,
859
- client: createNpmUpdateClient({
860
- currentVersion,
861
- packageRoot,
862
- cwd: options.repoRoot,
863
- env: process.env,
864
- }),
865
- });
866
- }
867
- catch (error) {
868
- const detail = error instanceof Error ? error.message : String(error);
869
- process.stderr.write(`loop-agent 更新检查已跳过:${detail}\n`);
846
+ const handoff = readConsoleHandoff(process.env);
847
+ const appDataRoot = options.appDataRoot ?? handoff?.appDataRoot;
848
+ if (handoff) {
849
+ if (path.resolve(handoff.repoRoot) !== path.resolve(options.repoRoot) ||
850
+ handoff.host !== options.host ||
851
+ handoff.port !== options.port) {
852
+ throw new Error("console handoff does not match requested repo/host/port");
853
+ }
854
+ await waitForOldConsoleExit(handoff.oldPid);
870
855
  }
871
856
  const server = await createConsoleServer({
872
857
  repoRoot: options.repoRoot,
873
858
  host: options.host,
874
859
  port: options.port,
875
860
  debug: options.debug === true,
876
- appDataRoot: options.appDataRoot,
861
+ appDataRoot,
877
862
  defaultTimeoutMs: parseOptionalTimeoutMs(options.defaultTimeoutMsRaw),
863
+ ...(handoff ? { consoleInstanceId: handoff.consoleInstanceId } : {}),
878
864
  });
879
865
  // Keep stdout bind URL contract (may include 0.0.0.0 / ::); browser URL is normalized separately.
880
866
  process.stdout.write(`${server.url}
@@ -0,0 +1,82 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ const HANDOFF_ENV = "LOOP_CONSOLE_HANDOFF_V1";
3
+ const HANDOFF_HASH_ENV = "LOOP_CONSOLE_HANDOFF_SHA256";
4
+ function canonicalJson(value) {
5
+ return JSON.stringify(value);
6
+ }
7
+ function digest(value) {
8
+ return createHash("sha256").update(value, "utf8").digest("hex");
9
+ }
10
+ export function createConsoleHandoff(input) {
11
+ const handoff = {
12
+ schemaVersion: 1,
13
+ consoleInstanceId: `console_${randomBytes(16).toString("hex")}`,
14
+ ...input,
15
+ };
16
+ const encoded = canonicalJson(handoff);
17
+ return {
18
+ handoff,
19
+ env: {
20
+ [HANDOFF_ENV]: Buffer.from(encoded, "utf8").toString("base64url"),
21
+ [HANDOFF_HASH_ENV]: digest(encoded),
22
+ },
23
+ };
24
+ }
25
+ /** Parse an exact, hash-bound handoff contract inherited by the replacement process. */
26
+ export function readConsoleHandoff(env) {
27
+ const encoded = env[HANDOFF_ENV];
28
+ const expectedDigest = env[HANDOFF_HASH_ENV];
29
+ if (!encoded && !expectedDigest)
30
+ return undefined;
31
+ if (!encoded || !expectedDigest)
32
+ throw new Error("console handoff contract is incomplete");
33
+ let raw;
34
+ try {
35
+ raw = Buffer.from(encoded, "base64url").toString("utf8");
36
+ }
37
+ catch {
38
+ throw new Error("console handoff payload is invalid");
39
+ }
40
+ if (digest(raw) !== expectedDigest)
41
+ throw new Error("console handoff hash mismatch");
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(raw);
45
+ }
46
+ catch {
47
+ throw new Error("console handoff payload is not JSON");
48
+ }
49
+ const value = parsed;
50
+ if (value.schemaVersion !== 1 ||
51
+ !Number.isInteger(value.oldPid) ||
52
+ value.oldPid <= 0 ||
53
+ typeof value.repoRoot !== "string" ||
54
+ typeof value.host !== "string" ||
55
+ !Number.isInteger(value.port) ||
56
+ value.port < 0 ||
57
+ typeof value.targetVersion !== "string" ||
58
+ typeof value.consoleInstanceId !== "string") {
59
+ throw new Error("console handoff payload has invalid fields");
60
+ }
61
+ return value;
62
+ }
63
+ export async function waitForOldConsoleExit(oldPid, options = {}) {
64
+ const isAlive = options.isAlive ?? ((pid) => {
65
+ try {
66
+ process.kill(pid, 0);
67
+ return true;
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ });
73
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
74
+ const timeoutMs = options.timeoutMs ?? 15_000;
75
+ const started = Date.now();
76
+ while (isAlive(oldPid)) {
77
+ if (Date.now() - started >= timeoutMs) {
78
+ throw new Error(`old Console PID ${oldPid} did not exit before handoff timeout`);
79
+ }
80
+ await sleep(50);
81
+ }
82
+ }
@@ -0,0 +1,115 @@
1
+ import semver from "semver";
2
+ function notRunMaintenance(channel) {
3
+ return {
4
+ package: { status: "failed", channel },
5
+ initialization: { status: "not-run", pendingPaths: [] },
6
+ handoff: { status: "not-needed" },
7
+ };
8
+ }
9
+ function failed(code, message, maintenance = notRunMaintenance("latest")) {
10
+ return {
11
+ state: "failed",
12
+ maintenance,
13
+ errorCode: code,
14
+ errorMessage: message,
15
+ };
16
+ }
17
+ /**
18
+ * Deterministic core for the Console-only maintenance operation. It contains
19
+ * no HTTP, model, or process-global calls, which makes the safety ordering
20
+ * independently testable and keeps the caller responsible for privileged
21
+ * handoff mechanics.
22
+ */
23
+ export async function performConsoleUpdateAndInit(deps, channel = "latest") {
24
+ const preflight = await deps.preflight();
25
+ if (!preflight.ok) {
26
+ return failed(preflight.code, preflight.message, notRunMaintenance(channel));
27
+ }
28
+ const target = await deps.resolveVersion(channel);
29
+ if (!target.ok) {
30
+ return failed("REGISTRY_UNAVAILABLE", `could not verify npm ${channel} (${target.reason})`, notRunMaintenance(channel));
31
+ }
32
+ if (!semver.valid(deps.currentVersion) || !semver.valid(target.version)) {
33
+ return failed("INVALID_VERSION", "installed or selected channel version is invalid", notRunMaintenance(channel));
34
+ }
35
+ if (channel === "latest" && semver.lt(target.version, deps.currentVersion)) {
36
+ return failed("REGISTRY_VERSION_REGRESSION", `npm latest ${target.version} is older than installed ${deps.currentVersion}`, notRunMaintenance(channel));
37
+ }
38
+ const release = await deps.acquireLock();
39
+ if (!release) {
40
+ return failed("UPDATE_LOCKED", "another loop-agent update is already in progress for this user", notRunMaintenance(channel));
41
+ }
42
+ try {
43
+ const packageChanged = target.version !== deps.currentVersion;
44
+ const maintenance = {
45
+ package: {
46
+ status: packageChanged ? "updated" : "current",
47
+ channel,
48
+ targetVersion: target.version,
49
+ },
50
+ initialization: { status: "not-run", pendingPaths: [] },
51
+ handoff: { status: "not-needed" },
52
+ };
53
+ if (packageChanged) {
54
+ const proof = await deps.canAutoInstall();
55
+ if (!proof.ok) {
56
+ maintenance.package = { status: "failed", channel, reason: proof.reason };
57
+ return failed("GLOBAL_INSTALL_UNPROVEN", proof.reason, maintenance);
58
+ }
59
+ const installed = await deps.installExact(target.version, channel);
60
+ if (!installed.ok) {
61
+ maintenance.package = { status: "failed", channel, reason: installed.reason };
62
+ return failed("INSTALL_FAILED", installed.reason, maintenance);
63
+ }
64
+ const verified = await deps.verifyInstalled(target.version);
65
+ if (!verified.ok) {
66
+ maintenance.package = { status: "failed", channel, reason: verified.reason };
67
+ return failed("INSTALL_VERIFICATION_FAILED", verified.reason, maintenance);
68
+ }
69
+ }
70
+ const initResult = await deps.applySafeInit();
71
+ maintenance.initialization = initResult.ok
72
+ ? { status: "clean", pendingPaths: initResult.pendingPaths }
73
+ : {
74
+ status: "needs-reconcile",
75
+ pendingPaths: initResult.pendingPaths,
76
+ message: initResult.message,
77
+ };
78
+ if (!packageChanged) {
79
+ return {
80
+ state: initResult.ok ? "succeeded" : "needs-reconcile",
81
+ maintenance,
82
+ ...(initResult.ok
83
+ ? {}
84
+ : {
85
+ errorCode: "INIT_NEEDS_RECONCILE",
86
+ errorMessage: initResult.message,
87
+ }),
88
+ };
89
+ }
90
+ const handoff = await deps.requestHandoff({
91
+ targetVersion: target.version,
92
+ initResult,
93
+ });
94
+ if (!handoff.ok) {
95
+ maintenance.handoff = {
96
+ status: "needs-reconcile",
97
+ message: handoff.message,
98
+ };
99
+ return {
100
+ state: "needs-reconcile",
101
+ maintenance,
102
+ errorCode: "HANDOFF_FAILED",
103
+ errorMessage: handoff.message,
104
+ };
105
+ }
106
+ maintenance.handoff = {
107
+ status: "requested",
108
+ consoleInstanceId: handoff.consoleInstanceId,
109
+ };
110
+ return { state: "restarting", maintenance };
111
+ }
112
+ finally {
113
+ await release().catch(() => undefined);
114
+ }
115
+ }