@tea-agent/loop-agent 0.39.0-beta.12 → 0.39.0-beta.13

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 (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/application/dag/generate-task-dag.js +6 -2
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/client-recovery.js +8 -36
  5. package/dist/executors/dag-pi-executor.js +343 -40
  6. package/dist/executors/pi-executor.js +8 -4
  7. package/dist/executors/pi-sdk-executor.js +33 -5
  8. package/dist/executors/shell-executor.js +102 -32
  9. package/dist/governance/checks.js +1 -0
  10. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  11. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  12. package/dist/shared/pi-context-pressure/env.js +58 -0
  13. package/dist/shared/pi-context-pressure/extension.js +100 -0
  14. package/dist/shared/pi-context-pressure/index.js +7 -0
  15. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  16. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  17. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  18. package/dist/task/frontend-project-capability.js +3 -1
  19. package/dist/task/source-prepare/fragment-inventory.js +4 -1
  20. package/dist/worker/console/chat/pi-runtime.js +146 -5
  21. package/dist/worker/console/chat/provider-error.js +2 -1
  22. package/dist/worker/console/chat/routes.js +3 -0
  23. package/dist/worker/console/chat/sift-bridge.js +1 -0
  24. package/dist/worker/console/dag-execution-receipt.js +20 -2
  25. package/dist/worker/console/operator-actions.js +4 -3
  26. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CXj_GnSb.js → abnfDiagram-N423BO3Z-DC863mud.js} +1 -1
  27. package/dist/worker/console/static/assets/{arc-BZp6JAp7.js → arc-CftC38G9.js} +1 -1
  28. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DfpcEuYU.js → architectureDiagram-T3A2C74G-B3PAPQCw.js} +1 -1
  29. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-_Gf0xadb.js → blockDiagram-VBNYF7ZC-C9UDJNRv.js} +1 -1
  30. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct2QPCmv.js → c4Diagram-5PPSVZJV--BJ76fv_.js} +1 -1
  31. package/dist/worker/console/static/assets/channel-CUz-Bg86.js +1 -0
  32. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BfklkzKl.js → chunk-2GRJ4B5K--hiIqoGp.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Dy25vZJV.js → chunk-2Q5K7J3B-DgTzpIa3.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BFlCRZep.js → chunk-5RXB4S5H-DIwpJziP.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-op3oVxIE.js → chunk-5VM5RSS4-BJzbdUi0.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C0R7rzF2.js → chunk-6Q2QTUOP-BbGouI1Z.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bt44TCGy.js → chunk-GF5L2VYU-B9247w69.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-_uEE_XFx.js → chunk-JWPE2WC7-CXob_wYy.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-C3TOYGZ9.js → chunk-KBJHAD2P-C6FUel1g.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cz60oBPV.js → chunk-RYQCIY6F-DGKRYKHu.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-8Bik0qis.js → chunk-XXDRQBXY-kNBqNPfx.js} +1 -1
  42. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlUacmD.js +1 -0
  43. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlUacmD.js +1 -0
  44. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C2QIOA4a.js → cose-bilkent-JH36ORCC--xmkDwfD.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CSH_yUUd.js → cynefin-VYW2F7L2-Cw5FMMuG.js} +1 -1
  46. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BDLw2XFM.js → cynefinDiagram-MW4NZA55-CuLslt2t.js} +1 -1
  47. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CcD9ZtF4.js → dagre-VZM6K2ZE-D9ngqrs2.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DqwtkBsI.js → diagram-7IWD3JNH-RDRSbHQp.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BWVEcqsC.js → diagram-B4RE2ZJO-BgQ1P9aV.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-M-WAbtQi.js → diagram-LBJQPF4R-D3WWyPtn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DKW7SixP.js → diagram-Q27KOJAE-L2k28wTR.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-UB23O5K3-PHHPPtrj.js → diagram-UB23O5K3-DDNrA5Qw.js} +1 -1
  53. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BQD-B4RX.js → ebnfDiagram-BXEA7PRR-BaRFCOdM.js} +1 -1
  54. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BFvULb51.js → erDiagram-JOGREHBK-CM33DVVM.js} +1 -1
  55. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Bw-aXTCb.js → flowDiagram-UKHOOZJN-CVBSjOxe.js} +1 -1
  56. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGKw04Qx.js → ganttDiagram-PKOTCBZU-BdscewE_.js} +1 -1
  57. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-RqKaPR-I.js → gitGraphDiagram-DS77QQ5N-HRuSZb7g.js} +1 -1
  58. package/dist/worker/console/static/assets/{index-B_D8rbWc.js → index-B9JJQsVK.js} +102 -72
  59. package/dist/worker/console/static/assets/index-rWaGv4jz.css +1 -0
  60. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-YO5dnzrJ.js → infoDiagram-6WML65LV-cYfHvfAR.js} +1 -1
  61. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js → ishikawaDiagram-WSZJBQD7-CZmaoOuy.js} +1 -1
  62. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Bszls1DD.js → journeyDiagram-NVQOT4AX-ntYijh7g.js} +1 -1
  63. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PhzeaZ09.js → kanban-definition-27J2QSJJ-Cz3b0DeD.js} +1 -1
  64. package/dist/worker/console/static/assets/{linear-BsjbDoXi.js → linear-DvGonpsP.js} +1 -1
  65. package/dist/worker/console/static/assets/{mermaid.core-0B7NnWKk.js → mermaid.core-B-3vjfyW.js} +5 -5
  66. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BJr4Fj-q.js → mindmap-definition-FAOFIHXS-5iKxlsX3.js} +1 -1
  67. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-moC4fpGB.js → pegDiagram-VL7TDLO6-C8BUwapW.js} +1 -1
  68. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BEw37-2c.js → pieDiagram-7S7Q4E2Y-B2rPoQQG.js} +1 -1
  69. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq6LyasU.js → quadrantDiagram-CIZ2JOQS-jDeVUAy4.js} +1 -1
  70. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DUCMcK0D.js → railroadDiagram-AXF67PYL-DV140Dwm.js} +1 -1
  71. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3upTZm7.js → requirementDiagram-LRYGKXZP-CnYgHtYC.js} +1 -1
  72. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BI_gMsCW.js → sankeyDiagram-W5VNT64P-SIK3CdWw.js} +1 -1
  73. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-YFOIRzfN.js → sequenceDiagram-SI44F4Z6-DW_vZix7.js} +1 -1
  74. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-dOnB7UDD.js → sizeCapture-X5ZJPWSS-NBAtkg0C.js} +1 -1
  75. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BXUniaIh.js → stateDiagram-OKZ733FA-Bi1bQxpi.js} +1 -1
  76. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DjWKYjQ1.js +1 -0
  77. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-2tA4wTNu.js → swimlanes-SLNWSIFB-8PT_uP_i.js} +2 -2
  78. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B4cdm7bH.js +8 -0
  79. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DO0HkJXC.js → timeline-definition-Z64GVDOM-CD0ZZPk0.js} +1 -1
  80. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DvIOzixv.js → vennDiagram-T6HMQDX7-BbEgWdhK.js} +1 -1
  81. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DXDZ0cTj.js → wardleyDiagram-T6FBY63Y-DCwPKdLl.js} +1 -1
  82. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B32Ark0D.js → xychartDiagram-ELKLHX3M-sfC5PcNg.js} +1 -1
  83. package/dist/worker/console/static/index.html +2 -2
  84. package/dist/worker/observe/static/styles.css +9 -0
  85. package/dist/worker/observe/static/views/dag-inspector.js +40 -0
  86. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  87. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
  88. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  89. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  90. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  91. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  92. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  93. package/dist/workflows/dag/frontend-implementation-contract.js +118 -22
  94. package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -1
  95. package/dist/workflows/dag/frontend-writer-admission.js +13 -0
  96. package/dist/workflows/dag/init-hybrid.js +7 -3
  97. package/dist/workflows/dag/node-execution.js +277 -3
  98. package/dist/workflows/dag/rerun-feedback.js +135 -3
  99. package/dist/workflows/dag/retry-policy.js +13 -122
  100. package/dist/workflows/dag/types.js +6 -1
  101. package/docs/architecture/runtime-boundaries.md +2 -1
  102. package/docs/templates/backend-test-dag.json +4 -3
  103. package/package.json +4 -3
  104. package/dist/worker/console/static/assets/channel-3TxJgYaH.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +0 -1
  106. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +0 -1
  107. package/dist/worker/console/static/assets/index-BdNx6fj0.css +0 -1
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +0 -1
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +0 -8
@@ -17,6 +17,7 @@ import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, pars
17
17
  import { getStructuredContractValidator } from "./contract-output-registry.js";
18
18
  import "./contract-validator-registrations.js";
19
19
  import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
20
+ import { parseLedgerJson } from "../../task/source-prepare/ledger.js";
20
21
  import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
21
22
  import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
22
23
  import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
@@ -221,6 +222,62 @@ export function buildContextOverflowRetryPrompt(task, basePrompt) {
221
222
  function isFrontendPlanLadderTask(task) {
222
223
  return task.id === FRONTEND_PLAN_NODE_ID;
223
224
  }
225
+ /** Contract node consumes the compiled ledger input; local predicate avoids a
226
+ * dag-pi-executor import cycle. */
227
+ function isFrontendContractTypedNode(task) {
228
+ return task.id === "frontend-contract-pi";
229
+ }
230
+ /** Resolve the source-fidelity ledger path from a v2 source binding, refusing
231
+ * any path that escapes the workspace root (mirrors dag-pi-executor). */
232
+ function resolveFrontendLedgerPath(sourceBinding, cwd) {
233
+ if (!sourceBinding || sourceBinding.schemaVersion !== 2) {
234
+ throw new Error(`frontend-contract-input-unavailable: sourceBinding v2 ledger required (got ${sourceBinding?.schemaVersion ?? "none"})`);
235
+ }
236
+ const absolutePath = path.resolve(cwd, sourceBinding.ledgerPath);
237
+ const workspaceRoot = path.resolve(cwd);
238
+ if (absolutePath !== workspaceRoot &&
239
+ !absolutePath.startsWith(`${workspaceRoot}${path.sep}`)) {
240
+ throw new Error(`frontend-contract-input-unavailable: ledger path escapes workspace root: ${sourceBinding.ledgerPath}`);
241
+ }
242
+ return absolutePath;
243
+ }
244
+ /** record_* submissions observed for the contract node in its session events. */
245
+ const FRONTEND_CONTRACT_RECORD_TOOL_NAMES_LOCAL = new Set([
246
+ "record_requirement",
247
+ "record_constraint",
248
+ "record_evidence_expectation",
249
+ "record_handoff_intent",
250
+ "record_open_question",
251
+ "record_split_proposal",
252
+ ]);
253
+ async function countContractRecordSubmissions(runDir, nodeId) {
254
+ const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
255
+ let count = 0;
256
+ try {
257
+ for (const line of (await readFile(eventsPath, "utf8")).split("\n")) {
258
+ if (!line.trim())
259
+ continue;
260
+ let event;
261
+ try {
262
+ event = JSON.parse(line);
263
+ }
264
+ catch {
265
+ continue;
266
+ }
267
+ if (event !== null &&
268
+ typeof event === "object" &&
269
+ event.type === "tool_execution_start" &&
270
+ typeof event.toolName === "string" &&
271
+ FRONTEND_CONTRACT_RECORD_TOOL_NAMES_LOCAL.has(event.toolName)) {
272
+ count += 1;
273
+ }
274
+ }
275
+ }
276
+ catch {
277
+ // Missing events file = zero submissions.
278
+ }
279
+ return count;
280
+ }
224
281
  /**
225
282
  * Read the committed plan ledger snapshot for the ladder's "no new committed
226
283
  * fact" check. The digest is over the sorted committed payload hashes, and
@@ -262,6 +319,146 @@ const FRONTEND_PLAN_INPUT_CAP_LADDER = [
262
319
  { text: 60, array: 10 },
263
320
  { text: 24, array: 4 },
264
321
  ];
322
+ /**
323
+ * Frozen requirement→PRD citation map for the plan review checklist: lets the
324
+ * model declare sourceRequirementIds whose section/line match the component
325
+ * purpose, so the runtime-derived specReference survives reviewer scrutiny
326
+ * (r12: 4 of 9 decision=new choices cited a mismatched PRD section).
327
+ */
328
+ async function resolveComponentSourceCitations(spec, cwd) {
329
+ const binding = spec.sourceBinding;
330
+ if (!binding || binding.schemaVersion !== 2 || !binding.ledgerPath)
331
+ return new Map();
332
+ const absolutePath = path.resolve(cwd, binding.ledgerPath);
333
+ const workspaceRoot = path.resolve(cwd);
334
+ if (absolutePath !== workspaceRoot &&
335
+ !absolutePath.startsWith(`${workspaceRoot}${path.sep}`))
336
+ return new Map();
337
+ try {
338
+ const ledger = JSON.parse(await readFile(absolutePath, "utf8"));
339
+ const fragmentsById = new Map((ledger.fragments ?? []).map((fragment) => [fragment.id, fragment]));
340
+ const references = new Map();
341
+ for (const requirement of ledger.canonicalRequirements ?? []) {
342
+ const id = typeof requirement.id === "string" ? requirement.id : "";
343
+ if (!id)
344
+ continue;
345
+ const citations = (requirement.sourceFragmentIds ?? [])
346
+ .map((fragmentId) => fragmentsById.get(fragmentId))
347
+ .filter((fragment) => fragment !== undefined)
348
+ .map((fragment) => ({
349
+ fragmentId: fragment.id,
350
+ path: typeof fragment.path === "string" ? fragment.path : "",
351
+ section: typeof fragment.headingPath === "string"
352
+ ? fragment.headingPath
353
+ : "",
354
+ line: typeof fragment.lineRange?.start === "number"
355
+ ? fragment.lineRange.start
356
+ : undefined,
357
+ }));
358
+ if (citations.length > 0)
359
+ references.set(id, citations);
360
+ }
361
+ return references;
362
+ }
363
+ catch {
364
+ return new Map();
365
+ }
366
+ }
367
+ /** Input bound for the contract node's compiled ledger block. */
368
+ const FRONTEND_CONTRACT_INPUT_MAX_CHARS = 12_000;
369
+ /**
370
+ * Render the contract node's complete-but-bounded ledger handoff. The
371
+ * source-fidelity ledger already extracted canonical requirements with source
372
+ * spans; the contract node confirms and commits them incrementally through
373
+ * record_requirement instead of re-reading the raw source (extreme-environment:
374
+ * a small output window cannot absorb a full source re-read).
375
+ *
376
+ * Same shape guarantees as the plan input block: always valid JSON under the
377
+ * char bound, ids never drop, texts degrade through the cap ladder.
378
+ */
379
+ export function renderFrontendContractInputContext(input) {
380
+ // Fragment bindings are ids, not prose: they are the one thing the contract
381
+ // must never lose. r6 regression — the last-resort degradation dropped
382
+ // sourceFragmentIds entirely, the model (correctly refusing to invent ids)
383
+ // committed empty bindings, and the plan compile failed the ledger-binding
384
+ // gate for every requirement. Bindings therefore bypass the cap ladder and
385
+ // every degradation level; only requirement TEXTS and fragment CONTEXT
386
+ // (path/headingPath) may degrade. Fragment context is rendered only for
387
+ // fragments actually referenced by a requirement and shrinks first.
388
+ const referencedFragmentIds = new Set(input.canonicalRequirements.flatMap((requirement) => planInputStrings(requirement.sourceFragmentIds)));
389
+ const referencedFragments = input.fragments.filter((fragment) => referencedFragmentIds.has(fragment.id));
390
+ const serializeAtCap = (cap) => JSON.stringify({
391
+ requirements: input.canonicalRequirements.map((requirement) => ({
392
+ id: requirement.id,
393
+ text: planInputText(requirement.text, cap.text),
394
+ sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds),
395
+ })),
396
+ fragments: referencedFragments.map((fragment) => ({
397
+ id: fragment.id,
398
+ path: planInputText(fragment.path, 200),
399
+ headingPath: planInputText(fragment.headingPath, 120),
400
+ lineRange: fragment.lineRange,
401
+ })),
402
+ });
403
+ let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
404
+ for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
405
+ if (serialized.length <= FRONTEND_CONTRACT_INPUT_MAX_CHARS)
406
+ break;
407
+ serialized = serializeAtCap(cap);
408
+ }
409
+ if (serialized.length > FRONTEND_CONTRACT_INPUT_MAX_CHARS) {
410
+ // Last resort: keep every requirement id AND its fragment bindings,
411
+ // degrade texts, and shrink referenced fragment context first (halve,
412
+ // then drop context fields, then drop the fragment list entirely).
413
+ // Requirement ids and sourceFragmentIds are never dropped.
414
+ let fragments = referencedFragments.map((fragment) => ({
415
+ id: fragment.id,
416
+ path: planInputText(fragment.path, 120),
417
+ }));
418
+ let requirements = input.canonicalRequirements.map((requirement) => {
419
+ const sourceFragmentIds = planInputStrings(requirement.sourceFragmentIds);
420
+ return {
421
+ id: requirement.id,
422
+ text: "(truncated)",
423
+ // Empty bindings carry no information; omit them so the payload
424
+ // stays inside the char bound when no requirement is bound.
425
+ ...(sourceFragmentIds.length > 0 ? { sourceFragmentIds } : {}),
426
+ };
427
+ });
428
+ let bounded = JSON.stringify({ degraded: "requirement-texts-truncated", requirements, fragments });
429
+ while (bounded.length > FRONTEND_CONTRACT_INPUT_MAX_CHARS && fragments.length > 0) {
430
+ fragments = fragments.slice(0, Math.floor(fragments.length / 2));
431
+ bounded = JSON.stringify({
432
+ degraded: "requirement-texts-truncated",
433
+ requirements,
434
+ fragments,
435
+ });
436
+ }
437
+ serialized = bounded;
438
+ }
439
+ return [
440
+ "<frontend_contract_input>",
441
+ "Canonical requirements extracted by the source-fidelity ledger, compiled by the runner. Treat them as the authoritative requirement inventory: confirm and commit each requirement through record_requirement (one per tool call); the ledger already binds source fragments, so do NOT re-read the raw source files.",
442
+ serialized,
443
+ "</frontend_contract_input>",
444
+ ].join("\n");
445
+ }
446
+ export async function buildFrontendContractInputContext(input) {
447
+ const ledger = parseLedgerJson(await readFile(input.ledgerPath, "utf8"));
448
+ return renderFrontendContractInputContext({
449
+ canonicalRequirements: ledger.canonicalRequirements.map((requirement) => ({
450
+ id: requirement.id,
451
+ text: requirement.text,
452
+ sourceFragmentIds: requirement.sourceFragmentIds ?? [],
453
+ })),
454
+ fragments: ledger.fragments.map((fragment) => ({
455
+ id: fragment.id,
456
+ path: fragment.path,
457
+ headingPath: fragment.headingPath,
458
+ lineRange: fragment.lineRange,
459
+ })),
460
+ });
461
+ }
265
462
  /**
266
463
  * Render the planner's complete-but-bounded evidence handoff from committed
267
464
  * typed facts. It deliberately excludes upstream response prose and artifact
@@ -307,6 +504,20 @@ export function renderFrontendPlanInputContext(input) {
307
504
  paths: fact.paths,
308
505
  conflicts: fact.conflicts,
309
506
  }));
507
+ // Reviewer-rubric scaffold: the design reviewer re-runs the design-policy
508
+ // checks on the committed facts, so publish the checklist to the producer.
509
+ // Requirements whose contract evidence expects behavioural verification are
510
+ // enumerated explicitly — those are the slots the reviewer finds missing
511
+ // when the plan models interactions ad hoc (r8/r9 findings).
512
+ const behaviorRequiredIds = requirements
513
+ .filter((requirement) => {
514
+ const fact = contractFacts.find((candidate) => candidate.kind === "requirement" &&
515
+ candidate.origin === "contract" &&
516
+ candidate.id === requirement.id);
517
+ const evidence = fact?.evidence;
518
+ return evidence?.behavior === "required";
519
+ })
520
+ .map((requirement) => requirement.id);
310
521
  const serializeAtCap = (cap) => JSON.stringify({
311
522
  requirements: requirements.map((requirement) => ({
312
523
  id: requirement.id,
@@ -364,15 +575,31 @@ export function renderFrontendPlanInputContext(input) {
364
575
  }
365
576
  serialized = bounded;
366
577
  }
578
+ const checklistLines = [
579
+ "1. Every interaction you record needs a uiComponentChoices entry whose purpose equals the interaction name, or one decision=reuse-existing choice covering behavioural interactions.",
580
+ "2. Every applicable UI state needs a purpose-matching component choice or a stylingStrategy.",
581
+ "3. Every requirement marked (behavior) below needs modelled interactions plus at least one verification target that references it.",
582
+ "4. targets.files must name the concrete deliverable files; never leave the scope broader than the frozen requirements state.",
583
+ "5. Verification targets may only reference UI states and requirements you actually recorded (the record_* tools reject unknown references).",
584
+ `Requirements requiring behavioural coverage: ${behaviorRequiredIds.length > 0 ? behaviorRequiredIds.join(", ") : "(none)"}`,
585
+ "6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency.",
586
+ ...[...input.componentSourceCitations ?? []]
587
+ .filter(([id]) => behaviorRequiredIds.includes(id))
588
+ .flatMap(([id, citations]) => citations.map((citation) => ` ${id} + ${citation.fragmentId} → ${citation.section}${citation.line ? ` (line ${citation.line})` : ""}`)),
589
+ ];
367
590
  return [
368
591
  "<frontend_plan_input>",
369
592
  "Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence.",
370
593
  serialized,
371
594
  "Do not read upstream artifacts, task sources, or repository files. If this input cannot support a decision, record a genuine evidence gap.",
372
595
  "</frontend_plan_input>",
596
+ "<plan_review_checklist>",
597
+ "The design reviewer re-runs these exact checks on the committed facts; satisfy every line before finalize_plan:",
598
+ ...checklistLines,
599
+ "</plan_review_checklist>",
373
600
  ].join("\n");
374
601
  }
375
- export async function buildFrontendPlanInputContext(runDir) {
602
+ export async function buildFrontendPlanInputContext(runDir, componentSourceCitations) {
376
603
  const contractFactsPath = path.join(runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
377
604
  const scoutFactsPath = path.join(runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
378
605
  let contractRecords;
@@ -393,7 +620,11 @@ export async function buildFrontendPlanInputContext(runDir) {
393
620
  if (committedCount === 0) {
394
621
  throw new Error(`frontend-plan-input-unavailable: no committed Contract/Scout facts in ${contractFactsPath} / ${scoutFactsPath}`);
395
622
  }
396
- return renderFrontendPlanInputContext({ contractRecords, scoutRecords });
623
+ return renderFrontendPlanInputContext({
624
+ contractRecords,
625
+ scoutRecords,
626
+ componentSourceCitations,
627
+ });
397
628
  }
398
629
  export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
399
630
  if (attemptNumber <= 1)
@@ -1098,7 +1329,8 @@ export async function executeDagNode(input) {
1098
1329
  if (isFrontendPlanLadderTask(task)) {
1099
1330
  let planInputContext;
1100
1331
  try {
1101
- planInputContext = await buildFrontendPlanInputContext(runDir);
1332
+ const componentSourceCitations = await resolveComponentSourceCitations(spec, cwd);
1333
+ planInputContext = await buildFrontendPlanInputContext(runDir, componentSourceCitations);
1102
1334
  }
1103
1335
  catch (error) {
1104
1336
  await failBeforePrompt(error, "frontend-plan-input-unavailable");
@@ -1106,6 +1338,26 @@ export async function executeDagNode(input) {
1106
1338
  }
1107
1339
  prompt = `${prompt}\n\n${planInputContext}`;
1108
1340
  }
1341
+ if (isFrontendContractTypedNode(task)) {
1342
+ // Extreme-environment input handoff: compile the ledger's canonical
1343
+ // requirements into a bounded block so the contract node never re-reads
1344
+ // the raw source (mirrors the plan node's compiled input; its tool set
1345
+ // is record_* + finalize_contract, no read tools).
1346
+ let contractInputContext;
1347
+ try {
1348
+ const ledgerPath = resolveFrontendLedgerPath(spec.sourceBinding, cwd);
1349
+ contractInputContext = await buildFrontendContractInputContext({
1350
+ ledgerPath,
1351
+ });
1352
+ prompt = `${prompt}\n\n${contractInputContext}`;
1353
+ }
1354
+ catch (error) {
1355
+ // Ledger unreadable is a broken pipeline: fail before spending a
1356
+ // model turn on a prompt that forbids reading anything.
1357
+ await failBeforePrompt(error, "frontend-contract-input-unavailable");
1358
+ return;
1359
+ }
1360
+ }
1109
1361
  node.resolvedSkills = resolvedSkills;
1110
1362
  await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
1111
1363
  let model = resolveModelForTask(task, spec.executorModels);
@@ -1405,6 +1657,28 @@ export async function executeDagNode(input) {
1405
1657
  prompt: promptRestartCandidate,
1406
1658
  });
1407
1659
  }
1660
+ // C: contract incremental-progress guard. A failed contract attempt that
1661
+ // committed zero record_* submissions is a "no-progress" failure, not an
1662
+ // opaque timeout/error: normalize it to empty-output so the retryPolicy
1663
+ // retries with the incremental-commit discipline instead of burning the
1664
+ // remaining attempts on the same stalled behavior.
1665
+ if (isFrontendContractTypedNode(task) &&
1666
+ !result.ok &&
1667
+ retryPolicy !== undefined) {
1668
+ const submissions = await countContractRecordSubmissions(runDir, nodeId);
1669
+ if (submissions === 0) {
1670
+ result = {
1671
+ ...result,
1672
+ failureCategory: "empty-output",
1673
+ stderr: [
1674
+ result.stderr,
1675
+ "contract attempt failed with zero record_* submissions; retrying with incremental-commit discipline (one record_* call per message, starting from the first tool call)",
1676
+ ]
1677
+ .filter(Boolean)
1678
+ .join("\n"),
1679
+ };
1680
+ }
1681
+ }
1408
1682
  const attemptRecord = {
1409
1683
  attempt: attemptNumber,
1410
1684
  startedAt: attemptStartedAt,
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { parseJsonReviewVerdict } from "./output-protocol.js";
5
5
  import { FRONTEND_DESIGN_REVIEW_NODE_ID, FRONTEND_REVIEW_NODE_ID, readCommittedDesignRequestFact, readCommittedReviewRequestFact, } from "./frontend-review-findings.js";
6
+ import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
6
7
  export const MAX_RERUN_FEEDBACK_CHARS = 12_000;
7
8
  const RERUN_FEEDBACK_TRUNCATION_MARKER = "\n\n...[上一轮反馈已按长度上限截断]...\n\n";
8
9
  const MAX_EVIDENCE_REFS = 8;
@@ -201,6 +202,127 @@ async function readTypedDesignRequestFact(runDir) {
201
202
  },
202
203
  };
203
204
  }
205
+ const RERUN_FEEDBACK_LINEAGE_MAX_DEPTH = 5;
206
+ const RERUN_FEEDBACK_LINEAGE_LIFECYCLES = [
207
+ "active",
208
+ "paused",
209
+ "completed",
210
+ ];
211
+ function safeLineageRunId(value) {
212
+ if (typeof value !== "string" || !value || value !== value.trim())
213
+ return undefined;
214
+ if (value === "." ||
215
+ value === ".." ||
216
+ value.includes("/") ||
217
+ value.includes("\\") ||
218
+ value.includes("\0"))
219
+ return undefined;
220
+ return value;
221
+ }
222
+ async function readCommittedTerminalKind(input) {
223
+ try {
224
+ const records = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.nodeId, input.factsFile));
225
+ for (const record of records) {
226
+ if (record.phase !== "committed")
227
+ continue;
228
+ const kind = record.fact.kind;
229
+ if (typeof kind === "string" && input.kinds.includes(kind))
230
+ return kind;
231
+ }
232
+ }
233
+ catch {
234
+ // A missing/unreadable terminal store means this run did not establish a
235
+ // usable resolution barrier; lineage lookup may continue.
236
+ }
237
+ return undefined;
238
+ }
239
+ async function readReviewTerminalKind(runDir) {
240
+ return readCommittedTerminalKind({
241
+ runDir,
242
+ nodeId: FRONTEND_REVIEW_NODE_ID,
243
+ factsFile: "review-typed-facts.jsonl",
244
+ kinds: ["request_review_changes", "approve_review"],
245
+ });
246
+ }
247
+ async function readDesignTerminalKind(runDir) {
248
+ return readCommittedTerminalKind({
249
+ runDir,
250
+ nodeId: FRONTEND_DESIGN_REVIEW_NODE_ID,
251
+ factsFile: "design-typed-facts.jsonl",
252
+ kinds: ["request_design_changes", "approve_design"],
253
+ });
254
+ }
255
+ /**
256
+ * Walk the rerun-task lineage chain (`<runDir>/.runtime/rerun-task-lineage.json`
257
+ * → parentRunId) looking for the nearest ancestor run with a committed review
258
+ * or design request fact. A nearer final-review approval resolves all older
259
+ * findings; a nearer design approval resolves older design findings while
260
+ * still allowing an older final-review request to survive. Bounded depth;
261
+ * unsafe ids, mismatched state identities, cycles, and unreadable lineage
262
+ * files terminate the walk with undefined.
263
+ */
264
+ async function readAncestorTypedRequestFact(runDir) {
265
+ let currentRunDir = runDir;
266
+ let currentRunId;
267
+ let designResolved = false;
268
+ const dagRunsRoot = path.resolve(runDir, "..", "..");
269
+ const visitedRunDirs = new Set([path.resolve(runDir)]);
270
+ for (let depth = 0; depth <= RERUN_FEEDBACK_LINEAGE_MAX_DEPTH; depth += 1) {
271
+ const reviewTerminalKind = await readReviewTerminalKind(currentRunDir);
272
+ if (currentRunId && reviewTerminalKind === "request_review_changes") {
273
+ const fact = await readTypedReviewRequestFact(currentRunDir);
274
+ if (fact)
275
+ return { ...fact, parentRunId: currentRunId };
276
+ }
277
+ if (reviewTerminalKind === "approve_review")
278
+ return undefined;
279
+ const designTerminalKind = await readDesignTerminalKind(currentRunDir);
280
+ if (currentRunId &&
281
+ !designResolved &&
282
+ designTerminalKind === "request_design_changes") {
283
+ const fact = await readTypedDesignRequestFact(currentRunDir);
284
+ if (fact)
285
+ return { ...fact, parentRunId: currentRunId };
286
+ }
287
+ if (designTerminalKind === "approve_design")
288
+ designResolved = true;
289
+ if (depth === RERUN_FEEDBACK_LINEAGE_MAX_DEPTH)
290
+ return undefined;
291
+ let parentRunId;
292
+ try {
293
+ const lineage = JSON.parse(await readFile(path.join(currentRunDir, ".runtime", "rerun-task-lineage.json"), "utf8"));
294
+ parentRunId = safeLineageRunId(lineage.parentRunId);
295
+ }
296
+ catch {
297
+ return undefined;
298
+ }
299
+ if (!parentRunId)
300
+ return undefined;
301
+ let ancestorRunDir;
302
+ for (const lifecycle of RERUN_FEEDBACK_LINEAGE_LIFECYCLES) {
303
+ const candidate = path.join(dagRunsRoot, lifecycle, parentRunId);
304
+ try {
305
+ const ancestorState = JSON.parse(await readFile(path.join(candidate, "state.json"), "utf8"));
306
+ if (ancestorState.runId === parentRunId) {
307
+ ancestorRunDir = candidate;
308
+ break;
309
+ }
310
+ }
311
+ catch {
312
+ // try next lifecycle
313
+ }
314
+ }
315
+ if (!ancestorRunDir)
316
+ return undefined;
317
+ const resolvedAncestorRunDir = path.resolve(ancestorRunDir);
318
+ if (visitedRunDirs.has(resolvedAncestorRunDir))
319
+ return undefined;
320
+ visitedRunDirs.add(resolvedAncestorRunDir);
321
+ currentRunDir = ancestorRunDir;
322
+ currentRunId = parentRunId;
323
+ }
324
+ return undefined;
325
+ }
204
326
  /**
205
327
  * Derive only the latest unresolved verify/review feedback from canonical
206
328
  * parent-run facts. Provider-only failures intentionally return undefined.
@@ -225,15 +347,25 @@ export async function deriveDagRerunFeedback(input) {
225
347
  // passHistory. Fall back to reading the typed event stores so findings
226
348
  // still flow into the next round as unresolved feedback. Prefer review;
227
349
  // if absent, fall through to the design review (a design rejection also
228
- // carries actionable findings the next plan round must consume).
350
+ // carries actionable findings the next plan round must consume). When
351
+ // the direct parent has neither because it failed before review, walk the
352
+ // rerun-task lineage chain — chained
353
+ // reruns otherwise lose the grandparent's code-review findings and the
354
+ // reviewer re-flags them as "unresolved previous-round feedback" (r18).
355
+ // A committed approval is a resolution barrier and prevents older
356
+ // findings from being resurrected.
229
357
  const typedFact = (await readTypedReviewRequestFact(input.runDir)) ??
230
- (await readTypedDesignRequestFact(input.runDir));
358
+ (await readTypedDesignRequestFact(input.runDir)) ??
359
+ (await readAncestorTypedRequestFact(input.runDir));
231
360
  if (typedFact) {
232
361
  const parentSourceBindingHash = sourceBindingHash(input.spec);
233
362
  return withDigest({
234
363
  schemaVersion: 1,
235
364
  kind: "unresolved-terminal-feedback",
236
- parentRunId: input.parentRunId,
365
+ parentRunId: "parentRunId" in typedFact &&
366
+ typeof typedFact.parentRunId === "string"
367
+ ? typedFact.parentRunId
368
+ : input.parentRunId,
237
369
  taskId: input.taskId,
238
370
  sourceNodeId: typedFact.nodeId,
239
371
  verdict: "request-revision",