@tea-agent/loop-agent 0.39.0-beta.4 → 0.39.0-beta.5
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/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +79 -12
- package/dist/workflows/dag/frontend-implementation-contract.js +6 -4
- package/dist/workflows/dag/frontend-plan-render.js +0 -3
- package/dist/workflows/dag/frontend-shadow-dual-write.js +28 -1
- package/dist/workflows/dag/frontend-typed-event-store.js +13 -0
- package/dist/workflows/dag/init-hybrid.js +8 -6
- package/dist/workflows/dag/node-execution.js +10 -0
- package/dist/workflows/dag/retry-policy.js +22 -0
- package/package.json +1 -1
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.39.0-beta.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-08-
|
|
3
|
+
"version": "0.39.0-beta.5",
|
|
4
|
+
"gitSha": "0981b80311d973603a870fd80693331cb5e48335",
|
|
5
|
+
"builtAt": "2026-08-23T11:38:56.113Z"
|
|
6
6
|
}
|
|
@@ -216,6 +216,9 @@ export const FRONTEND_PLAN_RECORD_TOOL_NAMES = [
|
|
|
216
216
|
"record_mock_api",
|
|
217
217
|
"record_design_deviation",
|
|
218
218
|
"record_dependency",
|
|
219
|
+
"record_plan_requirement",
|
|
220
|
+
"record_plan_verification_target",
|
|
221
|
+
"record_plan_evidence_gap",
|
|
219
222
|
];
|
|
220
223
|
export const FRONTEND_PLAN_TERMINAL_TOOL_NAMES = ["finalize_plan"];
|
|
221
224
|
export const FRONTEND_PLAN_ADOPT_TOOL_NAMES = ["adopt_staged_fact"];
|
|
@@ -923,16 +926,83 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
923
926
|
return planToolReceipt(result);
|
|
924
927
|
},
|
|
925
928
|
});
|
|
929
|
+
// Incremental plan payload records (one entry per call) so requirements,
|
|
930
|
+
// verification targets, evidence gaps, and implementation steps never have
|
|
931
|
+
// to be emitted as one large array inside a single finalize_plan call —
|
|
932
|
+
// they aggregate from the ledger in commit order. Mirrors the contract
|
|
933
|
+
// node's record_requirement pattern to stay within any model output budget.
|
|
934
|
+
const recordPlanRequirementTool = defineTool({
|
|
935
|
+
name: "record_plan_requirement",
|
|
936
|
+
label: "record_plan_requirement",
|
|
937
|
+
description: "Commit one plan requirement entry (origin=plan plan-requirement fact). Call once per requirement; each entry carries id, expectedOutcome, implementationTargets, verificationTargetIds, and optional evidenceGap.",
|
|
938
|
+
promptSnippet: "Commit one plan requirement entry.",
|
|
939
|
+
parameters: Type.Object({
|
|
940
|
+
entry: requirementSchema,
|
|
941
|
+
}, { additionalProperties: false }),
|
|
942
|
+
async execute(_toolCallId, params) {
|
|
943
|
+
const rawEntry = params?.entry;
|
|
944
|
+
if (!isRecordObject(rawEntry)) {
|
|
945
|
+
return planToolReceipt({
|
|
946
|
+
ok: false,
|
|
947
|
+
kind: "plan-requirement",
|
|
948
|
+
error: "record_plan_requirement requires a non-empty entry object",
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const entry = rawEntry;
|
|
952
|
+
const result = await adoptPlanFact("plan-requirement", `${attemptId}:record_plan_requirement:${randomUUID()}`, { kind: "plan-requirement", origin: "plan", entry });
|
|
953
|
+
return planToolReceipt(result);
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
const recordPlanVerificationTargetTool = defineTool({
|
|
957
|
+
name: "record_plan_verification_target",
|
|
958
|
+
label: "record_plan_verification_target",
|
|
959
|
+
description: "Commit one plan verification target entry (origin=plan plan-verification-target fact). Call once per target; entry carries id, type, commandLabel, file, symbol, requirementIds, uiStates.",
|
|
960
|
+
promptSnippet: "Commit one plan verification target entry.",
|
|
961
|
+
parameters: Type.Object({
|
|
962
|
+
entry: verificationTargetSchema,
|
|
963
|
+
}, { additionalProperties: false }),
|
|
964
|
+
async execute(_toolCallId, params) {
|
|
965
|
+
const rawEntry = params?.entry;
|
|
966
|
+
if (!isRecordObject(rawEntry)) {
|
|
967
|
+
return planToolReceipt({
|
|
968
|
+
ok: false,
|
|
969
|
+
kind: "plan-verification-target",
|
|
970
|
+
error: "record_plan_verification_target requires a non-empty entry object",
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
const entry = rawEntry;
|
|
974
|
+
const result = await adoptPlanFact("plan-verification-target", `${attemptId}:record_plan_verification_target:${randomUUID()}`, { kind: "plan-verification-target", origin: "plan", entry });
|
|
975
|
+
return planToolReceipt(result);
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
const recordPlanEvidenceGapTool = defineTool({
|
|
979
|
+
name: "record_plan_evidence_gap",
|
|
980
|
+
label: "record_plan_evidence_gap",
|
|
981
|
+
description: "Commit one plan evidence gap entry (origin=plan plan-evidence-gap fact). Call once per gap; entry carries requirementId, description, blocking.",
|
|
982
|
+
promptSnippet: "Commit one plan evidence gap entry.",
|
|
983
|
+
parameters: Type.Object({
|
|
984
|
+
entry: evidenceGapSchema,
|
|
985
|
+
}, { additionalProperties: false }),
|
|
986
|
+
async execute(_toolCallId, params) {
|
|
987
|
+
const rawEntry = params?.entry;
|
|
988
|
+
if (!isRecordObject(rawEntry)) {
|
|
989
|
+
return planToolReceipt({
|
|
990
|
+
ok: false,
|
|
991
|
+
kind: "plan-evidence-gap",
|
|
992
|
+
error: "record_plan_evidence_gap requires a non-empty entry object",
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
const entry = rawEntry;
|
|
996
|
+
const result = await adoptPlanFact("plan-evidence-gap", `${attemptId}:record_plan_evidence_gap:${randomUUID()}`, { kind: "plan-evidence-gap", origin: "plan", entry });
|
|
997
|
+
return planToolReceipt(result);
|
|
998
|
+
},
|
|
999
|
+
});
|
|
926
1000
|
const finalizePlanTool = defineTool({
|
|
927
1001
|
name: "finalize_plan",
|
|
928
1002
|
label: "finalize_plan",
|
|
929
|
-
description: "
|
|
930
|
-
promptSnippet: "
|
|
1003
|
+
description: "Commit the finalize_plan terminal. Requirements, verification targets, and evidence gaps (optional) were already committed incrementally through record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap; finalize_plan assembles them from the ledger together with these optional remaining fields, publishes the canonical editable patch on a target-surface fact, and commits the terminal. Call exactly once.",
|
|
1004
|
+
promptSnippet: "Commit the finalize_plan terminal (ledger fields + optional residualRisks / realIntegrationGap).",
|
|
931
1005
|
parameters: Type.Object({
|
|
932
|
-
requirements: Type.Array(requirementSchema),
|
|
933
|
-
verificationTargets: Type.Array(verificationTargetSchema),
|
|
934
|
-
evidenceGaps: Type.Array(evidenceGapSchema),
|
|
935
|
-
implementationSteps: optionalStringArray,
|
|
936
1006
|
residualRisks: optionalStringArray,
|
|
937
1007
|
realIntegrationGap: optionalString,
|
|
938
1008
|
}, { additionalProperties: false }),
|
|
@@ -942,12 +1012,6 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
942
1012
|
const fragment = assemblePlanPatchFromCommittedFacts(committed) ?? {};
|
|
943
1013
|
const patch = {
|
|
944
1014
|
...fragment,
|
|
945
|
-
requirements: params?.requirements ?? [],
|
|
946
|
-
verificationTargets: params?.verificationTargets ?? [],
|
|
947
|
-
evidenceGaps: params?.evidenceGaps ?? [],
|
|
948
|
-
...(params?.implementationSteps
|
|
949
|
-
? { implementationSteps: params.implementationSteps }
|
|
950
|
-
: {}),
|
|
951
1015
|
...(params?.residualRisks
|
|
952
1016
|
? { residualRisks: params.residualRisks }
|
|
953
1017
|
: {}),
|
|
@@ -1024,6 +1088,9 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1024
1088
|
recordMockApiTool,
|
|
1025
1089
|
recordDesignDeviationTool,
|
|
1026
1090
|
recordDependencyTool,
|
|
1091
|
+
recordPlanRequirementTool,
|
|
1092
|
+
recordPlanVerificationTargetTool,
|
|
1093
|
+
recordPlanEvidenceGapTool,
|
|
1027
1094
|
adoptStagedFactTool,
|
|
1028
1095
|
finalizePlanTool,
|
|
1029
1096
|
],
|
|
@@ -209,7 +209,6 @@ export function loadFrontendImplementationContractJsonSchema(startDir = path.dir
|
|
|
209
209
|
"mockApi",
|
|
210
210
|
"designEvidence",
|
|
211
211
|
"verificationTargets",
|
|
212
|
-
"evidenceGaps",
|
|
213
212
|
];
|
|
214
213
|
const actualRequired = Array.isArray(schema.required) ? schema.required : [];
|
|
215
214
|
const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
|
|
@@ -407,7 +406,7 @@ export const frontendImplementationContractSchema = z
|
|
|
407
406
|
uiStates: z.array(z.string().min(1)),
|
|
408
407
|
})
|
|
409
408
|
.strict()).min(1),
|
|
410
|
-
evidenceGaps: z.array(gap),
|
|
409
|
+
evidenceGaps: z.array(gap).optional(),
|
|
411
410
|
implementationSteps: z.array(z.string().min(1)).optional(),
|
|
412
411
|
stylingStrategy: z.string().min(1).optional(),
|
|
413
412
|
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
@@ -1687,7 +1686,7 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1687
1686
|
if (!result.success)
|
|
1688
1687
|
fail("retryable-invalid", `invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, candidateJsonSha256);
|
|
1689
1688
|
const blockingGaps = [
|
|
1690
|
-
...result.data.evidenceGaps,
|
|
1689
|
+
...(result.data.evidenceGaps ?? []),
|
|
1691
1690
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
1692
1691
|
].filter((item) => item.blocking);
|
|
1693
1692
|
if (blockingGaps.length > 0)
|
|
@@ -1696,7 +1695,7 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1696
1695
|
.join(", ")}`, candidateJsonSha256);
|
|
1697
1696
|
for (const requirementId of canonicalBinding.requirementIds)
|
|
1698
1697
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
1699
|
-
!result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
|
|
1698
|
+
!(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
|
|
1700
1699
|
fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
|
|
1701
1700
|
return {
|
|
1702
1701
|
canonical: result.data,
|
|
@@ -1728,6 +1727,9 @@ const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
|
1728
1727
|
"mock-api",
|
|
1729
1728
|
"design-deviation",
|
|
1730
1729
|
"dependency",
|
|
1730
|
+
"plan-requirement",
|
|
1731
|
+
"plan-verification-target",
|
|
1732
|
+
"plan-evidence-gap",
|
|
1731
1733
|
];
|
|
1732
1734
|
/**
|
|
1733
1735
|
* Restore the editable RFC 7386 plan patch from a committed plan ledger.
|
|
@@ -54,9 +54,6 @@ export function renderFrontendPlanMarkdown(contract) {
|
|
|
54
54
|
"## Requirement Coverage",
|
|
55
55
|
requirementsCoverage(contract),
|
|
56
56
|
"",
|
|
57
|
-
"## Implementation Steps",
|
|
58
|
-
bulletList(contract.implementationSteps) || "_(not specified)_",
|
|
59
|
-
"",
|
|
60
57
|
"## Target Files",
|
|
61
58
|
bulletList(contract.targets.files) || "_(none)_",
|
|
62
59
|
"",
|
|
@@ -232,6 +232,9 @@ export const PLAN_LEDGER_FACT_KINDS = [
|
|
|
232
232
|
"mock-api",
|
|
233
233
|
"design-deviation",
|
|
234
234
|
"dependency",
|
|
235
|
+
"plan-requirement",
|
|
236
|
+
"plan-verification-target",
|
|
237
|
+
"plan-evidence-gap",
|
|
235
238
|
];
|
|
236
239
|
function skeletonTargetFiles(skeleton) {
|
|
237
240
|
if (!isRecord(skeleton))
|
|
@@ -320,6 +323,29 @@ export function assemblePlanPatchFromCommittedFacts(records) {
|
|
|
320
323
|
const dependencyPolicy = asString(dependency?.policy);
|
|
321
324
|
if (dependencyPolicy)
|
|
322
325
|
patch.dependencyPolicy = dependencyPolicy;
|
|
326
|
+
// Requirements / verification targets / evidence gaps / implementation
|
|
327
|
+
// steps arrive as individual facts (one per record_plan_* call) so a large
|
|
328
|
+
// plan never exceeds a single model output budget. Aggregate them in
|
|
329
|
+
// ledger order.
|
|
330
|
+
const requirements = facts
|
|
331
|
+
.filter((fact) => fact.kind === "plan-requirement")
|
|
332
|
+
.map((fact) => fact.entry)
|
|
333
|
+
.filter(isRecord);
|
|
334
|
+
if (requirements.length > 0)
|
|
335
|
+
patch.requirements = requirements;
|
|
336
|
+
const verificationTargets = facts
|
|
337
|
+
.filter((fact) => fact.kind === "plan-verification-target")
|
|
338
|
+
.map((fact) => fact.entry)
|
|
339
|
+
.filter(isRecord);
|
|
340
|
+
if (verificationTargets.length > 0) {
|
|
341
|
+
patch.verificationTargets = verificationTargets;
|
|
342
|
+
}
|
|
343
|
+
const evidenceGaps = facts
|
|
344
|
+
.filter((fact) => fact.kind === "plan-evidence-gap")
|
|
345
|
+
.map((fact) => fact.entry)
|
|
346
|
+
.filter(isRecord);
|
|
347
|
+
if (evidenceGaps.length > 0)
|
|
348
|
+
patch.evidenceGaps = evidenceGaps;
|
|
323
349
|
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
324
350
|
}
|
|
325
351
|
/** Derive the 7 origin=plan ledger facts from an editable patch. Skeleton
|
|
@@ -460,7 +486,8 @@ function planLedgerFactsFromCommittedRecords(records) {
|
|
|
460
486
|
.filter((fact) => isRecord(fact) &&
|
|
461
487
|
fact.origin === "plan" &&
|
|
462
488
|
typeof fact.kind === "string" &&
|
|
463
|
-
PLAN_LEDGER_FACT_KINDS.includes(fact.kind))
|
|
489
|
+
PLAN_LEDGER_FACT_KINDS.includes(fact.kind))
|
|
490
|
+
.map((fact) => fact);
|
|
464
491
|
}
|
|
465
492
|
export async function loadCommittedPlanLedgerFacts(runDir, nodeId = "frontend-plan-pi") {
|
|
466
493
|
const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
|
|
@@ -113,6 +113,18 @@ export const contractBlockingOwnerSchema = z.enum([
|
|
|
113
113
|
* `finalize_plan` terminal fact.
|
|
114
114
|
*/
|
|
115
115
|
export const PLAN_TERMINAL_FACT_KINDS = ["finalize_plan"];
|
|
116
|
+
/**
|
|
117
|
+
* A+B: incremental payload records for `frontend-plan-pi` (requirements,
|
|
118
|
+
* verification targets, evidence gaps). Each is committed by one
|
|
119
|
+
* `record_plan_*` call so a large plan never exceeds a single model output
|
|
120
|
+
* budget; `assemblePlanPatchFromCommittedFacts` aggregates them from the
|
|
121
|
+
* ledger in commit order.
|
|
122
|
+
*/
|
|
123
|
+
export const PLAN_RECORD_FACT_KINDS = [
|
|
124
|
+
"plan-requirement",
|
|
125
|
+
"plan-verification-target",
|
|
126
|
+
"plan-evidence-gap",
|
|
127
|
+
];
|
|
116
128
|
/**
|
|
117
129
|
* A+B (AC-009): typed issue category shared by review and design change
|
|
118
130
|
* requests. The five-value enum replaces free-form issueCategory strings.
|
|
@@ -143,6 +155,7 @@ const ALL_TYPED_EVENT_FACT_KIND_VALUES = [
|
|
|
143
155
|
...FRONTEND_SHAPE_FACT_KINDS,
|
|
144
156
|
...CONTRACT_FACT_KINDS,
|
|
145
157
|
...PLAN_TERMINAL_FACT_KINDS,
|
|
158
|
+
...PLAN_RECORD_FACT_KINDS,
|
|
146
159
|
]),
|
|
147
160
|
];
|
|
148
161
|
export const typedEventFactKindSchema = z.enum(ALL_TYPED_EVENT_FACT_KIND_VALUES);
|
|
@@ -11,7 +11,7 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
|
11
11
|
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, DEFAULT_FRONTEND_SPEC_ROOTS, extractTaskSourceFrontendSpecPaths, } from "../../shared/openspec-spec.js";
|
|
12
12
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
13
13
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
14
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
14
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, FRONTEND_REVIEW_TERMINAL_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
15
15
|
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
16
16
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
17
17
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
@@ -2651,7 +2651,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2651
2651
|
'{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
|
|
2652
2652
|
"",
|
|
2653
2653
|
"### optional plan fields - GOOD (all optional; omit when absent):",
|
|
2654
|
-
'{"
|
|
2654
|
+
'{"stylingStrategy":"reuse existing design tokens","dependencyPolicy":"no new runtime deps","residualRisks":["browser a11y not-run"],"realIntegrationGap":"FE-TEST owns live HTTP"}',
|
|
2655
2655
|
"",
|
|
2656
2656
|
"### optional plan fields - BAD (present-but-empty strings are rejected):",
|
|
2657
2657
|
'{"stylingStrategy":"","dependencyPolicy":""} <-- REJECTED: optional string fields must be non-empty when present; omit them instead',
|
|
@@ -2676,8 +2676,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2676
2676
|
"- targets: files, routes, publicApiChanges",
|
|
2677
2677
|
"- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
|
|
2678
2678
|
"- verificationTargets[]: id, type, commandLabel, file, symbol, requirementIds, uiStates",
|
|
2679
|
-
"- designEvidence: source, paths, conflicts; evidenceGaps[]",
|
|
2680
|
-
"- optional:
|
|
2679
|
+
"- designEvidence: source, paths, conflicts; evidenceGaps[] (optional)",
|
|
2680
|
+
"- optional: stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
|
|
2681
2681
|
"- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
|
|
2682
2682
|
"Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
|
|
2683
2683
|
].join("\n");
|
|
@@ -2982,13 +2982,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2982
2982
|
retryOnInvalid: true,
|
|
2983
2983
|
skeleton: frontendContractSkeleton,
|
|
2984
2984
|
},
|
|
2985
|
-
outputContract: "Short Markdown narrative plus incremental typed plan record tools (record_target_surface / record_component_choice / record_state_flow / record_data_flow / record_mock_api / record_design_deviation / record_dependency) and exactly one finalize_plan terminal call. finalize_plan assembles the canonical editable patch from committed facts (requirements / verificationTargets / evidenceGaps /
|
|
2985
|
+
outputContract: "Short Markdown narrative plus incremental typed plan record tools (record_target_surface / record_component_choice / record_state_flow / record_data_flow / record_mock_api / record_design_deviation / record_dependency / record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap) and exactly one finalize_plan terminal call. finalize_plan assembles the canonical editable patch from the committed ledger facts (requirements / verificationTargets / evidenceGaps / residualRisks / realIntegrationGap). Commit requirements and verification targets incrementally — one entry per record call — so the plan never needs a single large output; evidence gaps are optional and only needed for genuine gaps. Implementation steps are NOT part of the plan — the implementer designs its own ordering. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime compiles facts ⊕ skeleton into canonical full-contract JSON for downstream review. No file writes.",
|
|
2986
2986
|
subtask_prompt: [
|
|
2987
2987
|
"Use frontend-contract-pi typed requirement facts (stable REQ/BR/AC identifiers, dispositions, evidence expectations, and frontend-test handoff intents), frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Record the plan decision ledger through the incremental record_* tools, then call finalize_plan exactly once. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
|
|
2988
|
+
"Commit requirements with record_plan_requirement (one entry per call) and verification targets with record_plan_verification_target (one entry per call). Evidence gaps are optional — call record_plan_evidence_gap only for genuine gaps. Never bundle these into finalize_plan arguments — finalize_plan only closes the plan. Splitting one entry per call keeps every single output small. Do NOT plan implementation steps: the implementer decides ordering itself.",
|
|
2988
2989
|
...(requiresOpenspecClassification ? ["Additionally consume the frontend-contract-pi openspec classification. Successfully read every required selection (including mandatory paths) and declare any used component specReference in the typed decision ledger; relevant/irrelevant selections are not forced into the ledger unless the plan actually uses them."] : []),
|
|
2989
2990
|
"The incremental record_* facts become the complete implementation plan after deterministic merge with the protected skeleton. Do not treat leftover JSON in the narrative as the compile authority.",
|
|
2990
2991
|
"Select the Mock / API strategy only through record_mock_api. Encode endpoint/fixture mapping, explicit activation, verification commands, and Real Integration Gap in schema-defined fields; productionDefaultOff comes from the protected skeleton and there is no second plan output.",
|
|
2991
|
-
"Encode
|
|
2992
|
+
"Encode target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the ledger payload. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2992
2993
|
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
2993
2994
|
"Consume the Scout target surface and design evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
|
|
2994
2995
|
"Output a short Markdown narrative, record the incremental facts, then call finalize_plan exactly once. Do NOT emit protected skeleton fields or a full contract as the authority.",
|
|
@@ -3234,6 +3235,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3234
3235
|
executor: "pi",
|
|
3235
3236
|
complexity: "HIGH",
|
|
3236
3237
|
writePolicy: "read-only",
|
|
3238
|
+
retryPolicy: FRONTEND_REVIEW_TERMINAL_RETRY_POLICY,
|
|
3237
3239
|
allowedPaths: readOnlyPaths,
|
|
3238
3240
|
forbiddenPaths,
|
|
3239
3241
|
skills: FRONTEND_REVIEW_SKILLS,
|
|
@@ -228,6 +228,16 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
228
228
|
buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
|
|
229
229
|
].join("\n");
|
|
230
230
|
}
|
|
231
|
+
if (previousFailureCategory === "review-terminal-missing") {
|
|
232
|
+
return [
|
|
233
|
+
basePrompt,
|
|
234
|
+
"",
|
|
235
|
+
"<retry_instruction>",
|
|
236
|
+
"The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
|
|
237
|
+
"Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
|
|
238
|
+
"</retry_instruction>",
|
|
239
|
+
].join("\n");
|
|
240
|
+
}
|
|
231
241
|
if (previousFailureCategory === "invalid-output" &&
|
|
232
242
|
task.structuredContractOutput &&
|
|
233
243
|
previousProtocolReason) {
|
|
@@ -25,6 +25,13 @@ export const PROTOCOL_INVALID_RETRY_CATEGORY = "protocol-invalid";
|
|
|
25
25
|
export const STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY = "invalid-output";
|
|
26
26
|
/** Provider stopReason=length truncated the response before the JSON contract completed. */
|
|
27
27
|
export const STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY = "structured-output-truncated";
|
|
28
|
+
/**
|
|
29
|
+
* The review node emitted a verdict in response text (e.g. "VERDICT: pass")
|
|
30
|
+
* but never committed the authoritative typed terminal tool call
|
|
31
|
+
* (approve_review / request_review_changes). Read-only and safe to retry with
|
|
32
|
+
* a corrected instruction; the typed terminal is the only authority.
|
|
33
|
+
*/
|
|
34
|
+
export const REVIEW_TERMINAL_MISSING_RETRY_CATEGORY = "review-terminal-missing";
|
|
28
35
|
/** Retry only a proven no-op from an explicitly opt-in bounded Pi writer. */
|
|
29
36
|
export const WRITER_EMPTY_DIFF_RETRY_CATEGORY = "writer-empty-diff";
|
|
30
37
|
/** Retry when a backend-test writer finished but Completeness Gate found missing/broken targets. */
|
|
@@ -57,6 +64,7 @@ export const ALL_DAG_RETRY_CATEGORIES = [
|
|
|
57
64
|
WRITER_EMPTY_DIFF_RETRY_CATEGORY,
|
|
58
65
|
INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
|
|
59
66
|
WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
|
|
67
|
+
REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
|
|
60
68
|
];
|
|
61
69
|
const RETRY_SAFE_PI_ROLES = new Set([
|
|
62
70
|
"planner",
|
|
@@ -143,6 +151,20 @@ export const PROTOCOL_AWARE_PI_RETRY_POLICY = {
|
|
|
143
151
|
...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
144
152
|
retryCategories: [...PROTOCOL_AWARE_DAG_RETRY_CATEGORIES],
|
|
145
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* frontend-review-pi retry policy. The review verdict must be committed
|
|
156
|
+
* through the typed terminal tools (approve_review / request_review_changes);
|
|
157
|
+
* a model that only echoes "VERDICT: pass" text fails as
|
|
158
|
+
* review-terminal-missing. That is read-only and safe to retry with a
|
|
159
|
+
* corrected instruction so a single model slip does not burn the whole run.
|
|
160
|
+
*/
|
|
161
|
+
export const FRONTEND_REVIEW_TERMINAL_RETRY_POLICY = {
|
|
162
|
+
...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
163
|
+
retryCategories: [
|
|
164
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
165
|
+
REVIEW_TERMINAL_MISSING_RETRY_CATEGORY,
|
|
166
|
+
],
|
|
167
|
+
};
|
|
146
168
|
/**
|
|
147
169
|
* The sole writer retry policy for requireChangedFiles writers. It is
|
|
148
170
|
* intentionally not included in any read-only default: a writer may retry only
|