@tea-agent/loop-agent 0.39.0 → 0.39.1

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 (135) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +9 -2
  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/executors/shell-executor.js +1 -1
  9. package/dist/shared/dag-prompt-override.js +4 -5
  10. package/dist/shared/update/npm-client.js +33 -11
  11. package/dist/task/contract/apply.js +11 -1
  12. package/dist/task/contract/constants.js +2 -0
  13. package/dist/task/contract/hash.js +3 -0
  14. package/dist/task/contract/observe.js +25 -4
  15. package/dist/task/contract/paths.js +2 -1
  16. package/dist/task/contract/project.js +15 -1
  17. package/dist/task/contract/schema.js +2 -0
  18. package/dist/task/contract/transaction.js +12 -0
  19. package/dist/task/source-prepare/completeness.js +188 -0
  20. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  21. package/dist/task/source-prepare/index.js +3 -0
  22. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  23. package/dist/task/source-prepare/ledger-review.js +214 -0
  24. package/dist/task/source-prepare/ledger.js +545 -0
  25. package/dist/task/source-prepare/prepare.js +262 -1
  26. package/dist/task/source-prepare/semantic-intake.js +19 -3
  27. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  28. package/dist/task/source-prepare/types.js +22 -0
  29. package/dist/worker/cli.js +12 -26
  30. package/dist/worker/console/console-handoff.js +82 -0
  31. package/dist/worker/console/console-update-and-init.js +115 -0
  32. package/dist/worker/console/console-update-runtime.js +84 -0
  33. package/dist/worker/console/draft-store.js +49 -0
  34. package/dist/worker/console/operation-store.js +1 -1
  35. package/dist/worker/console/operator-actions.js +129 -4
  36. package/dist/worker/console/operator-surface-health.js +1 -0
  37. package/dist/worker/console/prd-intake-bridge.js +13 -0
  38. package/dist/worker/console/routes.js +3 -0
  39. package/dist/worker/console/server.js +73 -5
  40. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
  41. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-B4i9cVxX.js} +1 -1
  42. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
  43. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
  44. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
  45. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
  46. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
  56. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
  57. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
  58. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
  59. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
  60. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
  61. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
  62. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
  63. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
  64. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
  65. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
  66. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
  67. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
  68. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
  69. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
  70. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
  71. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
  72. package/dist/worker/console/static/assets/{index-Ci8ksvBm.js → index-CW88Fjbw.js} +81 -81
  73. package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
  74. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
  75. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
  76. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
  77. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
  78. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-aGmcjgTn.js} +1 -1
  79. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-Biyo-tqA.js} +5 -5
  80. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
  81. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
  82. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
  83. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
  84. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
  85. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
  86. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
  87. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
  88. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
  89. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
  90. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
  91. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
  92. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
  93. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
  94. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
  95. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
  96. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
  97. package/dist/worker/console/static/index.html +2 -2
  98. package/dist/worker/console/static-src/app/console-types.js +40 -1
  99. package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
  100. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  101. package/dist/worker/observability/init-runtime-activity.js +26 -0
  102. package/dist/worker/observe/node-input.js +88 -2
  103. package/dist/worker/observe/routes.js +188 -0
  104. package/dist/worker/observe/static/api.js +3 -3
  105. package/dist/worker/observe/static/format.js +37 -0
  106. package/dist/worker/observe/static/index.html +5 -0
  107. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  108. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  109. package/dist/worker/observe/static/styles.css +12 -0
  110. package/dist/worker/observe/static/views/dag-inspector.js +316 -16
  111. package/dist/worker/observe/static/views/task.js +55 -0
  112. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  113. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  114. package/dist/workflows/dag/backend-test-pytest-collection.js +87 -4
  115. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  116. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  117. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  119. package/dist/workflows/dag/frontend-review-context.js +92 -10
  120. package/dist/workflows/dag/init-hybrid.js +125 -4
  121. package/dist/workflows/dag/node-execution.js +45 -0
  122. package/dist/workflows/dag/types.js +42 -11
  123. package/docs/README.md +1 -0
  124. package/docs/architecture/evolution.md +45 -0
  125. package/docs/init-surface.manifest.json +1 -0
  126. package/docs/templates/README.md +1 -1
  127. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  128. package/package.json +1 -1
  129. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  130. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  131. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  132. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  133. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  135. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
@@ -2,9 +2,12 @@ import { createHash } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
+ import { isDagSourceBindingV2 } from "./types.js";
5
6
  import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
6
7
  import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
7
8
  import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
9
+ import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
10
+ import { parseLedgerJson, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
8
11
  import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
9
12
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
10
13
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
@@ -42,6 +45,14 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
42
45
  "component-choices-missing",
43
46
  "contract-too-large",
44
47
  "revision-patch-unmergeable",
48
+ // Source fidelity ledger 类目(SOURCE_FIDELITY_*/source-* 稳定错误码)。
49
+ "source-fidelity-ledger-missing",
50
+ "source-fidelity-ledger-stale",
51
+ "source-fidelity-unresolved",
52
+ "source-fidelity-conflict",
53
+ "source-fidelity-high-risk-uncovered",
54
+ "source-fidelity-requirement-not-covered",
55
+ "source-fidelity-source-ref-invalid",
45
56
  ]);
46
57
  export const frontendPrewriteResultV1Schema = z
47
58
  .object({
@@ -442,6 +453,162 @@ async function finalizePrewrite(input, pending) {
442
453
  openspecPolicy: pending.openspecPolicy,
443
454
  };
444
455
  }
456
+ /**
457
+ * Source fidelity ledger 门禁检查(AC-FIDELITY-002/003/006):绑定携带 ledger 指针时,
458
+ * 依次核验 ledger 存在、绑定 hash 一致、inputDigest 新鲜、无 unresolved/conflict/
459
+ * 高风险未覆盖、canonical requirement 全覆盖、来源引用有效且非伪造。任一失败 fail closed。
460
+ */
461
+ export async function checkFrontendSourceFidelityLedger(input) {
462
+ const { binding } = input;
463
+ // v1 绑定不携带 ledger 指针:非 frontend task / 无可解析引用 → 无 v2 校验路径。
464
+ if (!isDagSourceBindingV2(binding)) {
465
+ return { ok: true, failureCode: null, failureReason: null, ledger: null };
466
+ }
467
+ const absolute = resolveDagTaskSourcePath({
468
+ workspaceRoot: input.workspaceRoot,
469
+ taskId: binding.taskId,
470
+ sourcePath: binding.ledgerPath,
471
+ });
472
+ let raw;
473
+ try {
474
+ raw = await readFile(absolute, "utf8");
475
+ }
476
+ catch (error) {
477
+ return {
478
+ ok: false,
479
+ failureCode: "source-fidelity-ledger-missing",
480
+ failureReason: `frontend source fidelity ledger missing: ${binding.ledgerPath} (${error instanceof Error ? error.message : String(error)})`,
481
+ ledger: null,
482
+ };
483
+ }
484
+ const actualSha = createHash("sha256").update(raw).digest("hex");
485
+ if (actualSha !== binding.ledgerSha256) {
486
+ return {
487
+ ok: false,
488
+ failureCode: "source-fidelity-ledger-stale",
489
+ failureReason: `frontend source fidelity ledger stale or tampered: ${binding.ledgerPath} (bound ${binding.ledgerSha256}, observed ${actualSha})`,
490
+ ledger: null,
491
+ };
492
+ }
493
+ let ledger;
494
+ try {
495
+ ledger = parseLedgerJson(raw);
496
+ }
497
+ catch (error) {
498
+ return {
499
+ ok: false,
500
+ failureCode: "source-fidelity-ledger-stale",
501
+ failureReason: `frontend source fidelity ledger is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`,
502
+ ledger: null,
503
+ };
504
+ }
505
+ if (ledger.inputDigest !== binding.inputDigest) {
506
+ return {
507
+ ok: false,
508
+ failureCode: "source-fidelity-ledger-stale",
509
+ failureReason: `frontend source fidelity ledger inputDigest mismatch: bound ${binding.inputDigest}, ledger ${ledger.inputDigest}`,
510
+ ledger,
511
+ };
512
+ }
513
+ const validation = validateRequirementLedger(ledger);
514
+ const firstError = (code) => validation.find((error) => error.code === code)?.message ?? null;
515
+ if (ledger.stats.unresolvedCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_UNRESOLVED")) {
516
+ return {
517
+ ok: false,
518
+ failureCode: "source-fidelity-unresolved",
519
+ failureReason: firstError("SOURCE_FIDELITY_UNRESOLVED") ??
520
+ `frontend source fidelity ledger has ${ledger.stats.unresolvedCount} unresolved fragment(s)`,
521
+ ledger,
522
+ };
523
+ }
524
+ if (ledger.stats.conflictCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_CONFLICT")) {
525
+ return {
526
+ ok: false,
527
+ failureCode: "source-fidelity-conflict",
528
+ failureReason: firstError("SOURCE_FIDELITY_CONFLICT") ??
529
+ `frontend source fidelity ledger has ${ledger.stats.conflictCount} conflict(s)`,
530
+ ledger,
531
+ };
532
+ }
533
+ if (ledger.stats.highRiskUncoveredCount > 0 ||
534
+ validation.some((e) => e.code === "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED")) {
535
+ return {
536
+ ok: false,
537
+ failureCode: "source-fidelity-high-risk-uncovered",
538
+ failureReason: firstError("SOURCE_FIDELITY_HIGH_RISK_UNCOVERED") ??
539
+ `frontend source fidelity ledger has ${ledger.stats.highRiskUncoveredCount} high-risk uncovered fragment(s)`,
540
+ ledger,
541
+ };
542
+ }
543
+ // AC-BLOCK-001:canonical requirement 缺少 fragment/sourceRef 证据(ledger 侧
544
+ // REQUIREMENT_NOT_COVERED)与来源引用/片段无效(SOURCE_REF_INVALID /
545
+ // FRAGMENT_INVALID)均为阻断性失败,结构化失败结果不得产生可继续执行的成功。
546
+ const notCoveredError = validation.find((e) => e.code === "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED");
547
+ if (notCoveredError) {
548
+ return {
549
+ ok: false,
550
+ failureCode: "source-fidelity-requirement-not-covered",
551
+ failureReason: notCoveredError.message,
552
+ ledger,
553
+ };
554
+ }
555
+ const refInvalidError = validation.find((e) => e.code === "SOURCE_FIDELITY_SOURCE_REF_INVALID" ||
556
+ e.code === "SOURCE_FIDELITY_FRAGMENT_INVALID");
557
+ if (refInvalidError) {
558
+ return {
559
+ ok: false,
560
+ failureCode: "source-fidelity-source-ref-invalid",
561
+ failureReason: refInvalidError.message,
562
+ ledger,
563
+ };
564
+ }
565
+ const ledgerIds = new Set(ledger.canonicalRequirements.map((requirement) => requirement.id));
566
+ const contractIds = new Set(input.contractRequirements.map((r) => r.id));
567
+ const uncoveredRequirements = [...ledgerIds].filter((id) => !contractIds.has(id));
568
+ if (uncoveredRequirements.length > 0) {
569
+ return {
570
+ ok: false,
571
+ failureCode: "source-fidelity-requirement-not-covered",
572
+ failureReason: `frontend contract requirements do not cover ledger canonical requirement ids: ${uncoveredRequirements.join(", ")}`,
573
+ ledger,
574
+ };
575
+ }
576
+ const fragmentById = new Map(ledger.fragments.map((fragment) => [fragment.id, fragment]));
577
+ for (const requirement of input.contractRequirements) {
578
+ if (!ledgerIds.has(requirement.id))
579
+ continue;
580
+ const declared = requirement.sourceFragmentIds ?? [];
581
+ if (declared.length === 0) {
582
+ return {
583
+ ok: false,
584
+ failureCode: "source-fidelity-source-ref-invalid",
585
+ failureReason: `frontend contract requirement ${requirement.id} lacks sourceFragmentIds for the bound ledger`,
586
+ ledger,
587
+ };
588
+ }
589
+ const boundFragments = new Set(ledger.canonicalRequirements.find((r) => r.id === requirement.id)
590
+ ?.sourceFragmentIds ?? []);
591
+ for (const fragmentId of declared) {
592
+ if (!fragmentById.has(fragmentId)) {
593
+ return {
594
+ ok: false,
595
+ failureCode: "source-fidelity-source-ref-invalid",
596
+ failureReason: `frontend contract requirement ${requirement.id} references unknown fragment ${fragmentId}`,
597
+ ledger,
598
+ };
599
+ }
600
+ if (!boundFragments.has(fragmentId)) {
601
+ return {
602
+ ok: false,
603
+ failureCode: "source-fidelity-source-ref-invalid",
604
+ failureReason: `frontend contract requirement ${requirement.id} declares fabricated fragment reference ${fragmentId} (not bound in ledger)`,
605
+ ledger,
606
+ };
607
+ }
608
+ }
609
+ }
610
+ return { ok: true, failureCode: null, failureReason: null, ledger };
611
+ }
445
612
  export async function runFrontendPrewriteGate(input) {
446
613
  const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
447
614
  // selectNode throws for a scheduling defect (no FINISHED plan/review). That
@@ -589,6 +756,33 @@ export async function runFrontendPrewriteGate(input) {
589
756
  }
590
757
  throw error;
591
758
  }
759
+ // Source fidelity ledger gate (20260824-frontend-source-fidelity-ledger-v1):
760
+ // 在 verdict/missing-requirement-ids/source-binding-not-fresh 之后插入 ledger
761
+ // 检查。缺失即 fail closed 且 writer 不获授权;所有检查通过才继续物化契约。
762
+ if (input.sourceBinding) {
763
+ const ledgerCheck = await checkFrontendSourceFidelityLedger({
764
+ workspaceRoot: workspaceRoot ?? "",
765
+ binding: input.sourceBinding,
766
+ contractRequirements: analysis.canonical.requirements.map((requirement) => ({
767
+ id: requirement.id,
768
+ sourceFragmentIds: requirement.sourceFragmentIds,
769
+ sourceRefs: requirement.sourceRefs,
770
+ })),
771
+ });
772
+ if (!ledgerCheck.ok && ledgerCheck.failureCode) {
773
+ return finalizePrewrite(input, {
774
+ ...basePending,
775
+ verdict,
776
+ candidateJsonSha256: analysis.candidateJsonSha256,
777
+ normalizationActions: [],
778
+ mockStrategy: analysis.canonical.mockApi.strategy,
779
+ classification: "blocked",
780
+ failureReason: ledgerCheck.failureReason ??
781
+ "frontend source fidelity ledger gate failed",
782
+ failureCode: ledgerCheck.failureCode,
783
+ });
784
+ }
785
+ }
592
786
  // Size guard: a canonical contract above 64KB is a retryable-invalid defect
593
787
  // (compress the contract) rather than a writer authorization failure.
594
788
  const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
@@ -947,6 +1141,9 @@ export function formatFrontendPrewriteGateStdout(result) {
947
1141
  }
948
1142
  lines.push(`Prewrite result: ${result.prewriteResult.path}`);
949
1143
  lines.push(`Openspec policy: ${result.openspecPolicy}`);
1144
+ if (result.failureCode?.startsWith("source-fidelity-")) {
1145
+ lines.push(`Source fidelity ledger: blocked (${result.failureCode})`);
1146
+ }
950
1147
  if (result.failureReason)
951
1148
  lines.push(`Failure reason: ${result.failureReason}`);
952
1149
  if (result.openspecReadPaths.length > 0) {
@@ -1,17 +1,39 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
4
5
  import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "./frontend-worktree-diff.js";
5
6
  import { frontendLintAssessmentArtifactSchema } from "./frontend-lint-baseline.js";
6
- import { frontendImplementationContractSchema } from "./frontend-implementation-contract.js";
7
+ import { frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
7
8
  import { frontendRepairAssessmentSchema } from "./frontend-repair.js";
9
+ import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
10
+ import { ledgerStatsLine, parseLedgerJson, } from "../../task/source-prepare/ledger.js";
8
11
  export const FRONTEND_REVIEW_CONTEXT_SCHEMA_ID = "frontend-review-context-v1";
12
+ /**
13
+ * review-context 稳定失败枚举(AC-011 / AC-HARD-005):v2 drift / manifest 失败返回
14
+ * 结构化、可消费的 failure code(稳定枚举 + reason 的 envelope),既有调用方无需字符串
15
+ * 匹配裸 Error 文本。message 保持人类可读(shell stderr 兼容),code 供机械化路由。
16
+ */
17
+ export const FRONTEND_REVIEW_CONTEXT_FAILURE_CODES = [
18
+ "review-context-invalid-contract",
19
+ "review-context-verification-unbound",
20
+ "review-context-ledger-drift",
21
+ "review-context-manifest-missing",
22
+ ];
23
+ export class FrontendReviewContextFailure extends Error {
24
+ code;
25
+ constructor(code, message, options) {
26
+ super(message, options);
27
+ this.name = "FrontendReviewContextFailure";
28
+ this.code = code;
29
+ }
30
+ }
9
31
  async function readRequiredJson(runDir, relativePath) {
10
32
  try {
11
33
  return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
12
34
  }
13
- catch {
14
- throw new Error(`frontend review context missing or invalid ${relativePath}`);
35
+ catch (error) {
36
+ throw new FrontendReviewContextFailure("review-context-manifest-missing", `frontend review context missing or invalid ${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
15
37
  }
16
38
  }
17
39
  async function readOptionalLintAssessment(runDir) {
@@ -32,8 +54,9 @@ async function readOptionalLintAssessment(runDir) {
32
54
  }
33
55
  function assertReviewEvidence(input) {
34
56
  const contract = frontendImplementationContractSchema.safeParse(input.contract);
35
- if (!contract.success)
36
- throw new Error("frontend review context invalid implementation contract");
57
+ if (!contract.success) {
58
+ throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid implementation contract");
59
+ }
37
60
  const trace = input.verificationTrace;
38
61
  const expectedTargets = new Map(contract.data.verificationTargets.map((target) => [target.id, target]));
39
62
  const actualTargets = Array.isArray(trace?.targets) ? trace.targets : [];
@@ -69,17 +92,19 @@ function assertReviewEvidence(input) {
69
92
  actualIds.size !== expectedTargets.size ||
70
93
  !mockEvidenceBound ||
71
94
  trace.mockStrategy !== contract.data.mockApi.strategy) {
72
- throw new Error("frontend review context verification trace is not bound to a passing contract");
95
+ throw new FrontendReviewContextFailure("review-context-verification-unbound", "frontend review context verification trace is not bound to a passing contract");
73
96
  }
74
97
  const assessment = frontendRepairAssessmentSchema.safeParse(input.repairAssessment);
75
- if (!assessment.success)
76
- throw new Error("frontend review context invalid repair assessment");
98
+ if (!assessment.success) {
99
+ throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid repair assessment");
100
+ }
77
101
  }
78
102
  export async function runFrontendReviewContextGate(input) {
79
103
  const contract = await readRequiredJson(input.runDir, "contracts/frontend-implementation-contract.json");
80
104
  const parsedContract = frontendImplementationContractSchema.safeParse(contract);
81
- if (!parsedContract.success)
82
- throw new Error("frontend review context invalid implementation contract");
105
+ if (!parsedContract.success) {
106
+ throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid implementation contract");
107
+ }
83
108
  const diff = await runFrontendWorktreeDiffGate({
84
109
  ...input,
85
110
  authorizedChangedPaths: parsedContract.data.targets.files,
@@ -88,6 +113,9 @@ export async function runFrontendReviewContextGate(input) {
88
113
  const repairAssessment = await readRequiredJson(input.runDir, "contracts/frontend-repair-assessment.json");
89
114
  const lintAssessment = await readOptionalLintAssessment(input.runDir);
90
115
  assertReviewEvidence({ contract, verificationTrace, repairAssessment });
116
+ // Source fidelity ledger 核对(AC-008 / AC-FIDELITY-008):ledger 缺失/漂移即阻断;
117
+ // 同时把核对清单注入 review-context 供最终 reviewer 机械化核对。
118
+ const sourceFidelity = await buildReviewSourceFidelityChecklist(input, parsedContract.data);
91
119
  const payload = {
92
120
  schemaVersion: 1,
93
121
  schemaId: FRONTEND_REVIEW_CONTEXT_SCHEMA_ID,
@@ -95,6 +123,7 @@ export async function runFrontendReviewContextGate(input) {
95
123
  verificationTrace,
96
124
  repairAssessment,
97
125
  ...(lintAssessment ? { lintAssessment } : {}),
126
+ sourceFidelity,
98
127
  diff: {
99
128
  schemaId: diff.schemaId,
100
129
  patchPath: diff.patchPath,
@@ -107,6 +136,59 @@ export async function runFrontendReviewContextGate(input) {
107
136
  const artifactPath = await writeDagRunJsonArtifact(input.runDir, "contracts/frontend-review-context.json", payload);
108
137
  return { ok: true, artifactPath, diff };
109
138
  }
139
+ /**
140
+ * 最终 review 核对清单(机械化):ledger 缺失/漂移、canonical requirement 漏项、
141
+ * diff 越界、验证证据缺失均阻断 pass。清单以有界安全摘要注入提示词,不泄漏原文。
142
+ */
143
+ async function buildReviewSourceFidelityChecklist(input, contract) {
144
+ const binding = contract.sourceBinding;
145
+ const checklist = [];
146
+ // 契约级 sourceBinding 的 v2 语义:四个 ledger 字段必须齐全(缺失即 v1 遗留绑定)。
147
+ const boundV2 = Boolean(binding.ledgerPath) &&
148
+ Boolean(binding.ledgerSha256) &&
149
+ Boolean(binding.inputDigest) &&
150
+ binding.requirementToFragments !== undefined;
151
+ if (!boundV2) {
152
+ return {
153
+ bound: false,
154
+ ledger: null,
155
+ checklist: [
156
+ "source fidelity ledger: not bound (v1 binding); 按现有语义评审",
157
+ "canonical contract 与实际 diff 逐项核对,验证证据齐全后 verdict 可为 pass",
158
+ ],
159
+ };
160
+ }
161
+ const absolute = resolveDagTaskSourcePath({
162
+ workspaceRoot: input.workspaceRoot,
163
+ taskId: contract.sourceBinding.taskId,
164
+ sourcePath: binding.ledgerPath,
165
+ });
166
+ let ledger = null;
167
+ try {
168
+ const raw = await readFile(absolute, "utf8");
169
+ const actualSha = createHash("sha256").update(raw).digest("hex");
170
+ if (actualSha !== binding.ledgerSha256) {
171
+ throw new Error(`ledger sha256 mismatch (bound ${binding.ledgerSha256}, observed ${actualSha})`);
172
+ }
173
+ ledger = parseLedgerJson(raw);
174
+ }
175
+ catch (error) {
176
+ // AC-011 / AC-HARD-005:v2 drift 返回结构化 failure code(稳定枚举 + reason),
177
+ // 不再裸 throw new Error;message 保持人类可读供 shell stderr。
178
+ throw new FrontendReviewContextFailure("review-context-ledger-drift", `frontend review context source fidelity ledger unavailable or drifted: ${error instanceof Error ? error.message : String(error)}`);
179
+ }
180
+ checklist.push("ledger: 存在且绑定 sha256 一致(无缺失/漂移)", `ledger 覆盖统计: ${ledgerStatsLine(ledger)}`, "canonical contract requirements 必须覆盖 ledger 全部 canonical requirement IDs", "实际 diff 必须限制在契约 targets 内(越界即阻断 pass)", "验证证据必须绑定契约 verificationTargets(缺失即阻断 pass)", "仅满足派生摘要而遗漏 ledger requirement 时 verdict 不得为 pass");
181
+ return {
182
+ bound: true,
183
+ ledger: {
184
+ path: binding.ledgerPath,
185
+ sha256: binding.ledgerSha256,
186
+ inputDigest: binding.inputDigest,
187
+ stats: ledgerStatsLine(ledger),
188
+ },
189
+ checklist,
190
+ };
191
+ }
110
192
  export function formatFrontendReviewContextStdout(result) {
111
193
  return [
112
194
  "Frontend review context: pass",
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { access, readdir, readFile, realpath } from "node:fs/promises";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
6
6
  import { assertValidDagSpec } from "./validate.js";
@@ -22,6 +22,9 @@ import { discoverProjectGovernancePresence } from "./project-governance-context.
22
22
  import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
23
23
  import { materializeTaskReferenceDocs } from "../../task/source-references.js";
24
24
  import { observeTaskContract } from "../../task/contract/observe.js";
25
+ import { extractRequirementFactsFromMarkdown } from "../../task/source-prepare/parse-intent.js";
26
+ import { computeLedgerInputDigest, parseLedgerJson, recoverLedgerInputContract, } from "../../task/source-prepare/ledger.js";
27
+ import { REQUIREMENT_LEDGER_FILE_NAME } from "../../task/contract/constants.js";
25
28
  import { dagHasWriterExecution } from "./task-contract-binding.js";
26
29
  import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
27
30
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
@@ -1477,7 +1480,7 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
1477
1480
  .sort((a, b) => a - b);
1478
1481
  return numbered.map((value) => `AC-${value}`);
1479
1482
  }
1480
- function buildDagSourceBinding(sources) {
1483
+ function buildDagSourceBindingBase(sources) {
1481
1484
  const sourceEntries = [
1482
1485
  {
1483
1486
  kind: "requirement",
@@ -1512,6 +1515,124 @@ function buildDagSourceBinding(sources) {
1512
1515
  requirementIds: extractTaskScopedRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
1513
1516
  };
1514
1517
  }
1518
+ /**
1519
+ * Source binding v2(AC-001 / AC-FIDELITY-REPAIR-001):仅 frontend taskKind 且存在
1520
+ * 可解析 requirement/acceptance 引用时产出 schemaVersion=2 绑定(ledger 指针 +
1521
+ * requirement→fragment 映射全部必填)。绑定完全来自 task advance 已持久化的
1522
+ * source/requirement-ledger.json(intake 侧同一份受管账本的原始字节/hash/inputDigest),
1523
+ * 绝不从 materialized reference path 重建语义等价但 path/hash 不同的 ledger;
1524
+ * 缺失/损坏/漂移 fail closed(SOURCE_FIDELITY_LEDGER_MISSING / _STALE)。
1525
+ */
1526
+ function buildDagSourceBinding(sources, taskKind) {
1527
+ const base = buildDagSourceBindingBase(sources);
1528
+ if (taskKind !== "frontend-implementation")
1529
+ return base;
1530
+ const ledgerBinding = loadPersistedFrontendSourceBindingLedger(sources);
1531
+ if (!ledgerBinding)
1532
+ return base;
1533
+ // AC-002 / AC-HARD-002:物化 REQ-SRC-* canonical id 确定性并入 v2 requirementIds。
1534
+ // 生成型 REQ-SRC-* id 无法从 markdown 文本导出(必须来自持久化 ledger),此处
1535
+ // 在 required requirement ids 侧确定性注入,确保 prewrite missing-requirement-ids
1536
+ // 不误伤(不要求文本可导出)也不漏检(物化 id 必须被契约覆盖)。
1537
+ const materializedRequirementIds = Object.keys(ledgerBinding.requirementToFragments)
1538
+ .filter((id) => /^REQ-SRC-/.test(id) && !base.requirementIds.includes(id))
1539
+ .sort();
1540
+ return {
1541
+ schemaVersion: 2,
1542
+ taskId: base.taskId,
1543
+ sources: base.sources,
1544
+ requirementIds: [
1545
+ ...base.requirementIds,
1546
+ ...materializedRequirementIds,
1547
+ ],
1548
+ ledgerPath: ledgerBinding.ledgerPath,
1549
+ ledgerSha256: ledgerBinding.ledgerSha256,
1550
+ inputDigest: ledgerBinding.inputDigest,
1551
+ requirementToFragments: ledgerBinding.requirementToFragments,
1552
+ };
1553
+ }
1554
+ /**
1555
+ * 读取 Task Contract 已持久化的 source/requirement-ledger.json,核验字节 sha256
1556
+ * 与 inputDigest(与当前源输入 digest 一致)后产出 v2 绑定字段。缺失/损坏/漂移
1557
+ * 抛稳定错误(SOURCE_FIDELITY_LEDGER_MISSING / SOURCE_FIDELITY_LEDGER_STALE)。
1558
+ * 新鲜度侧不再"重新推导"权威输入,而是从持久化 ledger 恢复 build 侧的输入合同
1559
+ * (与 ledger build 使用完全相同的输入),避免 contract apply 重投影派生导航视图
1560
+ * 需求.md 导致重抽取漂移误判 stale(AC-DIGEST-001/002/003)。
1561
+ */
1562
+ function loadPersistedFrontendSourceBindingLedger(sources) {
1563
+ const referenceDocs = (sources.referenceDocuments ?? []).filter((doc) => doc.markdown.trim().length > 0);
1564
+ if (referenceDocs.length === 0)
1565
+ return null;
1566
+ // 存在性守卫(保持既有 v1 回退行为):派生视图无可抽取 requirement/acceptance
1567
+ // 引用时不要求持久化 ledger,直接回退 base binding。此守卫只用于"是否 ledger
1568
+ // 任务"判定,绝不参与 digest 键计算——键输入完全来自持久化 ledger 恢复。
1569
+ const guardFacts = extractRequirementFactsFromMarkdown(sources.requirementMarkdown);
1570
+ if (guardFacts.acceptanceCriteria.length === 0)
1571
+ return null;
1572
+ const ledgerAbsolutePath = path.join(sources.taskDir, "source", REQUIREMENT_LEDGER_FILE_NAME);
1573
+ let raw;
1574
+ try {
1575
+ raw = readFileSync(ledgerAbsolutePath, "utf8");
1576
+ }
1577
+ catch (error) {
1578
+ throw new Error(`SOURCE_FIDELITY_LEDGER_MISSING: frontend task ${sources.taskId} has resolvable requirement/acceptance references but no persisted ${REQUIREMENT_LEDGER_FILE_NAME} at ${ledgerAbsolutePath}; run task advance (intake) to persist the ledger before dag init-hybrid (${error instanceof Error ? error.message : String(error)})`);
1579
+ }
1580
+ const ledgerSha256 = createHash("sha256").update(raw).digest("hex");
1581
+ let ledger;
1582
+ try {
1583
+ ledger = parseLedgerJson(raw);
1584
+ }
1585
+ catch (error) {
1586
+ throw new Error(`SOURCE_FIDELITY_LEDGER_MISSING: persisted ${REQUIREMENT_LEDGER_FILE_NAME} is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`);
1587
+ }
1588
+ // canonical 侧:ledger.canonicalRequirements 按 build 顺序原样保留输入 canonical
1589
+ // requirements(id/text 逐字节),过滤物化 REQ-SRC-*(AC-HARD-002 "物化不入键")
1590
+ // 即精确恢复 digest 输入集;派生导航视图 需求.md 投影不参与键计算。
1591
+ const contract = recoverLedgerInputContract(ledger);
1592
+ if (contract.canonicalRequirements.length === 0) {
1593
+ // AC-HARD-003:存在 v2 ledger 但 canonical requirements 为空时 fail closed,
1594
+ // 绝不回退 v1 base 绑定(buildDagSourceBinding 不降级)。空 canonical 的 ledger
1595
+ // 无法提供 requirement→fragment 映射证据,任何 v1 回退都会静默放行 fail-open。
1596
+ throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} has an empty canonical requirement set; refusing to fall back to a v1 base binding; re-run task advance (intake) to rebuild the ledger`);
1597
+ }
1598
+ // source 侧:ledger.sourcePaths(build 时权威 sourceDocuments 的 repo-root
1599
+ // relative POSIX 路径集合)作为权威身份集合,对当前 materialized reference 文件
1600
+ // 做交集过滤;内容取当前字节(漂移检测)。权威路径缺失(删除/清空/重命名)→
1601
+ // fail-closed STALE;新增的非权威 role 文件被正确排除(不入键)。
1602
+ const currentByPath = new Map();
1603
+ for (const doc of referenceDocs) {
1604
+ currentByPath.set(toDagSourcePath(sources, doc.path), doc.markdown);
1605
+ }
1606
+ const missingAuthoritativePaths = contract.sourcePaths.filter((sourcePath) => !currentByPath.has(sourcePath));
1607
+ if (missingAuthoritativePaths.length > 0) {
1608
+ throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} was built from authoritative source documents no longer present (${missingAuthoritativePaths.join(", ")}); re-run task advance (intake) to rebuild the ledger`);
1609
+ }
1610
+ // inputDigest 新鲜度:与当前源输入(canonical repo-root relative identity)一致。
1611
+ // digest 只绑定语义权威输入(规范化源文档 + canonical requirements +
1612
+ // schema/reconciler version);派生导航视图 需求.md 投影不参与键计算。
1613
+ const expectedDigest = computeLedgerInputDigest({
1614
+ sourceDocuments: contract.sourcePaths.map((sourcePath) => ({
1615
+ path: sourcePath,
1616
+ content: currentByPath.get(sourcePath),
1617
+ })),
1618
+ canonicalRequirements: contract.canonicalRequirements,
1619
+ });
1620
+ if (ledger.inputDigest !== expectedDigest) {
1621
+ throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} inputDigest ${ledger.inputDigest} does not match current source input ${expectedDigest}; re-run task advance (intake) to rebuild the ledger`);
1622
+ }
1623
+ const requirementToFragments = {};
1624
+ for (const requirement of ledger.canonicalRequirements) {
1625
+ if (requirement.sourceFragmentIds.length > 0) {
1626
+ requirementToFragments[requirement.id] = requirement.sourceFragmentIds;
1627
+ }
1628
+ }
1629
+ return {
1630
+ ledgerPath: toDagSourcePath(sources, ledgerAbsolutePath),
1631
+ ledgerSha256,
1632
+ inputDigest: ledger.inputDigest,
1633
+ requirementToFragments,
1634
+ };
1635
+ }
1515
1636
  function buildBackendTestAnalysisSourceBindingContract(sources) {
1516
1637
  const binding = buildDagSourceBinding(sources);
1517
1638
  const requirement = binding.sources.find((source) => source.kind === "requirement");
@@ -2416,7 +2537,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2416
2537
  allowedPaths: taskConfig.allowedPaths,
2417
2538
  complexity: taskConfig.complexity,
2418
2539
  });
2419
- const frontendSourceBinding = buildDagSourceBinding(sources);
2540
+ const frontendSourceBinding = buildDagSourceBinding(sources, taskConfig.taskKind);
2420
2541
  const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
2421
2542
  sourceBinding: frontendSourceBinding,
2422
2543
  riskLevel: frontendRisk.selectedRisk,
@@ -6563,7 +6684,7 @@ async function buildHybridDagForTemplate(sources, template, options = {}) {
6563
6684
  if (!spec.runtimeContract) {
6564
6685
  spec.runtimeContract = GENERATED_DAG_RUNTIME_CONTRACT;
6565
6686
  }
6566
- spec.sourceBinding = buildDagSourceBinding(sources);
6687
+ spec.sourceBinding = buildDagSourceBinding(sources, template === "frontend-implementation" ? "frontend-implementation" : undefined);
6567
6688
  spec.taskContractBinding = taskContractBinding;
6568
6689
  assertNoGovernanceFlagOnDisallowedTemplate(spec, template);
6569
6690
  if (template === "standard-dag" ||
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { pathMatchesPattern } from "../../shared/git-progress.js";
6
+ import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
6
7
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
7
8
  import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
8
9
  import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
@@ -306,6 +307,35 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
306
307
  "</retry_instruction>",
307
308
  ].join("\n");
308
309
  }
310
+ const PROMPT_RESTART_CANDIDATE_MAX_BYTES = 64 * 1024;
311
+ function promptRestartCandidateForAttempt(input) {
312
+ if (input.attemptPrompt === input.baseRuntimePrompt) {
313
+ return input.taskPrompt;
314
+ }
315
+ const appendedPrefix = `${input.baseRuntimePrompt}\n\n`;
316
+ if (input.attemptPrompt.startsWith(appendedPrefix)) {
317
+ return [
318
+ input.taskPrompt,
319
+ input.attemptPrompt.slice(appendedPrefix.length),
320
+ ]
321
+ .filter(Boolean)
322
+ .join("\n\n");
323
+ }
324
+ return input.attemptPrompt;
325
+ }
326
+ async function persistFailedPromptRestartCandidate(input) {
327
+ const redacted = redactSecrets(input.prompt);
328
+ const preview = truncateUtf8Preview(redacted, PROMPT_RESTART_CANDIDATE_MAX_BYTES);
329
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, `prompt-candidate-attempt-${input.attempt}.json`, {
330
+ schemaVersion: 1,
331
+ attempt: input.attempt,
332
+ failureCategory: input.failureCategory ?? "unknown",
333
+ prompt: preview,
334
+ truncated: preview !== redacted,
335
+ maxBytes: PROMPT_RESTART_CANDIDATE_MAX_BYTES,
336
+ sha256: createHash("sha256").update(redacted).digest("hex"),
337
+ });
338
+ }
309
339
  export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd, options) {
310
340
  const policy = resolveContextPolicy(spec);
311
341
  const skillNames = policy.resolveSkills(spec, task);
@@ -770,6 +800,7 @@ export async function executeDagNode(input) {
770
800
  // exactly this attempt's segment (B1: earlier attempts legitimately use
771
801
  // ls/find and must not be attributed to the repair attempt).
772
802
  const attemptSessionEventsOffset = await sessionEventsByteLength(runDir, nodeId);
803
+ let promptRestartCandidate = task.subtask_prompt;
773
804
  let result;
774
805
  try {
775
806
  validateRepairArtifactGateBeforeShell({
@@ -833,6 +864,11 @@ export async function executeDagNode(input) {
833
864
  previousReason: previousProtocolReason,
834
865
  });
835
866
  }
867
+ promptRestartCandidate = promptRestartCandidateForAttempt({
868
+ taskPrompt: task.subtask_prompt,
869
+ baseRuntimePrompt: prompt,
870
+ attemptPrompt,
871
+ });
836
872
  result = await executeNode({
837
873
  task,
838
874
  cwd,
@@ -960,6 +996,15 @@ export async function executeDagNode(input) {
960
996
  const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
961
997
  totalAttemptWallDurationMs += attemptWallDurationMs;
962
998
  const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
999
+ if (task.executor === "pi" && !result.ok) {
1000
+ await persistFailedPromptRestartCandidate({
1001
+ runDir,
1002
+ nodeId,
1003
+ attempt: attemptNumber,
1004
+ failureCategory: result.failureCategory,
1005
+ prompt: promptRestartCandidate,
1006
+ });
1007
+ }
963
1008
  const attemptRecord = {
964
1009
  attempt: attemptNumber,
965
1010
  startedAt: attemptStartedAt,