@tea-agent/loop-agent 0.40.0-next.2 → 0.40.0-next.3

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 (136) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +8 -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/shared/dag-prompt-override.js +4 -5
  9. package/dist/shared/update/npm-client.js +33 -11
  10. package/dist/task/contract/apply.js +11 -1
  11. package/dist/task/contract/constants.js +2 -0
  12. package/dist/task/contract/hash.js +3 -0
  13. package/dist/task/contract/observe.js +25 -4
  14. package/dist/task/contract/paths.js +2 -1
  15. package/dist/task/contract/project.js +15 -1
  16. package/dist/task/contract/schema.js +2 -0
  17. package/dist/task/contract/transaction.js +12 -0
  18. package/dist/task/source-prepare/completeness.js +188 -0
  19. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  20. package/dist/task/source-prepare/index.js +3 -0
  21. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  22. package/dist/task/source-prepare/ledger-review.js +214 -0
  23. package/dist/task/source-prepare/ledger.js +545 -0
  24. package/dist/task/source-prepare/prepare.js +262 -1
  25. package/dist/task/source-prepare/semantic-intake.js +19 -3
  26. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  27. package/dist/task/source-prepare/types.js +22 -0
  28. package/dist/worker/cli.js +12 -26
  29. package/dist/worker/console/console-handoff.js +82 -0
  30. package/dist/worker/console/console-update-and-init.js +115 -0
  31. package/dist/worker/console/console-update-runtime.js +84 -0
  32. package/dist/worker/console/draft-store.js +49 -0
  33. package/dist/worker/console/operation-store.js +1 -1
  34. package/dist/worker/console/operator-actions.js +129 -4
  35. package/dist/worker/console/operator-surface-health.js +1 -0
  36. package/dist/worker/console/prd-intake-bridge.js +13 -0
  37. package/dist/worker/console/routes.js +3 -0
  38. package/dist/worker/console/server.js +73 -5
  39. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DLMldIE8.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
  40. package/dist/worker/console/static/assets/{arc-DVOg1dSW.js → arc-B4i9cVxX.js} +1 -1
  41. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-715v7joo.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
  42. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtMEGtAQ.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
  43. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-qUxXsdxu.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
  44. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
  45. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BYMag72f.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5jt5rtx.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DvZghfS2.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CXutxAmY.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BkANIDWk.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bwy1wwlr.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3pWYVCG.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DRzjzmFD.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C4xXDxXL.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BngIZ90b.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
  55. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
  56. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
  57. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-B40ACoFn.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
  58. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BE-1ZRAI.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
  59. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BoYbwVTh.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
  60. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-B5-KA0fK.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
  61. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DJpCToT5.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
  62. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-De8R04Ng.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
  63. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BU1N8DiA.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
  64. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BUUUzSYI.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
  65. package/dist/worker/console/static/assets/{diagram-UB23O5K3-zLoT1RC4.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
  66. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-_q4-4N8c.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
  67. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CTqzoNSL.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
  68. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMKSYKJM.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
  69. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Dj6U8Sb7.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
  70. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BfoxVYky.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
  71. package/dist/worker/console/static/assets/{index-CA0nQEbb.js → index-CW88Fjbw.js} +81 -81
  72. package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
  73. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-T5NfvHGV.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
  74. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BEhUMkut.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
  75. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-B2Ld6nNt.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
  76. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-nV9T85yS.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
  77. package/dist/worker/console/static/assets/{linear-CIO1zDYh.js → linear-aGmcjgTn.js} +1 -1
  78. package/dist/worker/console/static/assets/{mermaid.core-B_ucAqLq.js → mermaid.core-Biyo-tqA.js} +5 -5
  79. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BIA3VNcZ.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
  80. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CNzLjNV3.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
  81. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-PWytzfc_.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
  82. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CS0wGoIq.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
  83. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-C3Wsqph8.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
  84. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DF6FDTYg.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
  85. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BdQUJm6y.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
  86. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-Bv9rppGg.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
  87. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C3Hpvb_B.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
  88. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CZOWLpno.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
  89. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
  90. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CPQRIZx5.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
  91. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
  92. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CkwM9BgA.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
  93. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-wGSoDzYc.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
  94. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-GNVas2gx.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
  95. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-C5USnHw7.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
  96. package/dist/worker/console/static/index.html +2 -2
  97. package/dist/worker/console/static-src/app/console-types.js +40 -1
  98. package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
  99. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  100. package/dist/worker/observability/init-runtime-activity.js +26 -0
  101. package/dist/worker/observe/node-input.js +88 -2
  102. package/dist/worker/observe/routes.js +188 -0
  103. package/dist/worker/observe/static/api.js +3 -3
  104. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  105. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  106. package/dist/worker/observe/static/format.js +37 -0
  107. package/dist/worker/observe/static/index.html +5 -0
  108. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  109. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  110. package/dist/worker/observe/static/state.js +2 -2
  111. package/dist/worker/observe/static/styles.css +338 -35
  112. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  113. package/dist/worker/observe/static/views/dag-inspector.js +531 -78
  114. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  115. package/dist/worker/observe/static/views/dag.js +4 -3
  116. package/dist/worker/observe/static/views/task.js +55 -0
  117. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  118. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  119. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  120. package/dist/workflows/dag/frontend-review-context.js +92 -10
  121. package/dist/workflows/dag/init-hybrid.js +125 -4
  122. package/dist/workflows/dag/node-execution.js +45 -0
  123. package/dist/workflows/dag/types.js +42 -11
  124. package/docs/README.md +1 -0
  125. package/docs/architecture/evolution.md +45 -0
  126. package/docs/init-surface.manifest.json +1 -0
  127. package/docs/templates/README.md +1 -1
  128. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  129. package/package.json +1 -1
  130. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  131. package/dist/worker/console/static/assets/channel-BG6TLen_.js +0 -1
  132. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GuyUqs0h.js +0 -1
  133. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GuyUqs0h.js +0 -1
  134. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  135. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dgsvn63q.js +0 -1
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DaNfG1BJ.js +0 -8
@@ -6,11 +6,15 @@ import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
6
6
  import { getTaskContractPaths } from "../contract/paths.js";
7
7
  import { loadTaskConfig } from "../runtime.js";
8
8
  import { buildPrepareDraft } from "./build-draft.js";
9
- import { hasBlockingGaps, hasBlockingRisks, isFreshLegacyUnversioned, listPrepareGaps, } from "./completeness.js";
9
+ import { hasBlockingGaps, hasBlockingRisks, isFreshLegacyUnversioned, listPrepareGaps, appendLedgerGaps, } from "./completeness.js";
10
10
  import { detectProductArtifactMeta, isIntakeSoftGapCode, } from "./artifact-meta.js";
11
11
  import { extractRequirementFactsFromMarkdown, fillMissingRequirementFacts, mergeRequirementFacts, } from "./parse-intent.js";
12
12
  import { draftReferencesFromManifest, validateImportedPrdReferences, } from "./reference-integrity.js";
13
13
  import { readSourceManifest } from "../source-references.js";
14
+ import { buildRequirementLedger, classifyLedgerGate, effectiveSourceResolutions, summarizeLedger, validateRequirementLedger, } from "./ledger.js";
15
+ import { defaultLedgerReviewer, LEDGER_REVIEW_DEFAULT_BUDGET_CHARS, ledgerReviewItemsFromRecords, reviewLedgerFragments, } from "./ledger-review.js";
16
+ import { buildFragmentInventory } from "./fragment-inventory.js";
17
+ import { sha256Utf8 } from "../contract/hash.js";
14
18
  import { PREPARE_MAX_FILE_BYTES } from "./types.js";
15
19
  async function fileNonEmpty(filePath) {
16
20
  try {
@@ -67,6 +71,81 @@ function isProjectedSourceResolutionsSection(sectionBody) {
67
71
  return true;
68
72
  return lines.every((line) => entryRe.test(line));
69
73
  }
74
+ /**
75
+ * Typed parse of one canonical Source Resolutions line (project.ts L63-68):
76
+ * `- ${sourceRef}: ${conflict} → ${decision} (${reason}; decidedBy=${decidedBy})`.
77
+ * Tail-anchor strategy (AC-008 / AC-HARD-002, exact inverse of the projector's
78
+ * left-to-right concatenation): sourceRef ends at the first `: `, conflict ends
79
+ * at the first ` → `, and the reason group is reversed from the tail — the line
80
+ * must close with `)`, the REAL `; decidedBy=` separator is the LAST occurrence
81
+ * (reason may itself contain `; decidedBy=`), and the reason group opener is the
82
+ * LAST ` (` before that anchor (decision may itself contain ` (`). decision/reason
83
+ * round-trip byte-exact, including spaces and ASCII parentheses — no truncation
84
+ * at the first ` (`. Any missing field or shape mismatch returns null — the
85
+ * caller must reject the whole section (fail-closed), never partially accept.
86
+ * decidedBy is additionally enforced to be exactly "human" here
87
+ * (schema z.enum(['human'])); model/system/hand-edited values are treated as
88
+ * hand-edited/model-decided and block with MANAGED_PROJECTION_INCOMPLETE (AC-004b).
89
+ */
90
+ function parseSourceResolutionLine(line) {
91
+ const trimmed = line.trim();
92
+ if (!trimmed.startsWith("- "))
93
+ return null;
94
+ const rest = trimmed.slice(2);
95
+ const sourceRefEnd = rest.indexOf(": ");
96
+ if (sourceRefEnd <= 0)
97
+ return null;
98
+ const sourceRef = rest.slice(0, sourceRefEnd);
99
+ const afterSourceRef = rest.slice(sourceRefEnd + 2);
100
+ const arrow = afterSourceRef.indexOf(" → ");
101
+ if (arrow <= 0)
102
+ return null;
103
+ const conflict = afterSourceRef.slice(0, arrow);
104
+ const afterArrow = afterSourceRef.slice(arrow + 3);
105
+ // 尾部锚点:行必须以 `)` 收尾,真实分隔符取最后一次 `; decidedBy=`
106
+ // (reason 可能内嵌 ASCII `; decidedBy=`,首个锚点会截断 reason)。
107
+ if (!afterArrow.endsWith(")"))
108
+ return null;
109
+ const decidedByAnchor = "; decidedBy=";
110
+ const decidedIdx = afterArrow.lastIndexOf(decidedByAnchor);
111
+ if (decidedIdx < 0)
112
+ return null;
113
+ const decidedBy = afterArrow.slice(decidedIdx + decidedByAnchor.length, -1);
114
+ if (decidedBy !== "human")
115
+ return null;
116
+ // reason 起始:锚点之前最后一个 ` (`(decision 可能内嵌 ASCII ` (`,首个 ` (`
117
+ // 会截断 decision)。decision/reason 逐字节保留。
118
+ const reasonRegion = afterArrow.slice(0, decidedIdx);
119
+ const paren = reasonRegion.lastIndexOf(" (");
120
+ if (paren <= 0)
121
+ return null;
122
+ const decision = reasonRegion.slice(0, paren);
123
+ const reason = reasonRegion.slice(paren + 2);
124
+ if (!conflict.trim() || !decision.trim() || !reason.trim())
125
+ return null;
126
+ return { sourceRef, conflict, decision, reason, decidedBy };
127
+ }
128
+ /**
129
+ * Parse the whole `## Source Resolutions` section body into typed resolutions.
130
+ * Returns null when any line fails — the section is rejected as a whole, never
131
+ * partially accepted or silently downgraded (AC-004). Empty sections yield an
132
+ * empty array (kept semantics: isProjectedSourceResolutionsSection accepts
133
+ * empty sections).
134
+ */
135
+ function parseSourceResolutionsSection(sectionBody) {
136
+ const lines = sectionBody
137
+ .split(/\r?\n/)
138
+ .map((line) => line.trim())
139
+ .filter(Boolean);
140
+ const resolutions = [];
141
+ for (const line of lines) {
142
+ const parsed = parseSourceResolutionLine(line);
143
+ if (!parsed)
144
+ return null;
145
+ resolutions.push(parsed);
146
+ }
147
+ return resolutions;
148
+ }
70
149
  /** Section titles the read-back validator may inspect (static whitelist). */
71
150
  const MANAGED_PROJECTION_SECTION_TITLES = [
72
151
  "References",
@@ -135,6 +214,22 @@ async function loadManagedProjectionDraft(input) {
135
214
  " --from-draft <reviewed-draft.json> --json",
136
215
  };
137
216
  }
217
+ // Typed read-back (2026-08-24 L1): the shape regex above accepts any
218
+ // decidedBy value; the typed parser is the projector's exact inverse and
219
+ // additionally enforces decidedBy === 'human' (schema z.enum(['human'])).
220
+ // A regex-valid but non-canonical line (decidedBy=model/system/hand-edited)
221
+ // or any free-form line fails the whole section — never partial accept.
222
+ const parsedSourceResolutions = sourceResolutionsBody !== null
223
+ ? parseSourceResolutionsSection(sourceResolutionsBody)
224
+ : null;
225
+ if (sourceResolutionsBody !== null && parsedSourceResolutions === null) {
226
+ return {
227
+ ok: false,
228
+ 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 " +
229
+ input.taskId +
230
+ " --from-draft <reviewed-draft.json> --json",
231
+ };
232
+ }
138
233
  const requirementFacts = extractRequirementFactsFromMarkdown(requirementRaw);
139
234
  const constraintFacts = extractRequirementFactsFromMarkdown(constraints);
140
235
  if (!requirementFacts.objective?.trim()) {
@@ -184,6 +279,9 @@ async function loadManagedProjectionDraft(input) {
184
279
  ...(requirementFacts.assumptions.length > 0
185
280
  ? { assumptions: requirementFacts.assumptions }
186
281
  : {}),
282
+ ...(parsedSourceResolutions && parsedSourceResolutions.length > 0
283
+ ? { sourceResolutions: parsedSourceResolutions }
284
+ : {}),
187
285
  hardConstraints: config.hardConstraints,
188
286
  },
189
287
  };
@@ -601,6 +699,161 @@ export async function prepareTaskSource(input) {
601
699
  sourceFilesPresent,
602
700
  forceOverwriteSource: input.forceOverwriteSource,
603
701
  });
702
+ // -------------------------------------------------------------------
703
+ // Source fidelity ledger (20260824-frontend-source-fidelity-ledger-v1)
704
+ // 在 buildPrepareDraft/listPrepareGaps 与 applyTaskContract 之间编排
705
+ // fragment-inventory + ledger 构建。原始 requirement/acceptance 文档保持语义
706
+ // 权威;派生 需求.md 仅作导航视图。reconciliation 失败返回稳定错误并停止,
707
+ // 不回退摘要。
708
+ // -------------------------------------------------------------------
709
+ let ledger;
710
+ let ledgerSummary;
711
+ if (hasImportedPrd &&
712
+ sourceIntegrity &&
713
+ sourceIntegrity.parseableDocuments.some((doc) => doc.role === "requirement" || doc.role === "acceptance")) {
714
+ try {
715
+ const authoritativeDocs = sourceIntegrity.parseableDocuments.filter((doc) => (doc.role === "requirement" || doc.role === "acceptance") &&
716
+ typeof doc.content === "string" &&
717
+ doc.content.trim().length > 0);
718
+ const sourceDocuments = authoritativeDocs.map((doc) => {
719
+ // Canonical source identity 单一边界(AC-001 / AC-FIDELITY-REPAIR-001):
720
+ // ledger sourceDocuments[].path 与 DAG 侧 serializeDagTaskSourcePath /
721
+ // resolveDagTaskSourcePath 一致(repo-root relative POSIX),路径转换只
722
+ // 发生在该边界,不再二次转换进入 digest 计算。
723
+ const absolute = path.resolve(contractPaths.sourceDir, doc.materializedPath);
724
+ return {
725
+ path: path
726
+ .relative(repoRoot, absolute)
727
+ .split(path.sep)
728
+ .join("/"),
729
+ content: doc.content,
730
+ };
731
+ });
732
+ if (sourceDocuments.length > 0) {
733
+ const requirementProjection = projectRequirementMarkdown(built.draft);
734
+ ledger = await buildRequirementLedger({
735
+ taskId,
736
+ requirementSha256: sha256Utf8(requirementProjection),
737
+ sourceDocuments,
738
+ canonicalRequirements: built.draft.requirement.acceptanceCriteria,
739
+ sourceResolutions: (built.draft.sourceResolutions ?? []).map((res) => ({
740
+ sourceRef: res.sourceRef,
741
+ conflict: res.conflict,
742
+ decision: res.decision,
743
+ reason: res.reason,
744
+ decidedBy: res.decidedBy,
745
+ })),
746
+ ...(input.sourceFidelity?.reconcile
747
+ ? { reconcile: input.sourceFidelity.reconcile }
748
+ : {}),
749
+ });
750
+ const classification = classifyLedgerGate(ledger, {
751
+ expectedInputDigest: ledger.inputDigest,
752
+ });
753
+ const ledgerErrors = classification.ok ? [] : [
754
+ { code: classification.code, message: classification.message },
755
+ ];
756
+ if (!classification.ok &&
757
+ classification.code !== "SOURCE_FIDELITY_LEDGER_STALE") {
758
+ ledgerErrors.push(...validateRequirementLedger(ledger).filter((error) => error.code !== classification.code));
759
+ }
760
+ // 独立 reviewer(AC-007 / AC-FIDELITY-REPAIR-007):context-only /
761
+ // duplicate / conflict 进入独立审核阶段(与 reconciler 分离);低风险
762
+ // context-only 仅 reviewer pass 放行,高风险 context-only 始终阻断。
763
+ // review 条目携带按 budgetChars 有界截断的原始片段文本与精确 fragment id
764
+ // (AC-004 / AC-HARD-004);canonicalRequirements 采用 ledger 物化后的
765
+ // canonicalRequirements(含 REQ-SRC-*,不再仅取 draft acceptance)。
766
+ const review = await reviewLedgerFragments({
767
+ fragments: ledgerReviewItemsFromRecords(ledger.fragments, {
768
+ fragments: buildFragmentInventory({
769
+ documents: sourceDocuments,
770
+ canonicalRequirements: built.draft.requirement.acceptanceCriteria,
771
+ }).fragments,
772
+ budgetChars: LEDGER_REVIEW_DEFAULT_BUDGET_CHARS,
773
+ }),
774
+ canonicalRequirements: ledger.canonicalRequirements.map((requirement) => ({
775
+ id: requirement.id,
776
+ text: requirement.text,
777
+ })),
778
+ reviewer: input.sourceFidelity?.reviewer ?? defaultLedgerReviewer,
779
+ conflictIds: new Set(ledger.conflicts.map((entry) => entry.id)),
780
+ resolvedConflictIds: effectiveSourceResolutions(ledger),
781
+ });
782
+ // 独立 reviewer 拒绝 → 全码透传:低风险 context-only / 高风险 context-only /
783
+ // duplicate / conflict 均以精确稳定码进入 ledgerErrors;review.code===null
784
+ // 时防御性回退 SOURCE_FIDELITY_RECONCILIATION_FAILED(fail-closed)。message
785
+ // 取有界 review.reasons.join("; ")。追加前跳过与既有 ledgerErrors 中 code 与
786
+ // message 均完全一致的条目(不得按 code 单独去重:同码不同 message 必须保留)。
787
+ const reviewRejectedCodes = [];
788
+ if (!review.approved) {
789
+ const code = review.code ?? "SOURCE_FIDELITY_RECONCILIATION_FAILED";
790
+ const message = review.reasons.join("; ");
791
+ const alreadyPresent = ledgerErrors.some((error) => error.code === code && error.message === message);
792
+ if (!alreadyPresent) {
793
+ ledgerErrors.push({ code, message });
794
+ reviewRejectedCodes.push(code);
795
+ }
796
+ }
797
+ ledgerSummary = summarizeLedger(ledger, {
798
+ gateStatus: ledgerErrors.length > 0 ? "fail" : "pass",
799
+ failureCode: ledgerErrors[0]?.code ?? null,
800
+ });
801
+ appendLedgerGaps({
802
+ gaps,
803
+ risks,
804
+ ledgerSummary,
805
+ ledgerErrors,
806
+ taskKind: built.draft.taskKind,
807
+ });
808
+ // appendLedgerGaps 只消费 LEDGER_MISSING / CONTEXT_ONLY_UNREVIEWED 与摘要派生码
809
+ // (UNRESOLVED / CONFLICT / HIGH_RISK_UNCOVERED);reviewer 派生且未被消费的码
810
+ // (如 duplicate → SOURCE_FIDELITY_RECONCILIATION_FAILED)内联补 gap+risk,镜像
811
+ // catch 块 L917–L925 模式,保证 hasBlockingGaps(gaps) 对 frontend-implementation
812
+ // 触发、prepare/apply 真正 fail closed。追加前检查 gaps 中尚不存在同码同级别 gap
813
+ // (appendLedgerGaps 或 catch 块可能已产生),避免双条目。
814
+ const reviewerBlocking = built.draft.taskKind === "frontend-implementation";
815
+ const reviewerGapLevel = reviewerBlocking
816
+ ? "blocking"
817
+ : "warning";
818
+ for (const code of reviewRejectedCodes) {
819
+ if (gaps.some((gap) => gap.code === code &&
820
+ gap.level === reviewerGapLevel)) {
821
+ continue;
822
+ }
823
+ const message = ledgerErrors.find((error) => error.code === code)
824
+ ?.message ??
825
+ `independent reviewer rejected (${code})`;
826
+ gaps.push({
827
+ code,
828
+ level: reviewerGapLevel,
829
+ message,
830
+ });
831
+ risks.push({
832
+ code,
833
+ level: reviewerBlocking ? "blocking" : "elevated",
834
+ message,
835
+ });
836
+ }
837
+ }
838
+ }
839
+ catch (error) {
840
+ const message = error instanceof Error ? error.message : String(error);
841
+ gaps.push({
842
+ code: "SOURCE_FIDELITY_RECONCILIATION_FAILED",
843
+ level: "blocking",
844
+ message: `requirement ledger reconciliation failed: ${message}(不回退摘要)`,
845
+ });
846
+ risks.push({
847
+ code: "SOURCE_FIDELITY_RECONCILIATION_FAILED",
848
+ level: "blocking",
849
+ message: `requirement ledger reconciliation failed: ${message}`,
850
+ });
851
+ }
852
+ }
853
+ const ledgerResult = {
854
+ ...(ledger ? { ledger } : {}),
855
+ ...(ledgerSummary ? { ledgerSummary } : {}),
856
+ };
604
857
  if (managedProjectionError) {
605
858
  gaps.push({
606
859
  code: "MANAGED_PROJECTION_INCOMPLETE",
@@ -742,6 +995,7 @@ export async function prepareTaskSource(input) {
742
995
  next,
743
996
  contractStatus: state.effectiveStatus,
744
997
  warnings,
998
+ ...ledgerResult,
745
999
  ...(blocking
746
1000
  ? {
747
1001
  error: {
@@ -776,6 +1030,7 @@ export async function prepareTaskSource(input) {
776
1030
  next: [`loop-agent task advance ${taskId} --json`],
777
1031
  contractStatus: state.effectiveStatus,
778
1032
  warnings,
1033
+ ...ledgerResult,
779
1034
  error: {
780
1035
  code: "TRANSACTION_INCOMPLETE",
781
1036
  message: "unfinished transaction; re-run task advance (auto recover-contract)",
@@ -802,6 +1057,7 @@ export async function prepareTaskSource(input) {
802
1057
  next,
803
1058
  contractStatus: state.effectiveStatus,
804
1059
  warnings,
1060
+ ...ledgerResult,
805
1061
  error: {
806
1062
  code: blockingCodes.includes("DIRTY_SOURCE")
807
1063
  ? "DIRTY_SOURCE"
@@ -832,6 +1088,7 @@ export async function prepareTaskSource(input) {
832
1088
  risks,
833
1089
  projected,
834
1090
  next: [`loop-agent task status ${taskId} --details --json`],
1091
+ ...ledgerResult,
835
1092
  error: {
836
1093
  code: "OBSERVED_HASH_CONFLICT",
837
1094
  message: "unable to determine expectedObservedHash",
@@ -852,6 +1109,7 @@ export async function prepareTaskSource(input) {
852
1109
  requestId,
853
1110
  requestPayloadSha256,
854
1111
  payloadSha256Trusted: true,
1112
+ ...(ledger ? { requirementLedger: ledger } : {}),
855
1113
  });
856
1114
  return {
857
1115
  ok: true,
@@ -874,6 +1132,7 @@ export async function prepareTaskSource(input) {
874
1132
  }),
875
1133
  contractStatus: "managed",
876
1134
  warnings,
1135
+ ...ledgerResult,
877
1136
  };
878
1137
  }
879
1138
  catch (error) {
@@ -894,6 +1153,7 @@ export async function prepareTaskSource(input) {
894
1153
  `loop-agent task status ${taskId} --json`,
895
1154
  `loop-agent task status ${taskId} --details --json`,
896
1155
  ],
1156
+ ...ledgerResult,
897
1157
  error: {
898
1158
  code: error.code,
899
1159
  message: error.message,
@@ -911,6 +1171,7 @@ export async function prepareTaskSource(input) {
911
1171
  risks,
912
1172
  projected,
913
1173
  next: [],
1174
+ ...ledgerResult,
914
1175
  error: {
915
1176
  code: "INTERNAL_ERROR",
916
1177
  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}`,