@tea-agent/loop-agent 0.38.1 → 0.39.0-beta.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 (160) hide show
  1. package/CHANGELOG.md +115 -13
  2. package/README.md +2 -0
  3. package/dist/application/dag/generate-task-dag.js +9 -0
  4. package/dist/application/task-lifecycle/advance.js +4 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/init.js +2 -14
  7. package/dist/commands/task-advance.js +31 -0
  8. package/dist/executors/dag-pi-executor.js +41 -4
  9. package/dist/executors/pi-event-serializer.js +42 -1
  10. package/dist/executors/pi-executor.js +51 -0
  11. package/dist/executors/pi-sdk-executor.js +7 -26
  12. package/dist/executors/shell-executor.js +270 -16
  13. package/dist/governance/manifest-types.js +0 -2
  14. package/dist/shared/operator/capabilities.js +87 -22
  15. package/dist/task/config-types.js +22 -0
  16. package/dist/task/contract/constants.js +1 -0
  17. package/dist/task/contract/diff.js +26 -0
  18. package/dist/task/contract/project.js +5 -0
  19. package/dist/task/contract/schema.js +6 -1
  20. package/dist/task/source-prepare/build-draft.js +15 -0
  21. package/dist/worker/console/chat/deferred-turn.js +91 -0
  22. package/dist/worker/console/chat/pi-runtime.js +10 -5
  23. package/dist/worker/console/chat/routes.js +458 -47
  24. package/dist/worker/console/chat/semantic-activity.js +13 -13
  25. package/dist/worker/console/chat/session-store.js +146 -2
  26. package/dist/worker/console/harness-pi-model-matrix.js +2 -11
  27. package/dist/worker/console/operator-actions.js +5 -4
  28. package/dist/worker/console/pi-readiness.js +5 -4
  29. package/dist/worker/console/recovery-cta.js +4 -4
  30. package/dist/worker/console/recovery-error-copy.js +4 -4
  31. package/dist/worker/console/server.js +6 -1
  32. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
  33. package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-Bj2M8iO5.js} +1 -1
  34. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
  35. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
  36. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
  37. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
  38. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DvBN4JZc.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
  48. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
  49. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
  50. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CUZgsiRC.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
  51. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
  52. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
  53. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
  57. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
  58. package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
  59. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
  60. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
  61. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
  62. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
  63. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
  64. package/dist/worker/console/static/assets/index-DTZOKgAn.js +404 -0
  65. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
  66. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DzqWXNO_.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
  67. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
  68. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
  69. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
  70. package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-DdnapKIH.js} +1 -1
  71. package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-BVjAT9b8.js} +5 -5
  72. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
  73. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
  74. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
  75. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
  76. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
  77. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
  78. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
  79. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
  80. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
  81. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
  82. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
  83. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CcfkA-Dq.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
  84. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
  85. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DH8FjnOK.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
  86. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
  87. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
  88. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
  89. package/dist/worker/console/static/index.html +2 -2
  90. package/dist/worker/console/static-src/app/console-types.js +0 -12
  91. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  92. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  93. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +38 -1
  94. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  95. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  96. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  97. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  98. package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
  99. package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -1
  100. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +809 -53
  101. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  102. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  103. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  104. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  105. package/dist/workflows/dag/backend-test-layout.js +133 -0
  106. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  107. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  108. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  109. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  110. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  111. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  112. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  113. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  114. package/dist/workflows/dag/convergence/controller.js +141 -0
  115. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  116. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  117. package/dist/workflows/dag/frontend-implementation-contract.js +319 -13
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +200 -45
  119. package/dist/workflows/dag/init-hybrid.js +689 -91
  120. package/dist/workflows/dag/node-execution.js +71 -18
  121. package/dist/workflows/dag/prompt.js +68 -1
  122. package/dist/workflows/dag/rerun-feedback.js +212 -0
  123. package/dist/workflows/dag/rerun-plan.js +93 -27
  124. package/dist/workflows/dag/rerun-run.js +3 -0
  125. package/dist/workflows/dag/rerun-task.js +61 -2
  126. package/dist/workflows/dag/retry-policy.js +18 -0
  127. package/dist/workflows/dag/types.js +104 -0
  128. package/dist/workflows/dag/upstream-artifacts.js +4 -0
  129. package/docs/operations/local-development-environment.md +1 -5
  130. package/docs/skills/vetted-skill-registry.md +14 -0
  131. package/docs/templates/README.md +1 -1
  132. package/docs/templates/backend-test-dag.json +60 -35
  133. package/docs/templates/harness.schema.json +0 -8
  134. package/harness.json +1 -8
  135. package/package.json +8 -4
  136. package/skills/analyze-product-requirements/SKILL.md +8 -5
  137. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  138. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  139. package/skills/analyze-product-requirements/references/example.md +50 -0
  140. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  141. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  142. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  143. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  144. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  145. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  146. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  147. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  148. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  149. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  150. package/skills/improve-codebase-architecture/deepening.md +37 -0
  151. package/skills/improve-codebase-architecture/html-report.md +123 -0
  152. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  153. package/skills/improve-codebase-architecture/language.md +53 -0
  154. package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
  155. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
  156. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
  157. package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
  158. package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
  159. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
  160. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
@@ -4,14 +4,15 @@ import { z } from "zod";
4
4
  import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
5
5
  import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
6
6
  import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
7
- import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
7
+ import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_CANONICAL_CONTRACT_MAX_BYTES, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
8
8
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
9
9
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
10
10
  import { pathMatchesPattern } from "../../shared/git-progress.js";
11
+ import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
11
12
  export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
12
13
  export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
13
14
  /** Canonical contract serialization must stay at or below this byte size. */
14
- export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
15
+ export const FRONTEND_CONTRACT_MAX_BYTES = FRONTEND_CANONICAL_CONTRACT_MAX_BYTES;
15
16
  /**
16
17
  * Machine-readable failure classification for blocked/retryable-invalid
17
18
  * prewrite outcomes. Kept alongside the human-readable failureReason so
@@ -39,6 +40,7 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
39
40
  "component-spec-not-cited",
40
41
  "component-choices-missing",
41
42
  "contract-too-large",
43
+ "revision-patch-unmergeable",
42
44
  ]);
43
45
  export const frontendPrewriteResultV1Schema = z
44
46
  .object({
@@ -160,9 +162,12 @@ async function readNodeText(runDir, nodeId) {
160
162
  if (record.status !== "FINISHED") {
161
163
  throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
162
164
  }
163
- const text = [record.assistantText, record.stdout]
164
- .filter((value) => Boolean(value?.trim()))
165
- .join("\n");
165
+ // Pi execution mirrors a successful structured-contract normalization into
166
+ // both assistantText and stdout. Treat node output the same way as the DAG
167
+ // runtime's canonicalNodeOutput(): assistantText is authoritative and stdout
168
+ // is only a fallback. Concatenating the two duplicate fields makes a single
169
+ // canonical plan appear as two complete JSON contracts to the prewrite gate.
170
+ const text = record.assistantText?.trim() || record.stdout?.trim() || "";
166
171
  if (!text)
167
172
  throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
168
173
  return text;
@@ -171,16 +176,129 @@ async function readNodeRawText(runDir, nodeId) {
171
176
  const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
172
177
  return record.assistantText ?? record.stdout ?? "";
173
178
  }
179
+ function resolveRunOwnedArtifact(runDir, candidate, label) {
180
+ const root = path.resolve(runDir);
181
+ const resolved = path.resolve(candidate);
182
+ const relative = path.relative(root, resolved);
183
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
184
+ throw new Error(`frontend prewrite gate ${label} must be contained by the current run directory`);
185
+ }
186
+ return resolved;
187
+ }
188
+ async function readHashBoundArtifact(input) {
189
+ const resolved = resolveRunOwnedArtifact(input.runDir, input.artifactPath, input.label);
190
+ const text = await readFile(resolved, "utf8");
191
+ const actual = sha256Hex(text);
192
+ if (actual !== input.expectedSha256) {
193
+ throw new Error(`frontend prewrite gate ${input.label} SHA-256 mismatch (expected ${input.expectedSha256}, got ${actual})`);
194
+ }
195
+ return text;
196
+ }
197
+ async function readFrontendPlanNodeInput(runDir, nodeId) {
198
+ const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
199
+ if (record.status !== "FINISHED") {
200
+ throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
201
+ }
202
+ const frontend = record.frontendPlanOutput;
203
+ if (!frontend) {
204
+ const payloadText = record.assistantText?.trim() || record.stdout?.trim() || "";
205
+ if (!payloadText)
206
+ throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
207
+ return {
208
+ artifactFirst: false,
209
+ payloadText,
210
+ citationsText: payloadText,
211
+ rawCandidateSha256: sha256Hex(record.assistantText ?? record.stdout ?? ""),
212
+ payloadKind: "canonical-contract",
213
+ };
214
+ }
215
+ if (frontend.schemaVersion !== 1 ||
216
+ !record.structuredArtifactPath ||
217
+ !record.structuredArtifactSha256 ||
218
+ !record.structuredArtifactSchemaId) {
219
+ throw new Error(`frontend prewrite gate ${nodeId} has incomplete artifact-first metadata`);
220
+ }
221
+ const expectedSchema = frontend.payloadKind === "canonical-contract"
222
+ ? "frontend-implementation-contract-v1"
223
+ : "frontend-implementation-contract-revision-patch-v1";
224
+ if (record.structuredArtifactSchemaId !== expectedSchema) {
225
+ throw new Error(`frontend prewrite gate ${nodeId} artifact schema ${record.structuredArtifactSchemaId} does not match ${frontend.payloadKind}`);
226
+ }
227
+ const [payloadText, citationsText] = await Promise.all([
228
+ readHashBoundArtifact({
229
+ runDir,
230
+ artifactPath: record.structuredArtifactPath,
231
+ expectedSha256: record.structuredArtifactSha256,
232
+ label: `${nodeId} primary payload`,
233
+ }),
234
+ readHashBoundArtifact({
235
+ runDir,
236
+ artifactPath: frontend.citationsArtifactPath,
237
+ expectedSha256: frontend.citationsArtifactSha256,
238
+ label: `${nodeId} citations`,
239
+ }),
240
+ readHashBoundArtifact({
241
+ runDir,
242
+ artifactPath: frontend.rawCandidatePath,
243
+ expectedSha256: frontend.rawCandidateSha256,
244
+ label: `${nodeId} raw candidate`,
245
+ }).then(() => undefined),
246
+ ]);
247
+ return {
248
+ artifactFirst: true,
249
+ payloadText,
250
+ citationsText,
251
+ rawCandidateSha256: frontend.rawCandidateSha256,
252
+ payloadKind: frontend.payloadKind,
253
+ };
254
+ }
255
+ /**
256
+ * Same shape predicate as the internal `isContract` inside
257
+ * `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
258
+ * schemaVersion 1 + a plain `targets` object + `requirements` and
259
+ * `verificationTargets` arrays. A merge-patch delta does not carry these
260
+ * fields, so this is the deterministic boundary between a full contract and a
261
+ * patch. Keep the two predicates in sync.
262
+ */
263
+ function isFullContractShape(value) {
264
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
265
+ return false;
266
+ }
267
+ const record = value;
268
+ const targets = record.targets;
269
+ return (record.schemaVersion === 1 &&
270
+ targets !== null &&
271
+ typeof targets === "object" &&
272
+ !Array.isArray(targets) &&
273
+ Array.isArray(record.requirements) &&
274
+ Array.isArray(record.verificationTargets));
275
+ }
276
+ /**
277
+ * Shared merge pipeline for revisionPatch mode and content-driven
278
+ * compatibility: read the original contract node, extract the original
279
+ * contract and the effective node's patch, apply the RFC 7386 merge-patch, and
280
+ * serialize deterministically. Throws on any failure; callers map the error to
281
+ * their own classification (retryable-invalid for explicit revisionPatch mode,
282
+ * blocked for content-driven fallback).
283
+ */
284
+ async function mergeRevisionPatch(runDir, planText, originalNodeId) {
285
+ const originalText = await readNodeText(runDir, originalNodeId);
286
+ const originalValue = extractFrontendImplementationJson(originalText);
287
+ const patchValue = extractFrontendImplementationJson(planText);
288
+ const merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
289
+ return serializeDeterministicJson(merged);
290
+ }
174
291
  function firstNonEmptyVerdictLine(text) {
175
292
  const lines = text
176
293
  .split(/\r?\n/)
177
294
  .map((line) => line.trim())
178
295
  .filter(Boolean);
179
- const normalize = (line) => {
180
- const emphasized = line.match(/^(?:`{1,3}|\*{1,3})\s*(VERDICT:[^`*]+?)\s*(?:`{1,3}|\*{1,3})$/);
181
- return (emphasized?.[1] ?? line).trim();
182
- };
183
- const normalizedLines = lines.map(normalize);
296
+ // Emphasis stripping (backtick + asterisk) is shared with the runIf
297
+ // selector and node-level output-protocol validation via
298
+ // normalizeVerdictCandidateLine. The gate keeps its own loose
299
+ // pass/request-revision normalization below (prefix match + trailing
300
+ // prose tolerance) for deterministic fail-closed behavior.
301
+ const normalizedLines = lines.map(normalizeVerdictCandidateLine);
184
302
  const first = normalizedLines[0] ?? "";
185
303
  // The prompt requires VERDICT to be the first non-empty line, but model
186
304
  // output can still prepend a summary. Keep the protocol strict in the
@@ -416,9 +534,9 @@ export async function runFrontendPrewriteGate(input) {
416
534
  openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
417
535
  openspecPolicy,
418
536
  };
419
- let planText;
537
+ let planInput;
420
538
  try {
421
- planText = await readNodeText(input.runDir, planNodeId);
539
+ planInput = await readFrontendPlanNodeInput(input.runDir, planNodeId);
422
540
  }
423
541
  catch (error) {
424
542
  return finalizePrewrite(input, {
@@ -428,6 +546,9 @@ export async function runFrontendPrewriteGate(input) {
428
546
  failureCode: "plan-text-unreadable",
429
547
  });
430
548
  }
549
+ const planText = planInput.payloadText;
550
+ const citationsText = planInput.citationsText;
551
+ basePending.candidateRawSha256 = planInput.rawCandidateSha256;
431
552
  let reviewText;
432
553
  try {
433
554
  reviewText = await readNodeText(input.runDir, reviewNodeId);
@@ -459,9 +580,21 @@ export async function runFrontendPrewriteGate(input) {
459
580
  let effectiveContractText = null;
460
581
  if (revisionPatchMode) {
461
582
  const originalNodeId = input.config.planFallbackFromNodeIds[0];
462
- let originalText;
463
583
  try {
464
- originalText = await readNodeText(input.runDir, originalNodeId);
584
+ const originalInput = await readFrontendPlanNodeInput(input.runDir, originalNodeId);
585
+ if (planInput.artifactFirst && !originalInput.artifactFirst) {
586
+ throw new Error(`artifact-first revision ${planNodeId} requires artifact-first original contract ${originalNodeId}`);
587
+ }
588
+ if (planInput.artifactFirst) {
589
+ if (planInput.payloadKind !== "merge-patch" ||
590
+ originalInput.payloadKind !== "canonical-contract") {
591
+ throw new Error("artifact-first revision/original payload kinds are invalid");
592
+ }
593
+ effectiveContractText = serializeDeterministicJson(applyFrontendImplementationContractPatch(JSON.parse(originalInput.payloadText), JSON.parse(planInput.payloadText)));
594
+ }
595
+ else {
596
+ effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
597
+ }
465
598
  }
466
599
  catch (error) {
467
600
  return finalizePrewrite(input, {
@@ -472,35 +605,54 @@ export async function runFrontendPrewriteGate(input) {
472
605
  failureCode: "contract-invalid",
473
606
  });
474
607
  }
475
- let originalValue;
476
- let patchValue;
608
+ }
609
+ else {
610
+ // Content-driven compatibility (Bug 1): legacy DAGs without
611
+ // revisionPatch can still receive a merge-patch delta from the revision
612
+ // node. Detect the output shape instead of trusting the DAG flag alone.
613
+ // - Full contract shape → keep legacy full-contract semantics unchanged.
614
+ // - Patch shape with a usable original contract node → auto-merge and
615
+ // run the existing analysis/materialization pipeline.
616
+ // - Patch shape but unmergeable → blocked + revision-patch-unmergeable
617
+ // (no retryable loop).
618
+ let planJsonValue = null;
477
619
  try {
478
- originalValue = extractFrontendImplementationJson(originalText);
479
- patchValue = extractFrontendImplementationJson(planText);
480
- }
481
- catch (error) {
482
- return finalizePrewrite(input, {
483
- ...basePending,
484
- verdict,
485
- classification: "retryable-invalid",
486
- failureReason: `frontend prewrite gate revisionPatch mode failed to parse contract/patch: ${error instanceof Error ? error.message : String(error)}`,
487
- failureCode: "contract-invalid",
488
- });
620
+ planJsonValue = extractFrontendImplementationJson(planText);
489
621
  }
490
- let merged;
491
- try {
492
- merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
622
+ catch {
623
+ // Not machine-readable JSON: fall back to the legacy full-contract
624
+ // path where analyzeFrontendImplementationContract surfaces the
625
+ // original diagnostic (fail-closed without a new failure mode).
493
626
  }
494
- catch (error) {
495
- return finalizePrewrite(input, {
496
- ...basePending,
497
- verdict,
498
- classification: "retryable-invalid",
499
- failureReason: `frontend prewrite gate revisionPatch apply failed: ${error instanceof Error ? error.message : String(error)}`,
500
- failureCode: "contract-invalid",
501
- });
627
+ if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
628
+ const originalNodeId = input.config.planFallbackFromNodeIds[0];
629
+ if (!originalNodeId || originalNodeId === planNodeId) {
630
+ return finalizePrewrite(input, {
631
+ ...basePending,
632
+ verdict,
633
+ classification: "blocked",
634
+ failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
635
+ failureCode: "revision-patch-unmergeable",
636
+ });
637
+ }
638
+ try {
639
+ const originalText = await readNodeText(input.runDir, originalNodeId);
640
+ const originalValue = extractFrontendImplementationJson(originalText);
641
+ if (!isFullContractShape(originalValue)) {
642
+ throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
643
+ }
644
+ effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
645
+ }
646
+ catch (error) {
647
+ return finalizePrewrite(input, {
648
+ ...basePending,
649
+ verdict,
650
+ classification: "blocked",
651
+ failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
652
+ failureCode: "revision-patch-unmergeable",
653
+ });
654
+ }
502
655
  }
503
- effectiveContractText = serializeDeterministicJson(merged);
504
656
  }
505
657
  const requirementIdSearchText = effectiveContractText ?? planText;
506
658
  const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
@@ -550,14 +702,17 @@ export async function runFrontendPrewriteGate(input) {
550
702
  workspaceRoot,
551
703
  });
552
704
  }
553
- // Candidate -> canonical conversion. The gate consumes the selected
554
- // plan/revision assistant output directly and is the only conversion point.
705
+ // Candidate -> canonical conversion. Artifact-first inputs must keep using
706
+ // the already hash-verified payload: re-reading the selected node record
707
+ // here would silently fall back to its prose and defeat the integrity gate.
708
+ // Historical records intentionally retain their node-text compatibility path.
555
709
  let analysis;
556
710
  try {
711
+ const rawContractText = effectiveContractText ?? (planInput.artifactFirst ? planText : undefined);
557
712
  analysis = await analyzeFrontendImplementationContract({
558
713
  runDir: input.runDir,
559
- fromNodeId: effectiveContractText ? undefined : planNodeId,
560
- rawContractText: effectiveContractText ?? undefined,
714
+ fromNodeId: rawContractText ? undefined : planNodeId,
715
+ rawContractText,
561
716
  sourceBinding: input.sourceBinding,
562
717
  });
563
718
  }
@@ -702,7 +857,7 @@ export async function runFrontendPrewriteGate(input) {
702
857
  repoRoot: candidateRoot,
703
858
  });
704
859
  if (candidatePaths.length > 0) {
705
- const parsed = parseOpenspecCitations(planText, candidateRoot);
860
+ const parsed = parseOpenspecCitations(citationsText, candidateRoot);
706
861
  if (!parsed.blockPresent || parsed.unparseable) {
707
862
  return finalizePrewrite(input, {
708
863
  ...basePending,
@@ -843,7 +998,7 @@ export async function runFrontendPrewriteGate(input) {
843
998
  });
844
999
  }
845
1000
  if (openspecPolicy === "cited") {
846
- const parsedCitations = parseOpenspecCitations(planText, candidateRoot);
1001
+ const parsedCitations = parseOpenspecCitations(citationsText, candidateRoot);
847
1002
  const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
848
1003
  const readSet = new Set(openspecReadPaths);
849
1004
  const uncorroborated = [