@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.
- 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 +7 -3
- package/dist/workflows/dag/node-execution.js +277 -3
- 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/backend-test-dag.json +4 -3
- package/package.json +4 -3
- 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
|
|
@@ -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({
|
|
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
|
-
|
|
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:
|
|
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",
|