@tea-agent/loop-agent 0.39.0-beta.11 → 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.
- package/CHANGELOG.md +26 -0
- package/dist/application/dag/generate-task-dag.js +6 -2
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +8 -36
- package/dist/executors/dag-pi-executor.js +343 -40
- package/dist/executors/pi-executor.js +8 -4
- package/dist/executors/pi-sdk-executor.js +33 -5
- package/dist/executors/shell-executor.js +102 -32
- package/dist/governance/checks.js +1 -0
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- package/dist/task/frontend-project-capability.js +3 -1
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/worker/console/chat/pi-runtime.js +146 -5
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/routes.js +3 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +20 -2
- package/dist/worker/console/operator-actions.js +4 -3
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CXj_GnSb.js → abnfDiagram-N423BO3Z-DC863mud.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BZp6JAp7.js → arc-CftC38G9.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DfpcEuYU.js → architectureDiagram-T3A2C74G-B3PAPQCw.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-_Gf0xadb.js → blockDiagram-VBNYF7ZC-C9UDJNRv.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct2QPCmv.js → c4Diagram-5PPSVZJV--BJ76fv_.js} +1 -1
- package/dist/worker/console/static/assets/channel-CUz-Bg86.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BfklkzKl.js → chunk-2GRJ4B5K--hiIqoGp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Dy25vZJV.js → chunk-2Q5K7J3B-DgTzpIa3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BFlCRZep.js → chunk-5RXB4S5H-DIwpJziP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-op3oVxIE.js → chunk-5VM5RSS4-BJzbdUi0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C0R7rzF2.js → chunk-6Q2QTUOP-BbGouI1Z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bt44TCGy.js → chunk-GF5L2VYU-B9247w69.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-_uEE_XFx.js → chunk-JWPE2WC7-CXob_wYy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-C3TOYGZ9.js → chunk-KBJHAD2P-C6FUel1g.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cz60oBPV.js → chunk-RYQCIY6F-DGKRYKHu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-8Bik0qis.js → chunk-XXDRQBXY-kNBqNPfx.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlUacmD.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlUacmD.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C2QIOA4a.js → cose-bilkent-JH36ORCC--xmkDwfD.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CSH_yUUd.js → cynefin-VYW2F7L2-Cw5FMMuG.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BDLw2XFM.js → cynefinDiagram-MW4NZA55-CuLslt2t.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CcD9ZtF4.js → dagre-VZM6K2ZE-D9ngqrs2.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DqwtkBsI.js → diagram-7IWD3JNH-RDRSbHQp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BWVEcqsC.js → diagram-B4RE2ZJO-BgQ1P9aV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-M-WAbtQi.js → diagram-LBJQPF4R-D3WWyPtn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DKW7SixP.js → diagram-Q27KOJAE-L2k28wTR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-PHHPPtrj.js → diagram-UB23O5K3-DDNrA5Qw.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BQD-B4RX.js → ebnfDiagram-BXEA7PRR-BaRFCOdM.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BFvULb51.js → erDiagram-JOGREHBK-CM33DVVM.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Bw-aXTCb.js → flowDiagram-UKHOOZJN-CVBSjOxe.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGKw04Qx.js → ganttDiagram-PKOTCBZU-BdscewE_.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-RqKaPR-I.js → gitGraphDiagram-DS77QQ5N-HRuSZb7g.js} +1 -1
- package/dist/worker/console/static/assets/{index-B_D8rbWc.js → index-B9JJQsVK.js} +102 -72
- package/dist/worker/console/static/assets/index-rWaGv4jz.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-YO5dnzrJ.js → infoDiagram-6WML65LV-cYfHvfAR.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js → ishikawaDiagram-WSZJBQD7-CZmaoOuy.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Bszls1DD.js → journeyDiagram-NVQOT4AX-ntYijh7g.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PhzeaZ09.js → kanban-definition-27J2QSJJ-Cz3b0DeD.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BsjbDoXi.js → linear-DvGonpsP.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-0B7NnWKk.js → mermaid.core-B-3vjfyW.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BJr4Fj-q.js → mindmap-definition-FAOFIHXS-5iKxlsX3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-moC4fpGB.js → pegDiagram-VL7TDLO6-C8BUwapW.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BEw37-2c.js → pieDiagram-7S7Q4E2Y-B2rPoQQG.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq6LyasU.js → quadrantDiagram-CIZ2JOQS-jDeVUAy4.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DUCMcK0D.js → railroadDiagram-AXF67PYL-DV140Dwm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3upTZm7.js → requirementDiagram-LRYGKXZP-CnYgHtYC.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BI_gMsCW.js → sankeyDiagram-W5VNT64P-SIK3CdWw.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-YFOIRzfN.js → sequenceDiagram-SI44F4Z6-DW_vZix7.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-dOnB7UDD.js → sizeCapture-X5ZJPWSS-NBAtkg0C.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BXUniaIh.js → stateDiagram-OKZ733FA-Bi1bQxpi.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DjWKYjQ1.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-2tA4wTNu.js → swimlanes-SLNWSIFB-8PT_uP_i.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B4cdm7bH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DO0HkJXC.js → timeline-definition-Z64GVDOM-CD0ZZPk0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DvIOzixv.js → vennDiagram-T6HMQDX7-BbEgWdhK.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DXDZ0cTj.js → wardleyDiagram-T6FBY63Y-DCwPKdLl.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B32Ark0D.js → xychartDiagram-ELKLHX3M-sfC5PcNg.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/static/styles.css +9 -0
- package/dist/worker/observe/static/views/dag-inspector.js +40 -0
- package/dist/worker/observe/static/views/session-timeline.js +135 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
- package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
- package/dist/workflows/dag/backend-test-result-contract.js +4 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
- package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +118 -22
- package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -1
- package/dist/workflows/dag/frontend-writer-admission.js +13 -0
- package/dist/workflows/dag/init-hybrid.js +67 -3
- package/dist/workflows/dag/node-execution.js +384 -26
- package/dist/workflows/dag/rerun-feedback.js +135 -3
- package/dist/workflows/dag/retry-policy.js +13 -122
- package/dist/workflows/dag/types.js +6 -1
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/README.md +1 -0
- package/docs/templates/backend-test-dag.json +4 -3
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/package.json +4 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/channel-3TxJgYaH.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +0 -1
- package/dist/worker/console/static/assets/index-BdNx6fj0.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +0 -1
- 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
|
|
@@ -254,11 +311,165 @@ function planInputStrings(value) {
|
|
|
254
311
|
? value.filter((item) => typeof item === "string")
|
|
255
312
|
: [];
|
|
256
313
|
}
|
|
314
|
+
/** Input bound for the planner evidence block; protects the model input budget. */
|
|
315
|
+
const FRONTEND_PLAN_INPUT_MAX_CHARS = 12_000;
|
|
316
|
+
const FRONTEND_PLAN_INPUT_CAP_LADDER = [
|
|
317
|
+
{ text: 240, array: 40 },
|
|
318
|
+
{ text: 120, array: 20 },
|
|
319
|
+
{ text: 60, array: 10 },
|
|
320
|
+
{ text: 24, array: 4 },
|
|
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
|
+
}
|
|
257
462
|
/**
|
|
258
463
|
* Render the planner's complete-but-bounded evidence handoff from committed
|
|
259
464
|
* typed facts. It deliberately excludes upstream response prose and artifact
|
|
260
465
|
* paths: Contract and Scout have already established these facts, so Plan
|
|
261
466
|
* should decide and commit rather than spend another model turn reading them.
|
|
467
|
+
*
|
|
468
|
+
* The block is always valid JSON under the char bound: field texts shrink
|
|
469
|
+
* through a cap ladder before any fact is dropped, and the last-resort
|
|
470
|
+
* fallback keeps every requirement id (with `text: "(truncated)"`) while
|
|
471
|
+
* declaring the degradation, so the planner records targeted evidence gaps
|
|
472
|
+
* instead of receiving a silently corrupted tail.
|
|
262
473
|
*/
|
|
263
474
|
export function renderFrontendPlanInputContext(input) {
|
|
264
475
|
const committedFacts = (records) => records.flatMap((record) => record.phase === "committed" && planInputRecord(record.fact)
|
|
@@ -277,45 +488,143 @@ export function renderFrontendPlanInputContext(input) {
|
|
|
277
488
|
const targetSurface = scoutFacts
|
|
278
489
|
.filter((fact) => fact.kind === "target-surface" && fact.origin === "scout")
|
|
279
490
|
.map((fact) => ({
|
|
280
|
-
completeness:
|
|
281
|
-
entrypoint:
|
|
282
|
-
routeOrMount:
|
|
283
|
-
implementationPaths:
|
|
284
|
-
testPaths:
|
|
285
|
-
dataSource:
|
|
286
|
-
allowedPathConflicts:
|
|
287
|
-
unresolvedPaths:
|
|
491
|
+
completeness: fact.completeness,
|
|
492
|
+
entrypoint: fact.entrypoint,
|
|
493
|
+
routeOrMount: fact.routeOrMount,
|
|
494
|
+
implementationPaths: fact.implementationPaths,
|
|
495
|
+
testPaths: fact.testPaths,
|
|
496
|
+
dataSource: fact.dataSource,
|
|
497
|
+
allowedPathConflicts: fact.allowedPathConflicts,
|
|
498
|
+
unresolvedPaths: fact.unresolvedPaths,
|
|
288
499
|
}));
|
|
289
500
|
const designEvidence = scoutFacts
|
|
290
501
|
.filter((fact) => fact.kind === "design-evidence" && fact.origin === "scout")
|
|
291
502
|
.map((fact) => ({
|
|
292
|
-
source:
|
|
293
|
-
paths:
|
|
294
|
-
conflicts:
|
|
503
|
+
source: fact.source,
|
|
504
|
+
paths: fact.paths,
|
|
505
|
+
conflicts: fact.conflicts,
|
|
295
506
|
}));
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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);
|
|
521
|
+
const serializeAtCap = (cap) => JSON.stringify({
|
|
522
|
+
requirements: requirements.map((requirement) => ({
|
|
523
|
+
id: requirement.id,
|
|
524
|
+
text: planInputText(requirement.text, cap.text),
|
|
525
|
+
sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds).slice(0, cap.array),
|
|
526
|
+
})),
|
|
527
|
+
targetSurface: targetSurface.map((surface) => ({
|
|
528
|
+
completeness: planInputText(surface.completeness, 32),
|
|
529
|
+
entrypoint: planInputText(surface.entrypoint, cap.text),
|
|
530
|
+
routeOrMount: planInputText(surface.routeOrMount, cap.text),
|
|
531
|
+
implementationPaths: planInputStrings(surface.implementationPaths).slice(0, cap.array),
|
|
532
|
+
testPaths: planInputStrings(surface.testPaths).slice(0, cap.array),
|
|
533
|
+
dataSource: planInputText(surface.dataSource, cap.text),
|
|
534
|
+
allowedPathConflicts: planInputStrings(surface.allowedPathConflicts).slice(0, cap.array),
|
|
535
|
+
unresolvedPaths: planInputStrings(surface.unresolvedPaths).slice(0, cap.array),
|
|
536
|
+
})),
|
|
537
|
+
designEvidence: designEvidence.map((evidence) => ({
|
|
538
|
+
source: planInputText(evidence.source, cap.text),
|
|
539
|
+
paths: planInputStrings(evidence.paths).slice(0, cap.array),
|
|
540
|
+
conflicts: planInputStrings(evidence.conflicts).slice(0, cap.array),
|
|
541
|
+
})),
|
|
542
|
+
});
|
|
543
|
+
let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
|
|
544
|
+
for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
|
|
545
|
+
if (serialized.length <= FRONTEND_PLAN_INPUT_MAX_CHARS)
|
|
546
|
+
break;
|
|
547
|
+
serialized = serializeAtCap(cap);
|
|
548
|
+
}
|
|
549
|
+
if (serialized.length > FRONTEND_PLAN_INPUT_MAX_CHARS) {
|
|
550
|
+
// Last resort: keep every requirement id (ids are short and the plan
|
|
551
|
+
// prompt separately lists them) but drop their texts, shrink scout facts
|
|
552
|
+
// to the minimum, and declare the degradation instead of corrupting JSON.
|
|
553
|
+
let fallback = {
|
|
554
|
+
degraded: "requirement-texts-truncated",
|
|
555
|
+
requirements: requirements.map((requirement) => ({
|
|
556
|
+
id: requirement.id,
|
|
557
|
+
text: "(truncated)",
|
|
558
|
+
})),
|
|
559
|
+
targetSurface: targetSurface.map((surface) => ({
|
|
560
|
+
completeness: planInputText(surface.completeness, 32),
|
|
561
|
+
implementationPaths: planInputStrings(surface.implementationPaths).slice(0, FRONTEND_PLAN_INPUT_CAP_LADDER[3].array),
|
|
562
|
+
})),
|
|
563
|
+
designEvidence: [],
|
|
564
|
+
};
|
|
565
|
+
let bounded = JSON.stringify(fallback);
|
|
566
|
+
let keep = fallback.requirements.length;
|
|
567
|
+
while (bounded.length > FRONTEND_PLAN_INPUT_MAX_CHARS &&
|
|
568
|
+
keep > 0) {
|
|
569
|
+
keep = Math.max(0, Math.floor(keep / 2));
|
|
570
|
+
fallback = { ...fallback, requirements: fallback.requirements.slice(0, keep) };
|
|
571
|
+
bounded = JSON.stringify({
|
|
572
|
+
...fallback,
|
|
573
|
+
requirementIdsTruncated: keep < requirements.length,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
serialized = bounded;
|
|
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
|
+
];
|
|
300
590
|
return [
|
|
301
591
|
"<frontend_plan_input>",
|
|
302
592
|
"Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence.",
|
|
303
|
-
|
|
593
|
+
serialized,
|
|
304
594
|
"Do not read upstream artifacts, task sources, or repository files. If this input cannot support a decision, record a genuine evidence gap.",
|
|
305
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>",
|
|
306
600
|
].join("\n");
|
|
307
601
|
}
|
|
308
|
-
async function buildFrontendPlanInputContext(runDir) {
|
|
602
|
+
export async function buildFrontendPlanInputContext(runDir, componentSourceCitations) {
|
|
603
|
+
const contractFactsPath = path.join(runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
|
|
604
|
+
const scoutFactsPath = path.join(runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
|
|
605
|
+
let contractRecords;
|
|
606
|
+
let scoutRecords;
|
|
309
607
|
try {
|
|
310
|
-
|
|
311
|
-
readTypedEventStoreFromJsonl(
|
|
312
|
-
readTypedEventStoreFromJsonl(
|
|
608
|
+
[contractRecords, scoutRecords] = await Promise.all([
|
|
609
|
+
readTypedEventStoreFromJsonl(contractFactsPath),
|
|
610
|
+
readTypedEventStoreFromJsonl(scoutFactsPath),
|
|
313
611
|
]);
|
|
314
|
-
return renderFrontendPlanInputContext({ contractRecords, scoutRecords });
|
|
315
612
|
}
|
|
316
|
-
catch {
|
|
317
|
-
|
|
613
|
+
catch (error) {
|
|
614
|
+
// A missing/unreadable upstream store is a broken pipeline, not an
|
|
615
|
+
// evidence gap; fail before spending a model turn on a prompt that
|
|
616
|
+
// forbids reading anything.
|
|
617
|
+
throw new Error(`frontend-plan-input-unavailable: cannot read committed typed facts (${error instanceof Error ? error.message : String(error)})`);
|
|
618
|
+
}
|
|
619
|
+
const committedCount = [...contractRecords, ...scoutRecords].filter((record) => record.phase === "committed").length;
|
|
620
|
+
if (committedCount === 0) {
|
|
621
|
+
throw new Error(`frontend-plan-input-unavailable: no committed Contract/Scout facts in ${contractFactsPath} / ${scoutFactsPath}`);
|
|
318
622
|
}
|
|
623
|
+
return renderFrontendPlanInputContext({
|
|
624
|
+
contractRecords,
|
|
625
|
+
scoutRecords,
|
|
626
|
+
componentSourceCitations,
|
|
627
|
+
});
|
|
319
628
|
}
|
|
320
629
|
export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
|
|
321
630
|
if (attemptNumber <= 1)
|
|
@@ -1018,9 +1327,36 @@ export async function executeDagNode(input) {
|
|
|
1018
1327
|
return;
|
|
1019
1328
|
}
|
|
1020
1329
|
if (isFrontendPlanLadderTask(task)) {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1330
|
+
let planInputContext;
|
|
1331
|
+
try {
|
|
1332
|
+
const componentSourceCitations = await resolveComponentSourceCitations(spec, cwd);
|
|
1333
|
+
planInputContext = await buildFrontendPlanInputContext(runDir, componentSourceCitations);
|
|
1334
|
+
}
|
|
1335
|
+
catch (error) {
|
|
1336
|
+
await failBeforePrompt(error, "frontend-plan-input-unavailable");
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
prompt = `${prompt}\n\n${planInputContext}`;
|
|
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
|
+
}
|
|
1024
1360
|
}
|
|
1025
1361
|
node.resolvedSkills = resolvedSkills;
|
|
1026
1362
|
await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
|
|
@@ -1321,6 +1657,28 @@ export async function executeDagNode(input) {
|
|
|
1321
1657
|
prompt: promptRestartCandidate,
|
|
1322
1658
|
});
|
|
1323
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
|
+
}
|
|
1324
1682
|
const attemptRecord = {
|
|
1325
1683
|
attempt: attemptNumber,
|
|
1326
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:
|
|
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",
|