@tea-agent/loop-agent 0.39.0-beta.8 → 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 (158) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +17 -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/update/runtime-activity.js +1 -29
  7. package/dist/commands/dag-rerun.js +1 -1
  8. package/dist/commands/init.js +26 -0
  9. package/dist/executors/dag-pi-executor.js +326 -8
  10. package/dist/executors/pi-read-budget-policy.js +166 -0
  11. package/dist/executors/shell-executor.js +26 -30
  12. package/dist/shared/dag-prompt-override.js +4 -5
  13. package/dist/shared/update/npm-client.js +33 -11
  14. package/dist/task/contract/apply.js +11 -1
  15. package/dist/task/contract/constants.js +2 -0
  16. package/dist/task/contract/hash.js +3 -0
  17. package/dist/task/contract/observe.js +25 -4
  18. package/dist/task/contract/paths.js +2 -1
  19. package/dist/task/contract/project.js +15 -1
  20. package/dist/task/contract/schema.js +2 -0
  21. package/dist/task/contract/transaction.js +12 -0
  22. package/dist/task/frontend-project-capability.js +123 -4
  23. package/dist/task/source-prepare/completeness.js +188 -0
  24. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  25. package/dist/task/source-prepare/index.js +3 -0
  26. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  27. package/dist/task/source-prepare/ledger-review.js +214 -0
  28. package/dist/task/source-prepare/ledger.js +545 -0
  29. package/dist/task/source-prepare/prepare.js +262 -1
  30. package/dist/task/source-prepare/semantic-intake.js +25 -5
  31. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  32. package/dist/task/source-prepare/types.js +22 -0
  33. package/dist/worker/cli.js +12 -26
  34. package/dist/worker/console/console-handoff.js +82 -0
  35. package/dist/worker/console/console-update-and-init.js +115 -0
  36. package/dist/worker/console/console-update-runtime.js +84 -0
  37. package/dist/worker/console/draft-store.js +49 -0
  38. package/dist/worker/console/operation-store.js +1 -1
  39. package/dist/worker/console/operator-actions.js +129 -4
  40. package/dist/worker/console/operator-surface-health.js +1 -0
  41. package/dist/worker/console/prd-intake-bridge.js +13 -0
  42. package/dist/worker/console/routes.js +3 -0
  43. package/dist/worker/console/server.js +73 -5
  44. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-bPer1waP.js → abnfDiagram-N423BO3Z-D1-Z-BBa.js} +1 -1
  45. package/dist/worker/console/static/assets/{arc-BLdkwucW.js → arc-q8qAYm5m.js} +1 -1
  46. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw4wo4I1.js → architectureDiagram-T3A2C74G-CAu4_lve.js} +1 -1
  47. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DUUkc0gr.js → blockDiagram-VBNYF7ZC-D5GhQvHm.js} +1 -1
  48. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BC3O0nob.js → c4Diagram-5PPSVZJV-ClQjeLkO.js} +1 -1
  49. package/dist/worker/console/static/assets/channel-ClAuhSUL.js +1 -0
  50. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Dd4g2nti.js → chunk-2GRJ4B5K-DLqWlFbo.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-s2EDm7-n.js → chunk-2Q5K7J3B-DkRT_usL.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-GqtHM2yQ.js → chunk-5RXB4S5H-Cqp7Chi3.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BzRFrN8j.js → chunk-5VM5RSS4-CedEdFB9.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bw9nFjCJ.js → chunk-6Q2QTUOP-BHM8vP5x.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B4n-03Qm.js → chunk-GF5L2VYU-D7EFr377.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Dt75ZdBD.js → chunk-JWPE2WC7-DR38mTWV.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B_afMhHB.js → chunk-KBJHAD2P-BFv2LsR3.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Bd8Rqqg9.js → chunk-RYQCIY6F-BAvpJS5z.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-9boOMji4.js → chunk-XXDRQBXY-eKI9TO72.js} +1 -1
  60. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-cBgeCpVU.js +1 -0
  61. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-cBgeCpVU.js +1 -0
  62. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BtCycfCc.js → cose-bilkent-JH36ORCC-BMJXpgo4.js} +1 -1
  63. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-eTOts9L1.js → cynefin-VYW2F7L2-CKJ2ofZx.js} +1 -1
  64. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cibd2mjg.js → cynefinDiagram-MW4NZA55-CsKnsazm.js} +1 -1
  65. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CDjDZtDV.js → dagre-VZM6K2ZE-DVQ46vIw.js} +1 -1
  66. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Btexn-JJ.js → diagram-7IWD3JNH-D_RuonYS.js} +1 -1
  67. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cyl8sdpM.js → diagram-B4RE2ZJO-D2Mt0nMu.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Ev-83E1t.js → diagram-LBJQPF4R-DjBDuye6.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-41LqE0Mc.js → diagram-Q27KOJAE-8VZkqpE_.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BC39T5R3.js → diagram-UB23O5K3-D0izYxia.js} +1 -1
  71. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DzcNRuri.js → ebnfDiagram-BXEA7PRR-CncG2I95.js} +1 -1
  72. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B5Frvvnz.js → erDiagram-JOGREHBK-DiBUUXR8.js} +1 -1
  73. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BapxROIM.js → flowDiagram-UKHOOZJN-Dtc0fArb.js} +1 -1
  74. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cd4d4wQ0.js → ganttDiagram-PKOTCBZU-BPNFP5OZ.js} +1 -1
  75. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CrL6buFW.js → gitGraphDiagram-DS77QQ5N-CsgpxEEd.js} +1 -1
  76. package/dist/worker/console/static/assets/{index-DBh_x_Ek.js → index-Bai8Z25q.js} +81 -81
  77. package/dist/worker/console/static/assets/index-CH0Tr5hY.css +1 -0
  78. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BrSiNxyd.js → infoDiagram-6WML65LV-kHGxkJUS.js} +1 -1
  79. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-gvlvY-n9.js → ishikawaDiagram-WSZJBQD7-CbLe9wrr.js} +1 -1
  80. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-YaPzRQ.js → journeyDiagram-NVQOT4AX-BOkwxAcP.js} +1 -1
  81. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BsmDv9E_.js → kanban-definition-27J2QSJJ-BHYQ2rDu.js} +1 -1
  82. package/dist/worker/console/static/assets/{linear-UWaoDN3h.js → linear-CHT4ujob.js} +1 -1
  83. package/dist/worker/console/static/assets/{mermaid.core-BlvpGVG8.js → mermaid.core-yL8sfqnY.js} +5 -5
  84. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CUCIP5CP.js → mindmap-definition-FAOFIHXS-DZx1coV-.js} +1 -1
  85. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CB1aQUAK.js → pegDiagram-VL7TDLO6-DStWRCI8.js} +1 -1
  86. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BwehHxhf.js → pieDiagram-7S7Q4E2Y-DSKZkwzK.js} +1 -1
  87. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CFz30APB.js → quadrantDiagram-CIZ2JOQS-CmoEGOuW.js} +1 -1
  88. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwNmw1i6.js → railroadDiagram-AXF67PYL-BLqzhKyq.js} +1 -1
  89. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-jg-tPXlI.js → requirementDiagram-LRYGKXZP-gbHeUabM.js} +1 -1
  90. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BlyReDKl.js → sankeyDiagram-W5VNT64P-CaKkBCKO.js} +1 -1
  91. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DBUzW1f3.js → sequenceDiagram-SI44F4Z6-PUbi3Upu.js} +1 -1
  92. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BYcI2RAr.js → sizeCapture-X5ZJPWSS-dm7S9Qfe.js} +1 -1
  93. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D7AiyJdo.js → stateDiagram-OKZ733FA-C7WSzv4p.js} +1 -1
  94. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-sQDv5AP3.js +1 -0
  95. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BHGt7Kkv.js → swimlanes-SLNWSIFB-CBKCiQwd.js} +2 -2
  96. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BXwsmcPs.js +8 -0
  97. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-OPjooWcX.js → timeline-definition-Z64GVDOM-CvnUlylk.js} +1 -1
  98. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-ggsWDvrO.js → vennDiagram-T6HMQDX7-xM0StY1O.js} +1 -1
  99. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DjNe-BjZ.js → wardleyDiagram-T6FBY63Y-CWtJMagy.js} +1 -1
  100. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Byp63Hei.js → xychartDiagram-ELKLHX3M-BO_-MeK8.js} +1 -1
  101. package/dist/worker/console/static/index.html +2 -2
  102. package/dist/worker/console/static-src/app/console-types.js +40 -1
  103. package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
  104. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  105. package/dist/worker/observability/init-runtime-activity.js +26 -0
  106. package/dist/worker/observe/node-input.js +88 -2
  107. package/dist/worker/observe/routes.js +188 -0
  108. package/dist/worker/observe/static/api.js +3 -3
  109. package/dist/worker/observe/static/format.js +37 -0
  110. package/dist/worker/observe/static/index.html +5 -0
  111. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  112. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  113. package/dist/worker/observe/static/state.js +2 -2
  114. package/dist/worker/observe/static/styles.css +331 -35
  115. package/dist/worker/observe/static/views/dag-inspector.js +509 -78
  116. package/dist/worker/observe/static/views/task.js +55 -0
  117. package/dist/workflows/dag/frontend-design-policy.js +19 -9
  118. package/dist/workflows/dag/frontend-implementation-contract.js +109 -49
  119. package/dist/workflows/dag/frontend-plan-render.js +27 -0
  120. package/dist/workflows/dag/frontend-prewrite-gate.js +45 -1
  121. package/dist/workflows/dag/frontend-recovery-controller.js +1 -0
  122. package/dist/workflows/dag/frontend-review-context.js +102 -15
  123. package/dist/workflows/dag/frontend-shadow-dual-write.js +36 -2
  124. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  125. package/dist/workflows/dag/frontend-worktree-diff.js +154 -0
  126. package/dist/workflows/dag/frontend-writer-admission.js +12 -0
  127. package/dist/workflows/dag/init-hybrid.js +293 -65
  128. package/dist/workflows/dag/interrupt-request.js +7 -0
  129. package/dist/workflows/dag/node-execution.js +111 -1
  130. package/dist/workflows/dag/prompt.js +34 -1
  131. package/dist/workflows/dag/recovery-lease.js +80 -0
  132. package/dist/workflows/dag/rerun-run.js +23 -0
  133. package/dist/workflows/dag/rerun-task.js +29 -0
  134. package/dist/workflows/dag/retry-policy.js +26 -3
  135. package/dist/workflows/dag/runner.js +38 -8
  136. package/dist/workflows/dag/types.js +51 -11
  137. package/docs/README.md +1 -0
  138. package/docs/architecture/evolution.md +45 -0
  139. package/docs/init-surface.manifest.json +4 -0
  140. package/docs/templates/README.md +2 -1
  141. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  142. package/docs/templates/spec-registry.schema.json +45 -0
  143. package/harness.json +3 -3
  144. package/package.json +1 -1
  145. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  146. package/skills/frontend-design-review/SKILL.md +20 -41
  147. package/skills/frontend-implementation/SKILL.md +2 -3
  148. package/skills/frontend-implementation/references/design-spec.md +4 -3
  149. package/skills/frontend-implementation/references/node-contracts.md +4 -3
  150. package/skills/frontend-review/SKILL.md +22 -14
  151. package/skills/frontend-review/references/review-findings.md +6 -7
  152. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  153. package/dist/worker/console/static/assets/channel-AbFtVnFm.js +0 -1
  154. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +0 -1
  155. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +0 -1
  156. package/dist/worker/console/static/assets/index-DwOGauxU.css +0 -1
  157. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +0 -1
  158. 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,6 +13,7 @@ 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";
17
18
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
18
19
  import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
@@ -287,13 +288,13 @@ const gap = z
287
288
  * `specReference` is required (path/section/line hit).
288
289
  * - `reuse-existing`: reuse an existing repo component/convention not named by
289
290
  * the spec → `specReference` may be null, rationale explains the basis.
290
- * - `new`: neither spec nor existing code fits `specReference` must be null,
291
- * 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.
292
294
  *
293
- * `specReference` is nullable + optional because `stripNullValuesDeep` strips
294
- * null object values before validation, so a `new` choice's `specReference:
295
- * null` becomes an absent field; the superRefine below treats absent and null
296
- * 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.
297
298
  */
298
299
  const uiComponentSpecReferenceSchema = z
299
300
  .object({
@@ -309,7 +310,8 @@ const uiComponentChoiceSchema = z
309
310
  decision: z.enum(["specified", "reuse-existing", "new"]),
310
311
  specReference: uiComponentSpecReferenceSchema.nullable().optional(),
311
312
  // Optional: the decision enum already carries the semantic (specified →
312
- // specReference, new → deviation, reuse-existing → no rationale needed).
313
+ // specReference, new → task-source traceability, reuse-existing → no
314
+ // rationale needed).
313
315
  // Omit for reuse-existing to keep plan output small.
314
316
  rationale: z.string().min(1).optional(),
315
317
  })
@@ -339,49 +341,12 @@ function stripNullValuesDeep(value) {
339
341
  }
340
342
  return value;
341
343
  }
342
- /**
343
- * Normalize a model-emitted `uiComponentChoices` where a `new` choice carries a
344
- * degenerate empty `specReference` object (`{ path: "", section: "", line:
345
- * null }`) instead of the required `null`. The model intent is unambiguous
346
- * (new components must not carry a specReference; the empty object is a format
347
- * slip, not a real spec hit), so dropping the empty object before validation
348
- * avoids a full plan retry on an otherwise-correct emission. A `specReference`
349
- * with a non-empty `path` is preserved (it is a genuine `specified` hit).
350
- */
351
- export function normalizeNewComponentSpecReference(value) {
352
- if (!Array.isArray(value))
353
- return value;
354
- return value.map((item) => {
355
- if (!item || typeof item !== "object")
356
- return item;
357
- const record = item;
358
- if (record.decision !== "new")
359
- return item;
360
- const ref = record.specReference;
361
- if (!ref || typeof ref !== "object")
362
- return item;
363
- const refRecord = ref;
364
- const path = typeof refRecord.path === "string" ? refRecord.path.trim() : "";
365
- if (path.length > 0)
366
- return item;
367
- // Empty/degenerate specReference on a new choice → absent (null intent).
368
- const { specReference: _dropped, ...rest } = record;
369
- return rest;
370
- });
371
- }
372
344
  export const frontendImplementationContractSchema = z
373
345
  .preprocess((value) => {
374
346
  const record = asRecord(value);
375
347
  if (!record)
376
348
  return stripNullValuesDeep(value);
377
- return stripNullValuesDeep({
378
- ...record,
379
- ...(record.uiComponentChoices !== undefined
380
- ? {
381
- uiComponentChoices: normalizeNewComponentSpecReference(record.uiComponentChoices),
382
- }
383
- : {}),
384
- });
349
+ return stripNullValuesDeep(record);
385
350
  }, z
386
351
  .object({
387
352
  schemaVersion: z.literal(1),
@@ -392,8 +357,33 @@ export const frontendImplementationContractSchema = z
392
357
  requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
393
358
  referencePaths: z.array(safePath),
394
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(),
395
367
  })
396
- .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
+ }),
397
387
  riskLevel: z.enum(["small", "standard", "high-risk"]),
398
388
  targets: z
399
389
  .object({
@@ -409,6 +399,10 @@ export const frontendImplementationContractSchema = z
409
399
  implementationTargets: z.array(safePath),
410
400
  verificationTargetIds: z.array(z.string().min(1)),
411
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(),
412
406
  })
413
407
  .strict()).min(1),
414
408
  uiStates: z.array(z
@@ -545,6 +539,32 @@ export const frontendImplementationContractSchema = z
545
539
  message: `unknown verification target ${targetId}`,
546
540
  path: ["requirements"],
547
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
+ }
548
568
  }
549
569
  for (const state of value.uiStates) {
550
570
  if (state.applicable &&
@@ -574,10 +594,10 @@ export const frontendImplementationContractSchema = z
574
594
  }
575
595
  }
576
596
  else if (choice.decision === "new") {
577
- if (choice.specReference) {
597
+ if (!choice.specReference || !choice.specReference.path) {
578
598
  ctx.addIssue({
579
599
  code: "custom",
580
- message: "new component choice must not carry a specReference",
600
+ message: "new component choice requires a task-source specReference.path",
581
601
  path: ["uiComponentChoices", index, "specReference"],
582
602
  });
583
603
  }
@@ -681,6 +701,29 @@ export async function assertFrontendSourceBindingFresh(input) {
681
701
  throw new Error(`frontend source binding is stale: ${source.path}`);
682
702
  }
683
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
+ }
684
727
  }
685
728
  const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
686
729
  const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
@@ -855,7 +898,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
855
898
  const requirement = binding.sources.find((source) => source.kind === "requirement");
856
899
  if (!requirement)
857
900
  throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
858
- return {
901
+ const sourceBinding = {
859
902
  taskId: binding.taskId,
860
903
  requirementPath: requirement.path,
861
904
  requirementSha256: requirement.sha256,
@@ -865,6 +908,15 @@ export function canonicalFrontendContractSourceBinding(binding) {
865
908
  .sort(),
866
909
  requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
867
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;
868
920
  }
869
921
  function asRecord(value) {
870
922
  return value && typeof value === "object" && !Array.isArray(value)
@@ -1437,6 +1489,14 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1437
1489
  verificationTargetIds: verificationTargetIds.length > 0
1438
1490
  ? verificationTargetIds
1439
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 } : {}),
1440
1500
  };
1441
1501
  // Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
1442
1502
  if (gapText) {
@@ -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" };