@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
@@ -8,11 +8,15 @@ import { loadTaskConfig } from "../runtime.js";
8
8
  import { resolveFrontendSpecRootAliases } from "../frontend-project-capability.js";
9
9
  import { DEFAULT_FRONTEND_SPEC_ROOTS, extractDeclaredFrontendSpecRoots, } from "../../shared/openspec-spec.js";
10
10
  import { buildPrepareDraft } from "./build-draft.js";
11
- import { hasBlockingGaps, hasBlockingRisks, isFreshLegacyUnversioned, listPrepareGaps, } from "./completeness.js";
11
+ import { hasBlockingGaps, hasBlockingRisks, isFreshLegacyUnversioned, listPrepareGaps, appendLedgerGaps, } from "./completeness.js";
12
12
  import { detectProductArtifactMeta, isIntakeSoftGapCode, } from "./artifact-meta.js";
13
13
  import { extractRequirementFactsFromMarkdown, fillMissingRequirementFacts, mergeRequirementFacts, } from "./parse-intent.js";
14
14
  import { draftReferencesFromManifest, validateImportedPrdReferences, } from "./reference-integrity.js";
15
15
  import { readSourceManifest } from "../source-references.js";
16
+ import { buildRequirementLedger, classifyLedgerGate, effectiveSourceResolutions, summarizeLedger, validateRequirementLedger, } from "./ledger.js";
17
+ import { defaultLedgerReviewer, LEDGER_REVIEW_DEFAULT_BUDGET_CHARS, ledgerReviewItemsFromRecords, reviewLedgerFragments, } from "./ledger-review.js";
18
+ import { buildFragmentInventory } from "./fragment-inventory.js";
19
+ import { sha256Utf8 } from "../contract/hash.js";
16
20
  import { PREPARE_MAX_FILE_BYTES } from "./types.js";
17
21
  async function fileNonEmpty(filePath) {
18
22
  try {
@@ -69,6 +73,81 @@ function isProjectedSourceResolutionsSection(sectionBody) {
69
73
  return true;
70
74
  return lines.every((line) => entryRe.test(line));
71
75
  }
76
+ /**
77
+ * Typed parse of one canonical Source Resolutions line (project.ts L63-68):
78
+ * `- ${sourceRef}: ${conflict} → ${decision} (${reason}; decidedBy=${decidedBy})`.
79
+ * Tail-anchor strategy (AC-008 / AC-HARD-002, exact inverse of the projector's
80
+ * left-to-right concatenation): sourceRef ends at the first `: `, conflict ends
81
+ * at the first ` → `, and the reason group is reversed from the tail — the line
82
+ * must close with `)`, the REAL `; decidedBy=` separator is the LAST occurrence
83
+ * (reason may itself contain `; decidedBy=`), and the reason group opener is the
84
+ * LAST ` (` before that anchor (decision may itself contain ` (`). decision/reason
85
+ * round-trip byte-exact, including spaces and ASCII parentheses — no truncation
86
+ * at the first ` (`. Any missing field or shape mismatch returns null — the
87
+ * caller must reject the whole section (fail-closed), never partially accept.
88
+ * decidedBy is additionally enforced to be exactly "human" here
89
+ * (schema z.enum(['human'])); model/system/hand-edited values are treated as
90
+ * hand-edited/model-decided and block with MANAGED_PROJECTION_INCOMPLETE (AC-004b).
91
+ */
92
+ function parseSourceResolutionLine(line) {
93
+ const trimmed = line.trim();
94
+ if (!trimmed.startsWith("- "))
95
+ return null;
96
+ const rest = trimmed.slice(2);
97
+ const sourceRefEnd = rest.indexOf(": ");
98
+ if (sourceRefEnd <= 0)
99
+ return null;
100
+ const sourceRef = rest.slice(0, sourceRefEnd);
101
+ const afterSourceRef = rest.slice(sourceRefEnd + 2);
102
+ const arrow = afterSourceRef.indexOf(" → ");
103
+ if (arrow <= 0)
104
+ return null;
105
+ const conflict = afterSourceRef.slice(0, arrow);
106
+ const afterArrow = afterSourceRef.slice(arrow + 3);
107
+ // 尾部锚点:行必须以 `)` 收尾,真实分隔符取最后一次 `; decidedBy=`
108
+ // (reason 可能内嵌 ASCII `; decidedBy=`,首个锚点会截断 reason)。
109
+ if (!afterArrow.endsWith(")"))
110
+ return null;
111
+ const decidedByAnchor = "; decidedBy=";
112
+ const decidedIdx = afterArrow.lastIndexOf(decidedByAnchor);
113
+ if (decidedIdx < 0)
114
+ return null;
115
+ const decidedBy = afterArrow.slice(decidedIdx + decidedByAnchor.length, -1);
116
+ if (decidedBy !== "human")
117
+ return null;
118
+ // reason 起始:锚点之前最后一个 ` (`(decision 可能内嵌 ASCII ` (`,首个 ` (`
119
+ // 会截断 decision)。decision/reason 逐字节保留。
120
+ const reasonRegion = afterArrow.slice(0, decidedIdx);
121
+ const paren = reasonRegion.lastIndexOf(" (");
122
+ if (paren <= 0)
123
+ return null;
124
+ const decision = reasonRegion.slice(0, paren);
125
+ const reason = reasonRegion.slice(paren + 2);
126
+ if (!conflict.trim() || !decision.trim() || !reason.trim())
127
+ return null;
128
+ return { sourceRef, conflict, decision, reason, decidedBy };
129
+ }
130
+ /**
131
+ * Parse the whole `## Source Resolutions` section body into typed resolutions.
132
+ * Returns null when any line fails — the section is rejected as a whole, never
133
+ * partially accepted or silently downgraded (AC-004). Empty sections yield an
134
+ * empty array (kept semantics: isProjectedSourceResolutionsSection accepts
135
+ * empty sections).
136
+ */
137
+ function parseSourceResolutionsSection(sectionBody) {
138
+ const lines = sectionBody
139
+ .split(/\r?\n/)
140
+ .map((line) => line.trim())
141
+ .filter(Boolean);
142
+ const resolutions = [];
143
+ for (const line of lines) {
144
+ const parsed = parseSourceResolutionLine(line);
145
+ if (!parsed)
146
+ return null;
147
+ resolutions.push(parsed);
148
+ }
149
+ return resolutions;
150
+ }
72
151
  /** Section titles the read-back validator may inspect (static whitelist). */
73
152
  const MANAGED_PROJECTION_SECTION_TITLES = [
74
153
  "References",
@@ -137,6 +216,22 @@ async function loadManagedProjectionDraft(input) {
137
216
  " --from-draft <reviewed-draft.json> --json",
138
217
  };
139
218
  }
219
+ // Typed read-back (2026-08-24 L1): the shape regex above accepts any
220
+ // decidedBy value; the typed parser is the projector's exact inverse and
221
+ // additionally enforces decidedBy === 'human' (schema z.enum(['human'])).
222
+ // A regex-valid but non-canonical line (decidedBy=model/system/hand-edited)
223
+ // or any free-form line fails the whole section — never partial accept.
224
+ const parsedSourceResolutions = sourceResolutionsBody !== null
225
+ ? parseSourceResolutionsSection(sourceResolutionsBody)
226
+ : null;
227
+ if (sourceResolutionsBody !== null && parsedSourceResolutions === null) {
228
+ return {
229
+ ok: false,
230
+ message: "managed requirement projection contains hand-edited or model-decided Source Resolutions entries (only canonical decidedBy=human lines are accepted); use loop-agent task advance " +
231
+ input.taskId +
232
+ " --from-draft <reviewed-draft.json> --json",
233
+ };
234
+ }
140
235
  const requirementFacts = extractRequirementFactsFromMarkdown(requirementRaw);
141
236
  const constraintFacts = extractRequirementFactsFromMarkdown(constraints);
142
237
  if (!requirementFacts.objective?.trim()) {
@@ -186,6 +281,9 @@ async function loadManagedProjectionDraft(input) {
186
281
  ...(requirementFacts.assumptions.length > 0
187
282
  ? { assumptions: requirementFacts.assumptions }
188
283
  : {}),
284
+ ...(parsedSourceResolutions && parsedSourceResolutions.length > 0
285
+ ? { sourceResolutions: parsedSourceResolutions }
286
+ : {}),
189
287
  hardConstraints: config.hardConstraints,
190
288
  },
191
289
  };
@@ -637,6 +735,161 @@ export async function prepareTaskSource(input) {
637
735
  sourceFilesPresent,
638
736
  forceOverwriteSource: input.forceOverwriteSource,
639
737
  });
738
+ // -------------------------------------------------------------------
739
+ // Source fidelity ledger (20260824-frontend-source-fidelity-ledger-v1)
740
+ // 在 buildPrepareDraft/listPrepareGaps 与 applyTaskContract 之间编排
741
+ // fragment-inventory + ledger 构建。原始 requirement/acceptance 文档保持语义
742
+ // 权威;派生 需求.md 仅作导航视图。reconciliation 失败返回稳定错误并停止,
743
+ // 不回退摘要。
744
+ // -------------------------------------------------------------------
745
+ let ledger;
746
+ let ledgerSummary;
747
+ if (hasImportedPrd &&
748
+ sourceIntegrity &&
749
+ sourceIntegrity.parseableDocuments.some((doc) => doc.role === "requirement" || doc.role === "acceptance")) {
750
+ try {
751
+ const authoritativeDocs = sourceIntegrity.parseableDocuments.filter((doc) => (doc.role === "requirement" || doc.role === "acceptance") &&
752
+ typeof doc.content === "string" &&
753
+ doc.content.trim().length > 0);
754
+ const sourceDocuments = authoritativeDocs.map((doc) => {
755
+ // Canonical source identity 单一边界(AC-001 / AC-FIDELITY-REPAIR-001):
756
+ // ledger sourceDocuments[].path 与 DAG 侧 serializeDagTaskSourcePath /
757
+ // resolveDagTaskSourcePath 一致(repo-root relative POSIX),路径转换只
758
+ // 发生在该边界,不再二次转换进入 digest 计算。
759
+ const absolute = path.resolve(contractPaths.sourceDir, doc.materializedPath);
760
+ return {
761
+ path: path
762
+ .relative(repoRoot, absolute)
763
+ .split(path.sep)
764
+ .join("/"),
765
+ content: doc.content,
766
+ };
767
+ });
768
+ if (sourceDocuments.length > 0) {
769
+ const requirementProjection = projectRequirementMarkdown(built.draft);
770
+ ledger = await buildRequirementLedger({
771
+ taskId,
772
+ requirementSha256: sha256Utf8(requirementProjection),
773
+ sourceDocuments,
774
+ canonicalRequirements: built.draft.requirement.acceptanceCriteria,
775
+ sourceResolutions: (built.draft.sourceResolutions ?? []).map((res) => ({
776
+ sourceRef: res.sourceRef,
777
+ conflict: res.conflict,
778
+ decision: res.decision,
779
+ reason: res.reason,
780
+ decidedBy: res.decidedBy,
781
+ })),
782
+ ...(input.sourceFidelity?.reconcile
783
+ ? { reconcile: input.sourceFidelity.reconcile }
784
+ : {}),
785
+ });
786
+ const classification = classifyLedgerGate(ledger, {
787
+ expectedInputDigest: ledger.inputDigest,
788
+ });
789
+ const ledgerErrors = classification.ok ? [] : [
790
+ { code: classification.code, message: classification.message },
791
+ ];
792
+ if (!classification.ok &&
793
+ classification.code !== "SOURCE_FIDELITY_LEDGER_STALE") {
794
+ ledgerErrors.push(...validateRequirementLedger(ledger).filter((error) => error.code !== classification.code));
795
+ }
796
+ // 独立 reviewer(AC-007 / AC-FIDELITY-REPAIR-007):context-only /
797
+ // duplicate / conflict 进入独立审核阶段(与 reconciler 分离);低风险
798
+ // context-only 仅 reviewer pass 放行,高风险 context-only 始终阻断。
799
+ // review 条目携带按 budgetChars 有界截断的原始片段文本与精确 fragment id
800
+ // (AC-004 / AC-HARD-004);canonicalRequirements 采用 ledger 物化后的
801
+ // canonicalRequirements(含 REQ-SRC-*,不再仅取 draft acceptance)。
802
+ const review = await reviewLedgerFragments({
803
+ fragments: ledgerReviewItemsFromRecords(ledger.fragments, {
804
+ fragments: buildFragmentInventory({
805
+ documents: sourceDocuments,
806
+ canonicalRequirements: built.draft.requirement.acceptanceCriteria,
807
+ }).fragments,
808
+ budgetChars: LEDGER_REVIEW_DEFAULT_BUDGET_CHARS,
809
+ }),
810
+ canonicalRequirements: ledger.canonicalRequirements.map((requirement) => ({
811
+ id: requirement.id,
812
+ text: requirement.text,
813
+ })),
814
+ reviewer: input.sourceFidelity?.reviewer ?? defaultLedgerReviewer,
815
+ conflictIds: new Set(ledger.conflicts.map((entry) => entry.id)),
816
+ resolvedConflictIds: effectiveSourceResolutions(ledger),
817
+ });
818
+ // 独立 reviewer 拒绝 → 全码透传:低风险 context-only / 高风险 context-only /
819
+ // duplicate / conflict 均以精确稳定码进入 ledgerErrors;review.code===null
820
+ // 时防御性回退 SOURCE_FIDELITY_RECONCILIATION_FAILED(fail-closed)。message
821
+ // 取有界 review.reasons.join("; ")。追加前跳过与既有 ledgerErrors 中 code 与
822
+ // message 均完全一致的条目(不得按 code 单独去重:同码不同 message 必须保留)。
823
+ const reviewRejectedCodes = [];
824
+ if (!review.approved) {
825
+ const code = review.code ?? "SOURCE_FIDELITY_RECONCILIATION_FAILED";
826
+ const message = review.reasons.join("; ");
827
+ const alreadyPresent = ledgerErrors.some((error) => error.code === code && error.message === message);
828
+ if (!alreadyPresent) {
829
+ ledgerErrors.push({ code, message });
830
+ reviewRejectedCodes.push(code);
831
+ }
832
+ }
833
+ ledgerSummary = summarizeLedger(ledger, {
834
+ gateStatus: ledgerErrors.length > 0 ? "fail" : "pass",
835
+ failureCode: ledgerErrors[0]?.code ?? null,
836
+ });
837
+ appendLedgerGaps({
838
+ gaps,
839
+ risks,
840
+ ledgerSummary,
841
+ ledgerErrors,
842
+ taskKind: built.draft.taskKind,
843
+ });
844
+ // appendLedgerGaps 只消费 LEDGER_MISSING / CONTEXT_ONLY_UNREVIEWED 与摘要派生码
845
+ // (UNRESOLVED / CONFLICT / HIGH_RISK_UNCOVERED);reviewer 派生且未被消费的码
846
+ // (如 duplicate → SOURCE_FIDELITY_RECONCILIATION_FAILED)内联补 gap+risk,镜像
847
+ // catch 块 L917–L925 模式,保证 hasBlockingGaps(gaps) 对 frontend-implementation
848
+ // 触发、prepare/apply 真正 fail closed。追加前检查 gaps 中尚不存在同码同级别 gap
849
+ // (appendLedgerGaps 或 catch 块可能已产生),避免双条目。
850
+ const reviewerBlocking = built.draft.taskKind === "frontend-implementation";
851
+ const reviewerGapLevel = reviewerBlocking
852
+ ? "blocking"
853
+ : "warning";
854
+ for (const code of reviewRejectedCodes) {
855
+ if (gaps.some((gap) => gap.code === code &&
856
+ gap.level === reviewerGapLevel)) {
857
+ continue;
858
+ }
859
+ const message = ledgerErrors.find((error) => error.code === code)
860
+ ?.message ??
861
+ `independent reviewer rejected (${code})`;
862
+ gaps.push({
863
+ code,
864
+ level: reviewerGapLevel,
865
+ message,
866
+ });
867
+ risks.push({
868
+ code,
869
+ level: reviewerBlocking ? "blocking" : "elevated",
870
+ message,
871
+ });
872
+ }
873
+ }
874
+ }
875
+ catch (error) {
876
+ const message = error instanceof Error ? error.message : String(error);
877
+ gaps.push({
878
+ code: "SOURCE_FIDELITY_RECONCILIATION_FAILED",
879
+ level: "blocking",
880
+ message: `requirement ledger reconciliation failed: ${message}(不回退摘要)`,
881
+ });
882
+ risks.push({
883
+ code: "SOURCE_FIDELITY_RECONCILIATION_FAILED",
884
+ level: "blocking",
885
+ message: `requirement ledger reconciliation failed: ${message}`,
886
+ });
887
+ }
888
+ }
889
+ const ledgerResult = {
890
+ ...(ledger ? { ledger } : {}),
891
+ ...(ledgerSummary ? { ledgerSummary } : {}),
892
+ };
640
893
  if (managedProjectionError) {
641
894
  gaps.push({
642
895
  code: "MANAGED_PROJECTION_INCOMPLETE",
@@ -778,6 +1031,7 @@ export async function prepareTaskSource(input) {
778
1031
  next,
779
1032
  contractStatus: state.effectiveStatus,
780
1033
  warnings,
1034
+ ...ledgerResult,
781
1035
  ...(blocking
782
1036
  ? {
783
1037
  error: {
@@ -812,6 +1066,7 @@ export async function prepareTaskSource(input) {
812
1066
  next: [`loop-agent task advance ${taskId} --json`],
813
1067
  contractStatus: state.effectiveStatus,
814
1068
  warnings,
1069
+ ...ledgerResult,
815
1070
  error: {
816
1071
  code: "TRANSACTION_INCOMPLETE",
817
1072
  message: "unfinished transaction; re-run task advance (auto recover-contract)",
@@ -838,6 +1093,7 @@ export async function prepareTaskSource(input) {
838
1093
  next,
839
1094
  contractStatus: state.effectiveStatus,
840
1095
  warnings,
1096
+ ...ledgerResult,
841
1097
  error: {
842
1098
  code: blockingCodes.includes("DIRTY_SOURCE")
843
1099
  ? "DIRTY_SOURCE"
@@ -868,6 +1124,7 @@ export async function prepareTaskSource(input) {
868
1124
  risks,
869
1125
  projected,
870
1126
  next: [`loop-agent task status ${taskId} --details --json`],
1127
+ ...ledgerResult,
871
1128
  error: {
872
1129
  code: "OBSERVED_HASH_CONFLICT",
873
1130
  message: "unable to determine expectedObservedHash",
@@ -888,6 +1145,7 @@ export async function prepareTaskSource(input) {
888
1145
  requestId,
889
1146
  requestPayloadSha256,
890
1147
  payloadSha256Trusted: true,
1148
+ ...(ledger ? { requirementLedger: ledger } : {}),
891
1149
  });
892
1150
  return {
893
1151
  ok: true,
@@ -910,6 +1168,7 @@ export async function prepareTaskSource(input) {
910
1168
  }),
911
1169
  contractStatus: "managed",
912
1170
  warnings,
1171
+ ...ledgerResult,
913
1172
  };
914
1173
  }
915
1174
  catch (error) {
@@ -930,6 +1189,7 @@ export async function prepareTaskSource(input) {
930
1189
  `loop-agent task status ${taskId} --json`,
931
1190
  `loop-agent task status ${taskId} --details --json`,
932
1191
  ],
1192
+ ...ledgerResult,
933
1193
  error: {
934
1194
  code: error.code,
935
1195
  message: error.message,
@@ -947,6 +1207,7 @@ export async function prepareTaskSource(input) {
947
1207
  risks,
948
1208
  projected,
949
1209
  next: [],
1210
+ ...ledgerResult,
950
1211
  error: {
951
1212
  code: "INTERNAL_ERROR",
952
1213
  message: error instanceof Error ? error.message : String(error),
@@ -411,6 +411,24 @@ export async function buildRepoHint(repoRoot) {
411
411
  }
412
412
  return lines.join("\n");
413
413
  }
414
+ /**
415
+ * 在 maxChars 附近回退到最近的标题边界再截断(AC-005 / AC-FIDELITY-REPAIR-005
416
+ * 回归点移除):避免把可执行项/列表项从中间切开吞掉。仅用于模型提示词预算;
417
+ * ledger fragment inventory 始终消费完整原文,不经过此裁剪。
418
+ */
419
+ function clipAtSectionBoundary(content, maxChars) {
420
+ if (content.length <= maxChars)
421
+ return content;
422
+ const prefix = content.slice(0, maxChars);
423
+ const headingRe = /^(?:#{1,6})\s+.+$/gm;
424
+ let boundary = prefix.length;
425
+ let match;
426
+ while ((match = headingRe.exec(prefix)) !== null) {
427
+ if (match.index >= maxChars * 0.4)
428
+ boundary = match.index;
429
+ }
430
+ return `${content.slice(0, boundary)}\n\n[truncated at section boundary]`;
431
+ }
414
432
  async function buildPrompt(input) {
415
433
  const bodies = input.documents
416
434
  .map((doc, index) => {
@@ -418,9 +436,7 @@ async function buildPrompt(input) {
418
436
  const metaLine = meta.recognized
419
437
  ? `artifact_type=${meta.artifactType} status=${meta.requirementStatus ?? meta.analysisStatus ?? "n/a"}`
420
438
  : "artifact_type=unknown";
421
- const clipped = doc.content.length > 24_000
422
- ? `${doc.content.slice(0, 24_000)}\n\n[truncated]`
423
- : doc.content;
439
+ const clipped = clipAtSectionBoundary(doc.content, 24_000);
424
440
  return [
425
441
  `### Document ${index + 1}`,
426
442
  `role: ${doc.role}`,
@@ -441,8 +457,12 @@ async function buildPrompt(input) {
441
457
  "Do NOT invent product behavior that is not supported by the documents.",
442
458
  "You MAY recommend an optional engineering block when the repo layout hint supports it:",
443
459
  "- allowedPaths / forbiddenPaths as minimal precise globs (e.g. source/packages/**/*.test.ts)",
444
- "- verifyCommands as real package scripts (e.g. pnpm -C source test)",
445
- "- taskKind when clearly implied (backend-test / frontend-test / standard / …)",
460
+ "- verifyCommands as real package scripts (e.g. pnpm -C source test); when acceptance criteria name a specific test file to run (e.g. 定向 vitest(test/.../xxx.test.ts)), emit the corresponding targeted command as its own entry (e.g. npx vitest run test/.../xxx.test.ts) instead of only the full-suite script — targeted commands are what a frontend implementation plan maps to behavior verification targets",
461
+ "- taskKind when clearly implied, choosing from {standard / frontend-implementation / frontend-test / backend-test / knowledge-sync}:",
462
+ " - frontend-implementation: the deliverable is a frontend component/page/UI feature (even when it ships with its own tests)",
463
+ " - frontend-test: the deliverable is a browser/UI automation test suite for EXISTING functionality (no implementation work)",
464
+ " - backend-test: pytest/backend test suite; standard: general repo implementation",
465
+ " A task that IMPLEMENTS a component and adds tests for it is frontend-implementation, never frontend-test.",
446
466
  "Prefer smallest correct write boundary; do not invent product paths that are not in the layout hint.",
447
467
  "Do NOT rewrite or suggest editing the original PRD files.",
448
468
  "Prefer explicit text; use provenance derived for rephrasing; use inferred only when clearly implied — if you must invent ACs, leave acceptanceCriteria empty so the system can stop for clarification.",