@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
@@ -26,11 +26,12 @@ import { runConvergencePassController, } from "./convergence/controller.js";
26
26
  import { executeDagRanksOnce, FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isConditionSkippedReason, readFrontendPrewriteResult, } from "./scheduler.js";
27
27
  import { topoSortToRanks } from "./topo.js";
28
28
  import { isFrontendWriterTransientPartialWrite } from "./frontend-writer-recovery.js";
29
- import { classifyReviewIssueCategoryFailureOwner, readCommittedReviewRequestFact, writeFrontendReviewFindingsPrd, } from "./frontend-review-findings.js";
29
+ import { classifyReviewIssueCategoryFailureOwner, readCommittedDesignRequestFact, readCommittedReviewRequestFact, writeFrontendReviewFindingsPrd, } from "./frontend-review-findings.js";
30
30
  import { bindFrontendShapeStateToRunTopology, commitFrontendShapeTransitionCas, commitRuntimeFrontendShapeFact, isCanonicalFrontendShapeDag, } from "./frontend-shape-facts.js";
31
31
  import { assertFrontendTopologyBinding, buildFrontendTopologyBinding, computeFrontendShapeSourceDigest, } from "./frontend-shape.js";
32
32
  import { ensureFrontendShapeCapsuleRecord } from "./frontend-shape-capsule-store.js";
33
33
  import { parseDagSpec, resolveModelForTask, } from "./types.js";
34
+ import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
34
35
  import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
35
36
  import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
36
37
  import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
@@ -794,7 +795,7 @@ async function executeDagCheckpoint(input) {
794
795
  runDir = await moveToPausedRunDir(runDir, pausedRunDir);
795
796
  }
796
797
  else {
797
- const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
798
+ const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd, spec.frontendRecovery?.maxContinuations ?? 1);
798
799
  await finalizeRunOwnedInterrupt({
799
800
  runDir,
800
801
  state,
@@ -822,6 +823,15 @@ async function executeDagCheckpoint(input) {
822
823
  "frontend recovery request-id reuse conflict";
823
824
  state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
824
825
  }
826
+ else if (created.kind === "lease-held") {
827
+ // A different recovery entrance has already durably claimed this
828
+ // parent. Do not "help" by running its operation: that would reopen
829
+ // the exact two-child-DAG race the lease prevents.
830
+ state.status = "failed";
831
+ state.terminalReason =
832
+ "frontend recovery parent lease already held";
833
+ state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
834
+ }
825
835
  else {
826
836
  const { runRecoveryOperationController } = await import("./frontend-recovery-controller.js");
827
837
  const result = await runRecoveryOperationController({
@@ -1042,7 +1052,7 @@ function buildRecoveryResultForTrigger(state, trigger, outcome) {
1042
1052
  evidenceRefs: [],
1043
1053
  };
1044
1054
  }
1045
- export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1055
+ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, frontendRecoveryQuota = 1) {
1046
1056
  const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
1047
1057
  if (state.frontendShapeState?.terminalOutcome &&
1048
1058
  state.frontendShapeState.transitionCapsule) {
@@ -1064,16 +1074,53 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1064
1074
  // every other blocked admission is terminal.
1065
1075
  const planContractInvalid = admission.result.classification === "blocked" &&
1066
1076
  admission.result.failureSource === "frontend-plan-pi";
1077
+ // A blocked admission caused by the design review committing
1078
+ // `request_design_changes` is a definite finding (not a protocol or
1079
+ // environment error): the design was reviewed and rejected. It is the
1080
+ // design-review analog of review-request-changes — not terminal, but
1081
+ // re-enters the next plan round with the committed design findings.
1082
+ // The failureSource may be frontend-writer-admission-shell or
1083
+ // frontend-design-policy-shell; the committed design fact is the
1084
+ // authoritative signal.
1085
+ const designRequestFact = await readCommittedDesignRequestFact({
1086
+ runDir,
1087
+ });
1088
+ const designRejected = admission.result.classification === "blocked" &&
1089
+ designRequestFact !== undefined;
1067
1090
  const terminalAdmissionDenial = (admission.result.classification === "blocked" ||
1068
1091
  admission.result.classification === "stale") &&
1069
- !planContractInvalid;
1092
+ !planContractInvalid &&
1093
+ !designRejected;
1070
1094
  if (terminalAdmissionDenial) {
1071
1095
  state.status = "failed";
1072
1096
  return { recoveryPending: false };
1073
1097
  }
1098
+ if (designRejected && designRequestFact) {
1099
+ state.terminalReason = "design-request-changes";
1100
+ try {
1101
+ await writeFrontendReviewFindingsPrd({
1102
+ runDir,
1103
+ parentRunId: state.runId,
1104
+ fact: designRequestFact,
1105
+ });
1106
+ }
1107
+ catch (error) {
1108
+ console.warn(`[run-dag] warning: failed to write frontend design findings PRD: ${error instanceof Error ? error.message : String(error)}`);
1109
+ }
1110
+ const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1111
+ if (continuationCount < frontendRecoveryQuota) {
1112
+ recoveryTrigger = {
1113
+ requestId: state.runId,
1114
+ failureSource: "frontend-design-review-pi",
1115
+ failureOwner: classifyReviewIssueCategoryFailureOwner(designRequestFact.issueCategory),
1116
+ protocolFailureReason: `frontend design review request_design_changes (issueCategory=${designRequestFact.issueCategory})`,
1117
+ recoveryMode: "new-dag",
1118
+ };
1119
+ }
1120
+ }
1074
1121
  if (planContractInvalid) {
1075
1122
  const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1076
- if (continuationCount >= 1) {
1123
+ if (continuationCount >= frontendRecoveryQuota) {
1077
1124
  // Continuation quota exhausted: a second contract-invalid candidate
1078
1125
  // is authoritative candidate-contract-invalid → forced failed.
1079
1126
  state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
@@ -1109,7 +1156,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1109
1156
  });
1110
1157
  if (writerNodeId) {
1111
1158
  const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1112
- if (continuationCount < 1) {
1159
+ if (continuationCount < frontendRecoveryQuota) {
1113
1160
  const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
1114
1161
  const rollback = await rollbackFrontendWriter({
1115
1162
  cwd: repoRoot,
@@ -1141,7 +1188,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1141
1188
  const verifyFailed = state.nodes["frontend-verify-shell"]?.status === "ERROR";
1142
1189
  if (verifyFailed) {
1143
1190
  const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1144
- if (continuationCount < 1) {
1191
+ if (continuationCount < frontendRecoveryQuota) {
1145
1192
  recoveryTrigger = {
1146
1193
  requestId: state.runId,
1147
1194
  failureSource: "frontend-verify-shell",
@@ -1179,7 +1226,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1179
1226
  }
1180
1227
  if (!recoveryTrigger && !skipFrontendRecovery) {
1181
1228
  const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1182
- if (continuationCount < 1) {
1229
+ if (continuationCount < frontendRecoveryQuota) {
1183
1230
  recoveryTrigger = {
1184
1231
  requestId: state.runId,
1185
1232
  failureSource: "frontend-review-pi",
@@ -1226,16 +1273,25 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1226
1273
  /**
1227
1274
  * Construct (or reuse) the recovery operation for a failed frontend run.
1228
1275
  *
1229
- * The operation is keyed by a deterministic `clientRequestId`
1230
- * (`dag-recovery-<parentRunId>`), so repeated wakeups of the same parent hit
1231
- * `OperationStore.create`'s idempotent short-circuit instead of creating a
1232
- * second recovery run (AC-004). The continuation DagSpec/reset partition is
1233
- * frozen via `computeFrontendRecoveryPlanForSource` and serialized under the
1234
- * run dir's bounded `.runtime/` namespace never the repo root or `.harness`
1235
- * siblings. `planHash` is the second idempotency dimension of `actionParams`.
1276
+ * A parent-run atomic lease is acquired before `OperationStore.create`, then
1277
+ * its operation id is persisted into that lease before the controller may run.
1278
+ * `clientRequestId` remains a useful store-level replay key, but is not a
1279
+ * cross-process recovery admission boundary. The continuation DagSpec/reset
1280
+ * partition is frozen via `computeFrontendRecoveryPlanForSource` and
1281
+ * serialized under the run dir's bounded `.runtime/` namespace. `planHash` is
1282
+ * the second idempotency dimension of `actionParams`.
1236
1283
  */
1237
1284
  async function createRecoveryOperation(input) {
1238
1285
  const { cwd, runDir, spec, state, trigger } = input;
1286
+ const recoveryLease = await acquireDagRecoveryLease({
1287
+ cwd,
1288
+ parentRunId: state.runId,
1289
+ requestId: trigger.requestId,
1290
+ kind: "auto-frontend-recovery",
1291
+ });
1292
+ if (recoveryLease.kind === "held") {
1293
+ return { kind: "lease-held", lease: recoveryLease.lease };
1294
+ }
1239
1295
  const { computeFrontendRecoveryPlanForSource } = await import("./frontend-recovery-plan.js");
1240
1296
  const plan = computeFrontendRecoveryPlanForSource({
1241
1297
  spec,
@@ -1255,7 +1311,7 @@ async function createRecoveryOperation(input) {
1255
1311
  recoveryMode: trigger.recoveryMode,
1256
1312
  plan,
1257
1313
  });
1258
- return input.deps.store.create({
1314
+ const created = await input.deps.store.create({
1259
1315
  clientRequestId: `dag-recovery-${state.runId}`,
1260
1316
  action: "dagRunTask",
1261
1317
  actionParams: {
@@ -1277,6 +1333,17 @@ async function createRecoveryOperation(input) {
1277
1333
  "--json",
1278
1334
  ],
1279
1335
  });
1336
+ if (created.kind !== "conflict") {
1337
+ // The durable lease must name the operation before controller execution.
1338
+ // A crash in this narrow window leaves a safely blocked reservation rather
1339
+ // than a second process that can launch another recovery DAG.
1340
+ await bindDagRecoveryLeaseExecution({
1341
+ leasePath: recoveryLease.leasePath,
1342
+ lease: recoveryLease.lease,
1343
+ executionRef: created.operation.operationId,
1344
+ });
1345
+ }
1346
+ return created;
1280
1347
  }
1281
1348
  function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
1282
1349
  if (rankWriterNodeIds.length <= 1)
@@ -946,6 +946,15 @@ export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/
946
946
  role: dagRoleSchema.optional(),
947
947
  skills: z.array(z.string()).optional(),
948
948
  toolProfile: dagToolProfileSchema.optional(),
949
+ /** Bounded read-only discovery budget for nodes that inspect a repository. */
950
+ readBudget: z
951
+ .object({
952
+ maxFiles: z.number().int().positive(),
953
+ maxBytes: z.number().int().positive(),
954
+ maxMs: z.number().int().positive().optional(),
955
+ })
956
+ .strict()
957
+ .optional(),
949
958
  /**
950
959
  * Explicit Pi extension allowlist (backend implementation DAGs only):
951
960
  * frozen short ids resolved at run time against the Pi settings package
@@ -1071,20 +1080,51 @@ export const dagEvaluationBindingSchema = z
1071
1080
  .strict();
1072
1081
  /** Eval Lab Campaign Budget (W3.4); enforced by runner ledger when mode=hard. */
1073
1082
  export const dagBudgetSchema = campaignBudgetSchema;
1074
- export const dagSourceBindingSchema = z.object({
1083
+ export const dagSourceBindingSourceSchema = z.object({
1084
+ kind: z.enum(["requirement", "constraint", "reference"]),
1085
+ path: z.string().min(1),
1086
+ sha256: z
1087
+ .string()
1088
+ .regex(/^[a-f0-9]{64}$/, "sha256 must be lowercase hex"),
1089
+ });
1090
+ /**
1091
+ * DagSourceBinding v1:无 ledger 字段(schemaVersion=1,.strict() 拒绝 ledger
1092
+ * 字段混入,不得以 v1+optional 冒充 v2)。非 frontend task / 无可解析引用时使用。
1093
+ */
1094
+ export const dagSourceBindingV1Schema = z
1095
+ .object({
1075
1096
  schemaVersion: z.literal(1),
1076
1097
  taskId: z.string().min(1),
1077
- sources: z
1078
- .array(z.object({
1079
- kind: z.enum(["requirement", "constraint", "reference"]),
1080
- path: z.string().min(1),
1081
- sha256: z
1082
- .string()
1083
- .regex(/^[a-f0-9]{64}$/, "sha256 must be lowercase hex"),
1084
- }))
1085
- .min(1),
1098
+ sources: z.array(dagSourceBindingSourceSchema).min(1),
1086
1099
  requirementIds: z.array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/)),
1087
- });
1100
+ })
1101
+ .strict();
1102
+ /**
1103
+ * DagSourceBinding v2(20260824-frontend-source-fidelity-ledger-v1):仅
1104
+ * frontend-implementation 且存在可解析 requirement/acceptance 引用时产出;
1105
+ * ledgerPath/ledgerSha256/inputDigest/requirementToFragments 全部必填且完整,
1106
+ * 伪造或缺失任一字段被 schema/prewrite 拒绝(AC-002 / AC-FIDELITY-REPAIR-002)。
1107
+ */
1108
+ export const dagSourceBindingV2Schema = z
1109
+ .object({
1110
+ schemaVersion: z.literal(2),
1111
+ taskId: z.string().min(1),
1112
+ sources: z.array(dagSourceBindingSourceSchema).min(1),
1113
+ requirementIds: z.array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/)),
1114
+ /** 受管 requirement-ledger.json 的 DAG 可解析路径(.harness/tasks/…/source/…)。 */
1115
+ ledgerPath: z.string().min(1),
1116
+ ledgerSha256: z.string().regex(/^[a-f0-9]{64}$/),
1117
+ inputDigest: z.string().regex(/^[a-f0-9]{64}$/),
1118
+ requirementToFragments: z.record(z.string(), z.array(z.string())),
1119
+ })
1120
+ .strict();
1121
+ export const dagSourceBindingSchema = z.discriminatedUnion("schemaVersion", [
1122
+ dagSourceBindingV1Schema,
1123
+ dagSourceBindingV2Schema,
1124
+ ]);
1125
+ export function isDagSourceBindingV2(binding) {
1126
+ return binding.schemaVersion === 2;
1127
+ }
1088
1128
  const dagRerunFeedbackEvidencePathSchema = z
1089
1129
  .string()
1090
1130
  .min(1)
@@ -1147,6 +1187,22 @@ export const dagSpecSchema = z
1147
1187
  taskContractBinding: dagTaskContractBindingSchema.optional(),
1148
1188
  /** Optional unresolved parent-run feedback for a full task rerun. */
1149
1189
  rerunFeedback: dagRerunFeedbackSchema.optional(),
1190
+ /** Frontend recovery continuation quota (frozen at generation time from
1191
+ * task.json frontendRecovery.maxContinuations). Bounds how many times a
1192
+ * failed frontend run may auto-recover via M6 recovery operations before
1193
+ * the run stays failed and needs human intervention. */
1194
+ frontendRecovery: z
1195
+ .object({
1196
+ maxContinuations: z
1197
+ .number()
1198
+ .int()
1199
+ .min(1)
1200
+ .max(5)
1201
+ .optional()
1202
+ .default(1),
1203
+ })
1204
+ .strict()
1205
+ .optional(),
1150
1206
  /** Frozen backend-test artifact layout (plan A); absent = historical default testcase/ layout. */
1151
1207
  backendTestLayout: z
1152
1208
  .object({
package/docs/README.md CHANGED
@@ -27,6 +27,7 @@
27
27
  | 选择快速、标准或完整验证命令 | `governance/verification-matrix.md` |
28
28
  | 理解 CLI、runtime 和初始化能力边界 | `runtime/loop-agent-harness.md` |
29
29
  | 运行 DAG、查看报告或处理失败 | `runtime/agent-dag-runner.md`、`runtime/agent-dag-recovery-playbook.md`;Operator Chat P0 closeout 见 `reports/feature/2026-08-14-operator-chat-dag-recovery-p0.md` |
30
+ | 理解前端实现 DAG 与需求保真账本 | `runtime/frontend-implementation-workflow.md`(含 source fidelity ledger v2 绑定、顺序 reconciliation、独立 reviewer、human resolution 与 prewrite fail-closed);init-evolution 审查见 `reports/init-evolution/2026-08-24-frontend-source-fidelity-ledger-init-evolution-review.md` |
30
31
  | 按 TDD、验证和调试纪律推进修复 | `governance/harness-methodology-tdd.md`、`governance/harness-methodology-verification.md`、`governance/harness-methodology-debugging.md` |
31
32
  | 执行真实 backend-test provider 验证 | `runtime/backend-test-live-campaign.md` |
32
33
  | 判断单任务 loop-agent Production Readiness | `operations/production-readiness.md` |
@@ -33,6 +33,7 @@ loop-agent CLI
33
33
  | Worker readiness baseline | Attempt identity、immutable owner、lineage/revision、task-scoped lease、controller protocol、acceptance、reconcile | Agent Worker PR v1 Phase 1–7 / `0.32.0` |
34
34
  | Operator Surface | 裸 `agent-worker console`;Operate、`/inspect/`、Operator Chat、recovery CTA、intake bridge | ADR 0005、0011–0013 |
35
35
  | 自举与验证 | controller identity、skill snapshot、deterministic/live canary、verified tarball、`verify:tree` receipt | `runtime-boundaries.md`、verification matrix |
36
+ | Source fidelity ledger | 原始 requirement/acceptance 文档保持语义权威,派生 `source/需求.md` 仅作导航视图;确定性 fragment inventory、受管 `requirement-ledger.json`(进入 Task Contract transaction 与 canonical hash)、source binding v2(仅 frontend taskKind,schemaVersion=2 必填字段)、prewrite fail-closed 门禁(`SOURCE_FIDELITY_*`/`source-*` 稳定错误码)、顺序 reconciliation 与独立 reviewer、human resolution 生效、有界提示词与最终 reviewer 核对清单 | `20260824-frontend-source-fidelity-ledger-v1` + `2026-08-24-frontend-source-fidelity-repair-v2`(本页面下方 init-evolution 审查)、`CHANGELOG.md [Unreleased]` |
36
37
 
37
38
  ### 当前不变量
38
39
 
@@ -106,3 +107,47 @@ Evidence & Trust Plane
106
107
  2. 证明成立后更新 current capability summary 和本页“当前架构”。
107
108
  3. 未证明部分只保留在 active design/exec-plan,不能通过修改措辞升级状态。
108
109
  4. 使用者短索引同步到 `website/docs/overview/roadmap.md`,避免复制长版本流水账。
110
+
111
+ ## Init Evolution Review — 2026-08-24 frontend source fidelity ledger
112
+
113
+ Base/Head:见 `docs/reports/init-evolution/` 中对应 `*-init-evolution-review.md`(若已提交则含 Base/Head 字段;未提交工作区由 CI 非严格模式放行)。
114
+
115
+ ### Changed Surface
116
+
117
+ - `src/task/source-prepare/**`:新增 `fragment-inventory.ts`、`ledger.ts`;扩展 `types.ts`、`prepare.ts`、`completeness.ts`、`parse-intent.ts`(无外部 API 变化)。
118
+ - `src/task/contract/**`:`requirement-ledger.json` 进入受管 transaction 与 canonical hash;`TaskContractSourceHashesV1` 新增可选 `requirementLedgerSha256`(legacy ref 向后兼容)。
119
+ - `src/workflows/dag/**`:`dagSourceBindingSchema` 拆为 v1/v2 discriminated union(v2 schemaVersion=2,四个 ledger 字段全部必填;v1 strict 拒绝 ledger 字段);`init-hybrid` 改读已持久化 ledger(缺失/漂移 fail closed);prewrite 门禁新增 ledger 检查与失败码。
120
+ - `src/task/source-prepare/**`:新增 `ledger-reconciliation.ts`(顺序 reconciliation)、`ledger-review.ts`(独立 reviewer);fragment inventory 表格行切分、跨文档稳定 REQ-SRC ID、V4 显式 AC 标题;human resolution 生效。
121
+ - `src/worker/observe/**`:新增只读 `GET /api/tasks/:id/ledger` 与展示(安全摘要,不泄漏原文)。
122
+ - `src/worker/console/**`:Draft/ContractPanel/InspectSplitPanel 安全摘要展示。
123
+ - `docs/templates/frontend-implementation-contract.schema.json`:requirements 增加可选 `sourceFragmentIds`/`sourceRefs`,sourceBinding 增加可选 ledger 字段(all-or-none)。
124
+
125
+ ### Decision
126
+
127
+ **No init impact / Surface check only**。
128
+
129
+ Rationale:
130
+
131
+ - 新增均为模块内部实现;不新增 CLI 命令、不改 init 入口、不改变 target-project 初始化行为。
132
+ - `docs/templates/frontend-implementation-contract.schema.json` 已随 `docs/templates/` 目录 glob 打包(`package.json files`),无需新增 manifest 条目。
133
+ - 运行时加载 schema 的 `loadFrontendImplementationContractJsonSchema` 仅做结构性校验($id/required/properties/const),新增可选字段不破坏既有契约。
134
+ - 不新增任何运行时或开发依赖(node:crypto、既有 zod/Vitest/Vite/React 19)。
135
+
136
+ ### Updates Made
137
+
138
+ - `docs/architecture/evolution.md`:本条目。
139
+ - `CHANGELOG.md [Unreleased]`、`CONTEXT.md`:glossary 术语。
140
+ - `docs/templates/frontend-implementation-contract.schema.json`:可选字段同步。
141
+
142
+ ### Verification
143
+
144
+ ```bash
145
+ npm run typecheck && npm run console:typecheck
146
+ npx vitest run test/task-source-prepare.test.ts test/task-contract-canonicalize.test.ts test/task-contract-observe.test.ts test/dag-init-hybrid.test.ts test/frontend-implementation-contract.test.ts test/frontend-prewrite-gate.test.ts test/frontend-writer-admission.test.ts test/worker/observe/dag-inspector-node-input.test.ts test/worker/console/inspect-split-panel.test.ts
147
+ ```
148
+
149
+ ### Residual Risk
150
+
151
+ - 长尾 24K 截断遗漏回归:以确定性分片 inventory + ledger 覆盖统计断言固定(AC-FIDELITY-001/007)。
152
+ - 账本漂移/篡改:inputDigest 复用失效 + prewrite fail closed(AC-FIDELITY-003/006)。
153
+ - 既有带引用 frontend task 重建路径变化:ledger 纳入 canonical hash 改变 `observeTaskContractState` 判定,legacy ref 向后兼容覆盖(AC-FIDELITY-004)。
@@ -30,9 +30,11 @@
30
30
  "docs/templates/frontend-task-requirement.md",
31
31
  "docs/templates/frontend-task-constraints.md",
32
32
  "docs/templates/frontend-design-contract.md",
33
+ "docs/templates/frontend-implementation-contract.schema.json",
33
34
  "docs/templates/worker-dogfood-setup.md",
34
35
  "docs/templates/worker-dogfood-evidence.md",
35
36
  "docs/templates/harness.schema.json",
37
+ "docs/templates/spec-registry.schema.json",
36
38
  "docs/templates/interactive-ui-round2-experiment.md",
37
39
  "docs/templates/product-line/README.md",
38
40
  "docs/templates/product-line/AGENTS.md",
@@ -102,6 +104,7 @@
102
104
  "docs/feature-workflow.md",
103
105
  "docs/verification-matrix.md",
104
106
  "docs/loop-agent-harness.md",
107
+ "docs/spec-registry.json",
105
108
  "docs/harness-methodology-tdd.md",
106
109
  "docs/harness-methodology-verification.md",
107
110
  "docs/harness-methodology-debugging.md",
@@ -177,6 +180,7 @@
177
180
  ".pi/settings.json"
178
181
  ],
179
182
  "initSurface": {
183
+ "docs/spec-registry.json": "generated",
180
184
  "README.md": "managed-block",
181
185
  "AGENTS.md": "managed-block",
182
186
  ".gitignore": "managed-block",
@@ -37,7 +37,7 @@
37
37
  ## Frontend
38
38
 
39
39
  - `frontend-task-requirement.md`、`frontend-task-constraints.md`、`frontend-design-contract.md` — 前端任务输入与设计合同。
40
- - `frontend-implementation-contract.schema.json` — 前端实现合同 schema
40
+ - `frontend-implementation-contract.schema.json` — 前端实现合同 schema(sourceBinding v2 ledger 字段 all-or-none:任一出现则四项齐全,禁止 v1+optional 冒充 v2)。`requirements[].sourceRefs[].items` 使用合法 `$ref: #/$defs/path`(相对路径、无 `..` 逃逸、无反斜杠),模板 JSON 无非法键,运行时加载器可直接编译(AC-003)。
41
41
  - `frontend-test-dag.json` — frontend-test DAG 模板;其 deterministic checklist 会拒绝 alternative executable instructions,仅允许 allowlisted `playwright-cli` command。Pi generator/reviser 的 runtime writeSet 仅授权下列目标项目生成路径(不是本仓库文档索引目标):
42
42
 
43
43
  ~~~text
@@ -56,6 +56,7 @@
56
56
  - `knowledge-sync-dag.json`、`knowledge-graph-bootstrap-dag.json` — 知识同步与图谱初始化 DAG。
57
57
  - `knowledge-sync-draft.schema.json` — 知识同步草稿 schema。
58
58
  - `harness.schema.json` — `harness.json` 的 IDE JSON Schema。
59
+ - `spec-registry.schema.json` — 项目级 `ai_workspace/loop-agent/spec-registry.json` 的机器校验 schema。
59
60
 
60
61
  ## 子目录
61
62