@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
@@ -34,6 +34,7 @@ import {
34
34
  repoBaseName,
35
35
  parseMarkdownBlocks,
36
36
  markdownLinkHref,
37
+ formatLedgerSummary,
37
38
  } from "../format.js";
38
39
  import { parseRoute, navigate, buildHashPath } from "../router.js";
39
40
  import {
@@ -94,6 +95,7 @@ export async function renderTaskDetail(featureId, taskId) {
94
95
  const linksEl = document.getElementById("task-links");
95
96
  const evidenceEl = document.getElementById("task-evidence");
96
97
  const runsEl = document.getElementById("task-runs");
98
+ const ledgerEl = document.getElementById("task-ledger");
97
99
  if (!metaEl || !linksEl || !evidenceEl || !runsEl) return false;
98
100
  if (titleEl) titleEl.textContent = displayLabel;
99
101
  mountViewState(metaEl, "loading", "加载 Task…");
@@ -252,9 +254,62 @@ export async function renderTaskDetail(featureId, taskId) {
252
254
  const active = ["running", "pending", "blocked", "stale"].includes(
253
255
  (task.status ?? "").toLowerCase(),
254
256
  );
257
+ if (ledgerEl) {
258
+ void renderTaskLedger(ledgerEl, taskId);
259
+ }
255
260
  return active;
256
261
  }
257
262
 
263
+ /**
264
+ * Task 详情页 ledger 安全摘要(AC-009):从只读 GET /api/tasks/:id/ledger 拉取,
265
+ * 计数/状态/failure code + 有界排除理由;无 ledger 显示空态,失败安全降级。
266
+ */
267
+ async function renderTaskLedger(ledgerEl, taskId) {
268
+ clearNode(ledgerEl);
269
+ ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
270
+ mountViewState(ledgerEl, "loading", "加载 ledger…");
271
+ let body = null;
272
+ try {
273
+ body = await fetchJson(`/api/tasks/${encodeURIComponent(taskId)}/ledger`);
274
+ } catch {
275
+ body = null;
276
+ }
277
+ if (!body) {
278
+ mountViewState(ledgerEl, "error", "ledger 只读接口失败(安全降级)。");
279
+ return;
280
+ }
281
+ if (body.present !== true) {
282
+ mountViewState(
283
+ ledgerEl,
284
+ body.error ? "error" : "empty",
285
+ body.error
286
+ ? `ledger 不可用(${body.error})。`
287
+ : "无 source fidelity ledger(任务无引用或 ledger 缺失,不伪造计数)。",
288
+ );
289
+ return;
290
+ }
291
+ const pre = el(
292
+ "pre",
293
+ "cmd",
294
+ formatLedgerSummary({
295
+ present: true,
296
+ fragmentCount: body.fragmentCount,
297
+ canonicalRequirementCount: body.canonicalRequirementCount,
298
+ contextOnlyCount: body.contextOnlyCount,
299
+ duplicateCount: body.duplicateCount,
300
+ unresolvedCount: body.unresolvedCount,
301
+ conflictCount: body.conflictCount,
302
+ gateStatus: body.gateStatus,
303
+ failureCode: body.failureCode,
304
+ exclusions: body.exclusions ?? [],
305
+ }),
306
+ );
307
+ pre.dataset.testid = "task-ledger-summary";
308
+ clearNode(ledgerEl);
309
+ ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
310
+ ledgerEl.appendChild(pre);
311
+ }
312
+
258
313
  export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
259
314
  if (runsEl === undefined) {
260
315
  runsEl = before;
@@ -148,9 +148,10 @@ export function checkMockStrategy(contract, allowedMockStrategies) {
148
148
  }
149
149
  return findings;
150
150
  }
151
- /** 4. Component choice must be precisely spec-referenced: `specified` requires
152
- * a specReference whose normalized path is a candidate and a non-empty section;
153
- * `new` must not carry a specReference; `reuse-existing` may omit it. The
151
+ /** 4. Component choice must be precisely evidence-referenced: `specified`
152
+ * requires a specReference whose normalized path is a candidate and a non-empty
153
+ * section; `new` requires a task-source reference with a non-empty section;
154
+ * `reuse-existing` may omit it. The
154
155
  * candidate set is the full generation-frozen openspec candidate list (the
155
156
  * shell passes openspecCandidatePaths with componentSpecCandidatePaths as the
156
157
  * fallback), so directory-organized component libraries are never rejected.
@@ -199,12 +200,21 @@ export function checkComponentChoice(contract, componentSpecCandidatePaths) {
199
200
  });
200
201
  }
201
202
  }
202
- else if (choice.decision === "new" && choice.specReference) {
203
- findings.push({
204
- code: "component-new-with-spec-reference",
205
- message: `new component choice #${index} must not carry a specReference`,
206
- path: choice.purpose,
207
- });
203
+ else if (choice.decision === "new") {
204
+ if (!choice.specReference || !choice.specReference.path) {
205
+ findings.push({
206
+ code: "component-new-source-reference-missing",
207
+ message: `new component choice #${index} is missing its task-source specReference`,
208
+ path: choice.purpose,
209
+ });
210
+ }
211
+ else if (!choice.specReference.section.trim()) {
212
+ findings.push({
213
+ code: "component-new-source-reference-missing",
214
+ message: `new component choice #${index} has an empty task-source reference section`,
215
+ path: choice.purpose,
216
+ });
217
+ }
208
218
  }
209
219
  });
210
220
  return findings;
@@ -13,7 +13,9 @@ import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
13
13
  // only at call time, so the contract module and the policy module can depend
14
14
  // on each other without module-init ordering issues.
15
15
  import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
16
+ import { isDagSourceBindingV2 } from "./types.js";
16
17
  import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
18
+ import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
17
19
  import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
18
20
  export const frontendNormalizationActionSchema = z.enum([
19
21
  "remove-trailing-commas",
@@ -286,13 +288,13 @@ const gap = z
286
288
  * `specReference` is required (path/section/line hit).
287
289
  * - `reuse-existing`: reuse an existing repo component/convention not named by
288
290
  * the spec → `specReference` may be null, rationale explains the basis.
289
- * - `new`: neither spec nor existing code fits `specReference` must be null,
290
- * rationale must declare the deviation (design-review approves it).
291
+ * - `new`: the task source mandates a component absent from the repository
292
+ * `specReference` is required and points at that task-source declaration;
293
+ * rationale explains the bounded addition.
291
294
  *
292
- * `specReference` is nullable + optional because `stripNullValuesDeep` strips
293
- * null object values before validation, so a `new` choice's `specReference:
294
- * null` becomes an absent field; the superRefine below treats absent and null
295
- * identically.
295
+ * `specReference` is nullable + optional at the wire boundary because
296
+ * `stripNullValuesDeep` removes model-emitted nulls before validation. The
297
+ * decision-specific requirements are enforced by the superRefine below.
296
298
  */
297
299
  const uiComponentSpecReferenceSchema = z
298
300
  .object({
@@ -308,7 +310,8 @@ const uiComponentChoiceSchema = z
308
310
  decision: z.enum(["specified", "reuse-existing", "new"]),
309
311
  specReference: uiComponentSpecReferenceSchema.nullable().optional(),
310
312
  // Optional: the decision enum already carries the semantic (specified →
311
- // specReference, new → deviation, reuse-existing → no rationale needed).
313
+ // specReference, new → task-source traceability, reuse-existing → no
314
+ // rationale needed).
312
315
  // Omit for reuse-existing to keep plan output small.
313
316
  rationale: z.string().min(1).optional(),
314
317
  })
@@ -338,49 +341,12 @@ function stripNullValuesDeep(value) {
338
341
  }
339
342
  return value;
340
343
  }
341
- /**
342
- * Normalize a model-emitted `uiComponentChoices` where a `new` choice carries a
343
- * degenerate empty `specReference` object (`{ path: "", section: "", line:
344
- * null }`) instead of the required `null`. The model intent is unambiguous
345
- * (new components must not carry a specReference; the empty object is a format
346
- * slip, not a real spec hit), so dropping the empty object before validation
347
- * avoids a full plan retry on an otherwise-correct emission. A `specReference`
348
- * with a non-empty `path` is preserved (it is a genuine `specified` hit).
349
- */
350
- export function normalizeNewComponentSpecReference(value) {
351
- if (!Array.isArray(value))
352
- return value;
353
- return value.map((item) => {
354
- if (!item || typeof item !== "object")
355
- return item;
356
- const record = item;
357
- if (record.decision !== "new")
358
- return item;
359
- const ref = record.specReference;
360
- if (!ref || typeof ref !== "object")
361
- return item;
362
- const refRecord = ref;
363
- const path = typeof refRecord.path === "string" ? refRecord.path.trim() : "";
364
- if (path.length > 0)
365
- return item;
366
- // Empty/degenerate specReference on a new choice → absent (null intent).
367
- const { specReference: _dropped, ...rest } = record;
368
- return rest;
369
- });
370
- }
371
344
  export const frontendImplementationContractSchema = z
372
345
  .preprocess((value) => {
373
346
  const record = asRecord(value);
374
347
  if (!record)
375
348
  return stripNullValuesDeep(value);
376
- return stripNullValuesDeep({
377
- ...record,
378
- ...(record.uiComponentChoices !== undefined
379
- ? {
380
- uiComponentChoices: normalizeNewComponentSpecReference(record.uiComponentChoices),
381
- }
382
- : {}),
383
- });
349
+ return stripNullValuesDeep(record);
384
350
  }, z
385
351
  .object({
386
352
  schemaVersion: z.literal(1),
@@ -391,8 +357,33 @@ export const frontendImplementationContractSchema = z
391
357
  requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
392
358
  referencePaths: z.array(safePath),
393
359
  requirementIds: z.array(id),
360
+ // Source binding v2 (frontend): bound ledger pointer + fragment map.
361
+ ledgerPath: safePath.optional(),
362
+ ledgerSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
363
+ inputDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
364
+ requirementToFragments: z
365
+ .record(z.string(), z.array(z.string()))
366
+ .optional(),
394
367
  })
395
- .strict(),
368
+ .strict()
369
+ .superRefine((value, ctx) => {
370
+ // v2 all-or-none:任一 ledger 字段出现则四项必须齐全,防止 v1+optional 冒充 v2。
371
+ const anyLedgerField = value.ledgerPath !== undefined ||
372
+ value.ledgerSha256 !== undefined ||
373
+ value.inputDigest !== undefined ||
374
+ value.requirementToFragments !== undefined;
375
+ const allLedgerFields = value.ledgerPath !== undefined &&
376
+ value.ledgerSha256 !== undefined &&
377
+ value.inputDigest !== undefined &&
378
+ value.requirementToFragments !== undefined;
379
+ if (anyLedgerField && !allLedgerFields) {
380
+ ctx.addIssue({
381
+ code: z.ZodIssueCode.custom,
382
+ path: ["sourceBinding"],
383
+ message: "source binding v2 ledger fields (ledgerPath/ledgerSha256/inputDigest/requirementToFragments) must be provided together; partial or optional v1+ledger bindings are rejected",
384
+ });
385
+ }
386
+ }),
396
387
  riskLevel: z.enum(["small", "standard", "high-risk"]),
397
388
  targets: z
398
389
  .object({
@@ -408,6 +399,10 @@ export const frontendImplementationContractSchema = z
408
399
  implementationTargets: z.array(safePath),
409
400
  verificationTargetIds: z.array(z.string().min(1)),
410
401
  evidenceGap: gap.optional(),
402
+ // Source fidelity ledger binding (AC-005/AC-006): 每个 requirement
403
+ // 携带来源 fragment 证据,prewrite 门禁校验其真实性与覆盖。
404
+ sourceFragmentIds: z.array(z.string().min(1)).optional(),
405
+ sourceRefs: z.array(safePath).optional(),
411
406
  })
412
407
  .strict()).min(1),
413
408
  uiStates: z.array(z
@@ -544,6 +539,32 @@ export const frontendImplementationContractSchema = z
544
539
  message: `unknown verification target ${targetId}`,
545
540
  path: ["requirements"],
546
541
  });
542
+ // Source fidelity ledger binding (AC-005/AC-006): 绑定携带
543
+ // requirementToFragments(ledger v2)时,每个 requirement 必须携带
544
+ // 非空 sourceFragmentIds,否则 fail closed(防引用伪造/缺失)。
545
+ // sourceRefs(fragment→path 的派生展示)不设硬门槛:模型从 binding
546
+ // 只能拿到 fragment id 拿不到权威 path,强制只会诱导编造;防伪造的
547
+ // 真实性校验由 fidelity gate 按 sourceFragmentIds 执行(存在性 +
548
+ // 与 ledger boundFragments 一致)。
549
+ const boundFragments = value.sourceBinding.requirementToFragments?.[requirement.id];
550
+ if (value.sourceBinding.ledgerPath &&
551
+ (!requirement.sourceFragmentIds ||
552
+ requirement.sourceFragmentIds.length === 0)) {
553
+ ctx.addIssue({
554
+ code: "custom",
555
+ message: `requirement ${requirement.id} must carry sourceFragmentIds when a source fidelity ledger is bound`,
556
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
557
+ });
558
+ }
559
+ else if (boundFragments !== undefined &&
560
+ (!requirement.sourceFragmentIds ||
561
+ requirement.sourceFragmentIds.length === 0)) {
562
+ ctx.addIssue({
563
+ code: "custom",
564
+ message: `requirement ${requirement.id} is bound to ledger fragments ${boundFragments.join(", ")} but declares none`,
565
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
566
+ });
567
+ }
547
568
  }
548
569
  for (const state of value.uiStates) {
549
570
  if (state.applicable &&
@@ -573,10 +594,10 @@ export const frontendImplementationContractSchema = z
573
594
  }
574
595
  }
575
596
  else if (choice.decision === "new") {
576
- if (choice.specReference) {
597
+ if (!choice.specReference || !choice.specReference.path) {
577
598
  ctx.addIssue({
578
599
  code: "custom",
579
- message: "new component choice must not carry a specReference",
600
+ message: "new component choice requires a task-source specReference.path",
580
601
  path: ["uiComponentChoices", index, "specReference"],
581
602
  });
582
603
  }
@@ -680,6 +701,29 @@ export async function assertFrontendSourceBindingFresh(input) {
680
701
  throw new Error(`frontend source binding is stale: ${source.path}`);
681
702
  }
682
703
  }
704
+ // Source binding v2 ledger 新鲜度(AC-FIDELITY-REPAIR-001/002):v2 绑定携带的
705
+ // ledger 文件必须存在且 sha256 与绑定一致,否则 fail closed;v1 绑定无此路径。
706
+ if (isDagSourceBindingV2(input.binding)) {
707
+ const absolute = resolveDagTaskSourcePath({
708
+ workspaceRoot: input.workspaceRoot,
709
+ taskId: input.binding.taskId,
710
+ sourcePath: input.binding.ledgerPath,
711
+ });
712
+ let content;
713
+ try {
714
+ content = await readFile(absolute);
715
+ const info = await stat(absolute);
716
+ if (!info.isFile())
717
+ throw new Error("not a regular file");
718
+ }
719
+ catch (error) {
720
+ throw new Error(`frontend source fidelity ledger unavailable: ${input.binding.ledgerPath}: ${error instanceof Error ? error.message : String(error)}`);
721
+ }
722
+ const actual = createHash("sha256").update(content).digest("hex");
723
+ if (actual !== input.binding.ledgerSha256) {
724
+ throw new Error(`frontend source fidelity ledger is stale or tampered: ${input.binding.ledgerPath} (bound ${input.binding.ledgerSha256}, observed ${actual})`);
725
+ }
726
+ }
683
727
  }
684
728
  const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
685
729
  const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
@@ -854,7 +898,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
854
898
  const requirement = binding.sources.find((source) => source.kind === "requirement");
855
899
  if (!requirement)
856
900
  throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
857
- return {
901
+ const sourceBinding = {
858
902
  taskId: binding.taskId,
859
903
  requirementPath: requirement.path,
860
904
  requirementSha256: requirement.sha256,
@@ -864,6 +908,15 @@ export function canonicalFrontendContractSourceBinding(binding) {
864
908
  .sort(),
865
909
  requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
866
910
  };
911
+ // Source binding v2(schemaVersion=2 强制字段):透传 ledger 指针与
912
+ // requirement→fragment 映射。v1 绑定不得以 optional 字段冒充 v2。
913
+ if (isDagSourceBindingV2(binding)) {
914
+ sourceBinding.ledgerPath = binding.ledgerPath;
915
+ sourceBinding.ledgerSha256 = binding.ledgerSha256;
916
+ sourceBinding.inputDigest = binding.inputDigest;
917
+ sourceBinding.requirementToFragments = binding.requirementToFragments;
918
+ }
919
+ return sourceBinding;
867
920
  }
868
921
  function asRecord(value) {
869
922
  return value && typeof value === "object" && !Array.isArray(value)
@@ -1436,6 +1489,14 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1436
1489
  verificationTargetIds: verificationTargetIds.length > 0
1437
1490
  ? verificationTargetIds
1438
1491
  : defaultVerificationIds,
1492
+ // Source fidelity provenance 透传:兼容分支重建 requirement 时必须
1493
+ // 保留已声明的 sourceFragmentIds/sourceRefs(strict 分支通过
1494
+ // `...requirement` 保留,两边必须一致),否则 ledger-bound 契约在
1495
+ // designEvidence 缺失等情况下走兼容分支时丢失防伪造证据。
1496
+ ...(Array.isArray(req.sourceFragmentIds)
1497
+ ? { sourceFragmentIds: req.sourceFragmentIds }
1498
+ : {}),
1499
+ ...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
1439
1500
  };
1440
1501
  // Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
1441
1502
  if (gapText) {
@@ -2075,11 +2136,17 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
2075
2136
  sha256: canonicalArtifact.sha256,
2076
2137
  schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
2077
2138
  };
2139
+ // Human-readable plan summary appended after the artifact pointer. The
2140
+ // pointer remains the machine authority (path/schema/sha256); the
2141
+ // rendered summary is a deterministic view of the canonical contract so
2142
+ // Console/reviewers can read the plan without opening the JSON. It never
2143
+ // participates in downstream validation.
2144
+ const planSummary = renderFrontendPlanMarkdown(analysis.canonical);
2078
2145
  return {
2079
2146
  ok: true,
2080
2147
  contract: analysis.canonical,
2081
2148
  artifact,
2082
- normalizedText: formatStructuredArtifactPointer(artifact),
2149
+ normalizedText: `${formatStructuredArtifactPointer(artifact)}\n\n${planSummary}`,
2083
2150
  };
2084
2151
  }
2085
2152
  catch (error) {
@@ -47,6 +47,27 @@ function componentSelection(contract) {
47
47
  })
48
48
  .join("\n");
49
49
  }
50
+ /**
51
+ * 有界 source fidelity ledger 指针(AC-007 / AC-FIDELITY-007):长原文不重新
52
+ * 全量内联到节点提示词;只注入紧凑 canonical contract 与 bound ledger 指针
53
+ * (路径/哈希/inputDigest),ledger coverage 证明长尾未丢失。
54
+ */
55
+ function sourceFidelityLedger(contract) {
56
+ const binding = contract.sourceBinding;
57
+ if (!binding.ledgerPath)
58
+ return "_(not bound — v1 source binding)_";
59
+ const lines = [
60
+ `- ledger: ${binding.ledgerPath}`,
61
+ `- ledgerSha256: ${binding.ledgerSha256 ?? "(missing)"}`,
62
+ `- inputDigest: ${binding.inputDigest ?? "(missing)"}`,
63
+ "- 语义权威:原始 requirement/acceptance 文档与 requirement-ledger.json;本计划只消费派生摘要与 bound ledger 指针,长原文不内联。",
64
+ ];
65
+ const boundCount = Object.keys(binding.requirementToFragments ?? {}).length;
66
+ if (boundCount > 0) {
67
+ lines.push(`- requirement→fragment bindings: ${boundCount}`);
68
+ }
69
+ return lines.join("\n");
70
+ }
50
71
  export function renderFrontendPlanMarkdown(contract) {
51
72
  return [
52
73
  "# Frontend Implementation Plan",
@@ -54,6 +75,12 @@ export function renderFrontendPlanMarkdown(contract) {
54
75
  "## Requirement Coverage",
55
76
  requirementsCoverage(contract),
56
77
  "",
78
+ "## Source Fidelity Ledger",
79
+ sourceFidelityLedger(contract),
80
+ "",
81
+ "## Implementation Steps",
82
+ bulletList(contract.implementationSteps) || "_(not specified)_",
83
+ "",
57
84
  "## Target Files",
58
85
  bulletList(contract.targets.files) || "_(none)_",
59
86
  "",
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  import { OPENSPEC_SPEC_EXT_RE, isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
6
6
  import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
7
7
  import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
8
+ import { checkFrontendSourceFidelityLedger, } from "./frontend-source-fidelity-ledger.js";
8
9
  import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
9
10
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
10
11
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
@@ -42,6 +43,14 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
42
43
  "component-choices-missing",
43
44
  "contract-too-large",
44
45
  "revision-patch-unmergeable",
46
+ // Source fidelity ledger 类目(SOURCE_FIDELITY_*/source-* 稳定错误码)。
47
+ "source-fidelity-ledger-missing",
48
+ "source-fidelity-ledger-stale",
49
+ "source-fidelity-unresolved",
50
+ "source-fidelity-conflict",
51
+ "source-fidelity-high-risk-uncovered",
52
+ "source-fidelity-requirement-not-covered",
53
+ "source-fidelity-source-ref-invalid",
45
54
  ]);
46
55
  export const frontendPrewriteResultV1Schema = z
47
56
  .object({
@@ -503,6 +512,7 @@ async function finalizePrewrite(input, pending) {
503
512
  openspecPolicy: pending.openspecPolicy,
504
513
  };
505
514
  }
515
+ export { checkFrontendSourceFidelityLedger } from "./frontend-source-fidelity-ledger.js";
506
516
  export async function runFrontendPrewriteGate(input) {
507
517
  const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
508
518
  // selectNode throws for a scheduling defect (no FINISHED plan/review). That
@@ -647,6 +657,33 @@ export async function runFrontendPrewriteGate(input) {
647
657
  }
648
658
  throw error;
649
659
  }
660
+ // Source fidelity ledger gate (20260824-frontend-source-fidelity-ledger-v1):
661
+ // 在 verdict/missing-requirement-ids/source-binding-not-fresh 之后插入 ledger
662
+ // 检查。缺失即 fail closed 且 writer 不获授权;所有检查通过才继续物化契约。
663
+ if (input.sourceBinding) {
664
+ const ledgerCheck = await checkFrontendSourceFidelityLedger({
665
+ workspaceRoot: workspaceRoot ?? "",
666
+ binding: input.sourceBinding,
667
+ contractRequirements: analysis.canonical.requirements.map((requirement) => ({
668
+ id: requirement.id,
669
+ sourceFragmentIds: requirement.sourceFragmentIds,
670
+ sourceRefs: requirement.sourceRefs,
671
+ })),
672
+ });
673
+ if (!ledgerCheck.ok && ledgerCheck.failureCode) {
674
+ return finalizePrewrite(input, {
675
+ ...basePending,
676
+ verdict,
677
+ candidateJsonSha256: analysis.candidateJsonSha256,
678
+ normalizationActions: [],
679
+ mockStrategy: analysis.canonical.mockApi.strategy,
680
+ classification: "blocked",
681
+ failureReason: ledgerCheck.failureReason ??
682
+ "frontend source fidelity ledger gate failed",
683
+ failureCode: ledgerCheck.failureCode,
684
+ });
685
+ }
686
+ }
650
687
  // Size guard: a canonical contract above 64KB is a retryable-invalid defect
651
688
  // (compress the contract) rather than a writer authorization failure.
652
689
  const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
@@ -873,13 +910,17 @@ export async function runFrontendPrewriteGate(input) {
873
910
  normalizationActions: [],
874
911
  mockStrategy,
875
912
  classification: "retryable-invalid",
876
- failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new),specified 必须给 specReference.path/section/line,new 必须给 rationale 声明偏差。",
913
+ failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new);specified 必须给 OpenSpec specReference.path/section/line,new 必须给任务源 PRD specReference.path/section/line。",
877
914
  failureCode: "component-choices-missing",
878
915
  openspecReadPaths,
879
916
  openspecCandidatePaths: candidatePaths,
880
917
  });
881
918
  }
919
+ // Only `specified` choices cite the frozen OpenSpec candidate bucket. A
920
+ // `new` choice instead cites its task-source/PRD declaration and must not be
921
+ // rejected merely because that source is outside the OpenSpec roots.
882
922
  const componentSpecReferences = componentChoices
923
+ .filter((choice) => choice.decision === "specified")
883
924
  .map((choice) => ({
884
925
  raw: choice.specReference?.path ?? "",
885
926
  normalized: choice.specReference?.path
@@ -1024,6 +1065,9 @@ export function formatFrontendPrewriteGateStdout(result) {
1024
1065
  }
1025
1066
  lines.push(`Prewrite result: ${result.prewriteResult.path}`);
1026
1067
  lines.push(`Openspec policy: ${result.openspecPolicy}`);
1068
+ if (result.failureCode?.startsWith("source-fidelity-")) {
1069
+ lines.push(`Source fidelity ledger: blocked (${result.failureCode})`);
1070
+ }
1027
1071
  if (result.failureReason)
1028
1072
  lines.push(`Failure reason: ${result.failureReason}`);
1029
1073
  if (result.openspecReadPaths.length > 0) {
@@ -15,6 +15,7 @@ export function projectLegacyOperationState(state) {
15
15
  case "queued":
16
16
  case "starting":
17
17
  case "running":
18
+ case "restarting":
18
19
  return { operationState: "active" };
19
20
  case "succeeded":
20
21
  return { operationState: "success", operationTerminal: "success" };