@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.2
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 +18 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +762 -252
- package/dist/executors/pi-executor.js +20 -1
- package/dist/executors/pi-sdk-executor.js +64 -1
- package/dist/executors/shell-executor.js +5 -2
- package/dist/shared/frontend-execution-policy.js +22 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +26 -7
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/relations.js +2 -0
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +260 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +746 -265
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +48 -10
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +9 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-scopes.js +117 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/init-hybrid.js +16 -10
- package/dist/workflows/dag/node-execution.js +32 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/package.json +1 -1
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +2 -2
- package/skills/frontend-plan/references/decision-contract.md +18 -5
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { collectFrontendExecutionGroups, frontendExecutionSchema } from "../workflows/dag/frontend-execution-groups.js";
|
|
2
|
+
import { FRONTEND_SCOPE_TARGET_BYTES, packFrontendInputUnits, parseFrontendInputBlock, projectFrontendContractPrompt, projectFrontendInputScope } from "../workflows/dag/frontend-input-projection.js";
|
|
3
|
+
import { createDurableFrontendTools } from "../workflows/dag/frontend-durable-tools.js";
|
|
4
|
+
import { createFrontendReviewScopeProtocol, loadFrontendReviewScopes } from "../workflows/dag/frontend-review-scopes.js";
|
|
5
|
+
import { observeFrontendSession } from "../workflows/dag/frontend-session-budget.js";
|
|
1
6
|
import path from "node:path";
|
|
2
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
8
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -454,6 +459,7 @@ export const FRONTEND_CONTRACT_RECORD_TOOL_NAMES = [
|
|
|
454
459
|
"record_split_proposal",
|
|
455
460
|
"record_ui_state",
|
|
456
461
|
"record_required_deliverables",
|
|
462
|
+
"complete_contract_scope",
|
|
457
463
|
];
|
|
458
464
|
export const FRONTEND_CONTRACT_TERMINAL_TOOL_NAMES = [
|
|
459
465
|
"finalize_contract",
|
|
@@ -471,9 +477,11 @@ export const FRONTEND_PLAN_RECORD_TOOL_NAMES = [
|
|
|
471
477
|
"record_state_flow",
|
|
472
478
|
"record_data_flow",
|
|
473
479
|
"record_mock_api",
|
|
480
|
+
"record_mock_endpoint",
|
|
474
481
|
"record_design_deviation",
|
|
475
482
|
"record_dependency",
|
|
476
483
|
"record_plan_requirement",
|
|
484
|
+
"record_plan_group_coverage",
|
|
477
485
|
"record_plan_verification_target",
|
|
478
486
|
"record_plan_evidence_gap",
|
|
479
487
|
];
|
|
@@ -548,6 +556,7 @@ export function resolveDagPiToolNames(task) {
|
|
|
548
556
|
if (isFrontendReviewTypedTerminalNode(task)) {
|
|
549
557
|
return [
|
|
550
558
|
...DAG_PI_READONLY_TOOLS,
|
|
559
|
+
"record_review_finding",
|
|
551
560
|
"approve_review",
|
|
552
561
|
"request_review_changes",
|
|
553
562
|
];
|
|
@@ -555,6 +564,7 @@ export function resolveDagPiToolNames(task) {
|
|
|
555
564
|
if (isFrontendDesignTypedTerminalNode(task)) {
|
|
556
565
|
return [
|
|
557
566
|
...DAG_PI_READONLY_TOOLS,
|
|
567
|
+
"record_design_finding",
|
|
558
568
|
"approve_design",
|
|
559
569
|
"request_design_changes",
|
|
560
570
|
];
|
|
@@ -759,21 +769,35 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
759
769
|
]);
|
|
760
770
|
const { approveReviewFactSchema, readCommittedEvents, requestReviewChangesFactSchema, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
761
771
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
762
|
-
|
|
772
|
+
let store = input.store;
|
|
763
773
|
const attemptId = input.attemptId;
|
|
764
774
|
const findingSchema = Type.Object({
|
|
765
|
-
severity: Type.
|
|
766
|
-
|
|
767
|
-
}),
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
issue: Type.String({}),
|
|
771
|
-
requiredChange: Type.Optional(Type.String({})),
|
|
775
|
+
severity: Type.Enum({ Critical: "Critical", Important: "Important", Minor: "Minor", Info: "Info" }),
|
|
776
|
+
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
777
|
+
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
778
|
+
issue: Type.String({ minLength: 1 }),
|
|
779
|
+
requiredChange: Type.Optional(Type.String({ minLength: 1 })),
|
|
772
780
|
}, { additionalProperties: false });
|
|
781
|
+
const scopeProtocol = createFrontendReviewScopeProtocol({ phase: "review", inventory: input.inventory, getStore: () => store, attemptId });
|
|
782
|
+
const savedFindings = () => [...new Map(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "review-finding").map(r => [r.fact.id, r.fact.finding])).values()];
|
|
783
|
+
const allFindings = (direct) => [...new Map([...savedFindings(), ...(Array.isArray(direct) ? direct : [])].map(finding => [JSON.stringify(finding), finding])).values()];
|
|
784
|
+
const recordFindingTool = defineTool({
|
|
785
|
+
name: "record_review_finding", label: "record_review_finding",
|
|
786
|
+
description: "Save one finding with a stable id. Submit findings incrementally, then finalize without repeating the findings array. Saved blocking findings cannot be omitted from approval; correct a finding explicitly with replace:true.",
|
|
787
|
+
parameters: Type.Object({ id: Type.String({ minLength: 1 }), finding: findingSchema }, { additionalProperties: false }),
|
|
788
|
+
async execute(_callId, params) {
|
|
789
|
+
const fact = { kind: "review-finding", id: params.id, finding: params.finding };
|
|
790
|
+
const requestId = `${attemptId}:finding:${randomUUID()}`;
|
|
791
|
+
const staged = stageTypedEventFact({ store, requestId, attemptId, fact });
|
|
792
|
+
const adopted = await adoptTypedEventFact({ store, requestId, attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
|
|
793
|
+
const details = { ok: true, eventId: adopted.eventId, revision: adopted.revision };
|
|
794
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
795
|
+
},
|
|
796
|
+
});
|
|
773
797
|
const approveParameters = Type.Object({
|
|
774
|
-
findings: Type.Array(findingSchema, {
|
|
798
|
+
findings: Type.Optional(Type.Array(findingSchema, {
|
|
775
799
|
description: "Optional informational findings (Minor/Info only; no Critical/Important on approval)",
|
|
776
|
-
}),
|
|
800
|
+
})),
|
|
777
801
|
}, { additionalProperties: false });
|
|
778
802
|
const requestParameters = Type.Object({
|
|
779
803
|
issueCategory: Type.Enum({
|
|
@@ -783,16 +807,17 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
783
807
|
"contract-requirement-gap": "contract-requirement-gap",
|
|
784
808
|
"unknown": "unknown",
|
|
785
809
|
}, { description: "Typed issue category (five-value enum)" }),
|
|
786
|
-
evidenceRefs: Type.Array(Type.String({}), {
|
|
787
|
-
description: "Evidence refs (paths or artifact ids); at least one",
|
|
788
|
-
}),
|
|
789
|
-
findings: Type.Array(findingSchema, {
|
|
790
|
-
description: "At least one finding",
|
|
810
|
+
evidenceRefs: Type.Array(Type.String({ minLength: 1 }), {
|
|
811
|
+
description: "Evidence refs (paths or artifact ids); at least one", minItems: 1,
|
|
791
812
|
}),
|
|
813
|
+
findings: Type.Optional(Type.Array(findingSchema, {
|
|
814
|
+
description: "At least one finding", minItems: 1,
|
|
815
|
+
})),
|
|
792
816
|
}, { additionalProperties: false });
|
|
793
817
|
async function adoptReviewFact(kind, fact) {
|
|
794
818
|
const requestId = randomUUID();
|
|
795
819
|
try {
|
|
820
|
+
scopeProtocol.assertComplete();
|
|
796
821
|
const parsed = kind === "approve_review"
|
|
797
822
|
? approveReviewFactSchema.parse(fact)
|
|
798
823
|
: requestReviewChangesFactSchema.parse(fact);
|
|
@@ -854,7 +879,7 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
854
879
|
return adoptReviewFact("approve_review", {
|
|
855
880
|
kind: "approve_review",
|
|
856
881
|
verdict: "approve_review",
|
|
857
|
-
findings: params?.findings
|
|
882
|
+
findings: allFindings(params?.findings),
|
|
858
883
|
});
|
|
859
884
|
},
|
|
860
885
|
});
|
|
@@ -870,17 +895,15 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
870
895
|
verdict: "request_review_changes",
|
|
871
896
|
issueCategory: params?.issueCategory,
|
|
872
897
|
evidenceRefs: params?.evidenceRefs,
|
|
873
|
-
findings: params?.findings,
|
|
898
|
+
findings: allFindings(params?.findings),
|
|
874
899
|
});
|
|
875
900
|
},
|
|
876
901
|
});
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
},
|
|
883
|
-
};
|
|
902
|
+
const durable = await createDurableFrontendTools({
|
|
903
|
+
file: path.join(input.runDir, input.nodeId, "review-typed-facts.jsonl"), attemptId, store: input.store,
|
|
904
|
+
binding: { inputDigest: input.inputDigest, scopeDigest: input.inventory?.digest }, setWorkingStore: next => { store = next; }, tools: [...scopeProtocol.customTools, recordFindingTool, approveReviewTool, requestReviewChangesTool], validateRestored: async () => { await input.inventory?.validate(); },
|
|
905
|
+
});
|
|
906
|
+
return { ...durable, scopeProtocol };
|
|
884
907
|
}
|
|
885
908
|
/**
|
|
886
909
|
* M8: build the two committed typed design terminal tools (approve_design /
|
|
@@ -897,21 +920,35 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
897
920
|
]);
|
|
898
921
|
const { approveDesignFactSchema, readCommittedEvents, requestDesignChangesFactSchema, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
899
922
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
900
|
-
|
|
923
|
+
let store = input.store;
|
|
901
924
|
const attemptId = input.attemptId;
|
|
902
925
|
const findingSchema = Type.Object({
|
|
903
|
-
severity: Type.
|
|
904
|
-
|
|
905
|
-
}),
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
issue: Type.String({}),
|
|
909
|
-
requiredChange: Type.Optional(Type.String({})),
|
|
926
|
+
severity: Type.Enum({ Critical: "Critical", Important: "Important", Minor: "Minor", Info: "Info" }),
|
|
927
|
+
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
928
|
+
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
929
|
+
issue: Type.String({ minLength: 1 }),
|
|
930
|
+
requiredChange: Type.Optional(Type.String({ minLength: 1 })),
|
|
910
931
|
}, { additionalProperties: false });
|
|
932
|
+
const scopeProtocol = createFrontendReviewScopeProtocol({ phase: "design", inventory: input.inventory, getStore: () => store, attemptId });
|
|
933
|
+
const savedFindings = () => [...new Map(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "design-finding").map(r => [r.fact.id, r.fact.finding])).values()];
|
|
934
|
+
const allFindings = (direct) => [...new Map([...savedFindings(), ...(Array.isArray(direct) ? direct : [])].map(finding => [JSON.stringify(finding), finding])).values()];
|
|
935
|
+
const recordFindingTool = defineTool({
|
|
936
|
+
name: "record_design_finding", label: "record_design_finding",
|
|
937
|
+
description: "Save one finding with a stable id. Submit findings incrementally, then finalize without repeating the findings array. Saved blocking findings cannot be omitted from approval; correct a finding explicitly with replace:true.",
|
|
938
|
+
parameters: Type.Object({ id: Type.String({ minLength: 1 }), finding: findingSchema }, { additionalProperties: false }),
|
|
939
|
+
async execute(_callId, params) {
|
|
940
|
+
const fact = { kind: "design-finding", id: params.id, finding: params.finding };
|
|
941
|
+
const requestId = `${attemptId}:finding:${randomUUID()}`;
|
|
942
|
+
const staged = stageTypedEventFact({ store, requestId, attemptId, fact });
|
|
943
|
+
const adopted = await adoptTypedEventFact({ store, requestId, attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
|
|
944
|
+
const details = { ok: true, eventId: adopted.eventId, revision: adopted.revision };
|
|
945
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
946
|
+
},
|
|
947
|
+
});
|
|
911
948
|
const approveParameters = Type.Object({
|
|
912
|
-
findings: Type.Array(findingSchema, {
|
|
949
|
+
findings: Type.Optional(Type.Array(findingSchema, {
|
|
913
950
|
description: "Optional informational findings (Minor/Info only; no Critical/Important on approval)",
|
|
914
|
-
}),
|
|
951
|
+
})),
|
|
915
952
|
}, { additionalProperties: false });
|
|
916
953
|
const requestParameters = Type.Object({
|
|
917
954
|
issueCategory: Type.Enum({
|
|
@@ -921,16 +958,17 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
921
958
|
"contract-requirement-gap": "contract-requirement-gap",
|
|
922
959
|
"unknown": "unknown",
|
|
923
960
|
}, { description: "Typed issue category (five-value enum)" }),
|
|
924
|
-
evidenceRefs: Type.Array(Type.String({}), {
|
|
925
|
-
description: "Evidence refs (paths or artifact ids); at least one",
|
|
926
|
-
}),
|
|
927
|
-
findings: Type.Array(findingSchema, {
|
|
928
|
-
description: "At least one finding",
|
|
961
|
+
evidenceRefs: Type.Array(Type.String({ minLength: 1 }), {
|
|
962
|
+
description: "Evidence refs (paths or artifact ids); at least one", minItems: 1,
|
|
929
963
|
}),
|
|
964
|
+
findings: Type.Optional(Type.Array(findingSchema, {
|
|
965
|
+
description: "At least one finding", minItems: 1,
|
|
966
|
+
})),
|
|
930
967
|
}, { additionalProperties: false });
|
|
931
968
|
async function adoptDesignFact(kind, fact) {
|
|
932
969
|
const requestId = randomUUID();
|
|
933
970
|
try {
|
|
971
|
+
scopeProtocol.assertComplete();
|
|
934
972
|
const parsed = kind === "approve_design"
|
|
935
973
|
? approveDesignFactSchema.parse(fact)
|
|
936
974
|
: requestDesignChangesFactSchema.parse(fact);
|
|
@@ -992,7 +1030,7 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
992
1030
|
return adoptDesignFact("approve_design", {
|
|
993
1031
|
kind: "approve_design",
|
|
994
1032
|
verdict: "approve_design",
|
|
995
|
-
findings: params?.findings
|
|
1033
|
+
findings: allFindings(params?.findings),
|
|
996
1034
|
});
|
|
997
1035
|
},
|
|
998
1036
|
});
|
|
@@ -1008,17 +1046,15 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
1008
1046
|
verdict: "request_design_changes",
|
|
1009
1047
|
issueCategory: params?.issueCategory,
|
|
1010
1048
|
evidenceRefs: params?.evidenceRefs,
|
|
1011
|
-
findings: params?.findings,
|
|
1049
|
+
findings: allFindings(params?.findings),
|
|
1012
1050
|
});
|
|
1013
1051
|
},
|
|
1014
1052
|
});
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
},
|
|
1021
|
-
};
|
|
1053
|
+
const durable = await createDurableFrontendTools({
|
|
1054
|
+
file: path.join(input.runDir, input.nodeId, "design-typed-facts.jsonl"), attemptId, store: input.store,
|
|
1055
|
+
binding: { inputDigest: input.inputDigest, scopeDigest: input.inventory?.digest }, setWorkingStore: next => { store = next; }, tools: [...scopeProtocol.customTools, recordFindingTool, approveDesignTool, requestDesignChangesTool], validateRestored: async () => { await input.inventory?.validate(); },
|
|
1056
|
+
});
|
|
1057
|
+
return { ...durable, scopeProtocol };
|
|
1022
1058
|
}
|
|
1023
1059
|
/**
|
|
1024
1060
|
* Source fidelity ledger (AC-005/AC-006): load the contract node's committed
|
|
@@ -1063,6 +1099,8 @@ async function loadContractRequirementInheritance(runDir) {
|
|
|
1063
1099
|
: undefined;
|
|
1064
1100
|
if (sourceFragmentIds || sourceRefs) {
|
|
1065
1101
|
byId.set(id, {
|
|
1102
|
+
...(typeof recordFact.text === "string" ? { text: recordFact.text } : {}),
|
|
1103
|
+
...(recordFact.execution !== undefined ? { execution: frontendExecutionSchema.parse(recordFact.execution) } : {}),
|
|
1066
1104
|
...(sourceFragmentIds ? { sourceFragmentIds } : {}),
|
|
1067
1105
|
...(sourceRefs ? { sourceRefs } : {}),
|
|
1068
1106
|
});
|
|
@@ -1228,26 +1266,12 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1228
1266
|
const { loadTypedEventStore, readCommittedEvents, writeTypedEventStoreJsonl, } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
1229
1267
|
const { adoptStagedFact, adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
1230
1268
|
const { assemblePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
1231
|
-
|
|
1269
|
+
let store = input.store;
|
|
1232
1270
|
const attemptId = input.attemptId;
|
|
1233
1271
|
let activeRequirementScope = [];
|
|
1234
1272
|
const scopedRequirementIds = () => [...activeRequirementScope];
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
// tools; otherwise the first flush of a retry can overwrite facts that the
|
|
1238
|
-
// previous attempt had already committed. The on-disk file contains only
|
|
1239
|
-
// committed records, so loading it is also fail-closed with respect to
|
|
1240
|
-
// staged/quarantined facts.
|
|
1241
|
-
const persisted = await loadTypedEventStore(path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"));
|
|
1242
|
-
if (persisted.records.length > 0) {
|
|
1243
|
-
const existingEventIds = new Set(store.records.map((record) => record.eventId));
|
|
1244
|
-
for (const record of persisted.records) {
|
|
1245
|
-
if (!existingEventIds.has(record.eventId)) {
|
|
1246
|
-
store.records.push(record);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
store.revision = Math.max(store.revision, persisted.revision);
|
|
1250
|
-
}
|
|
1273
|
+
const contractInheritance = await loadContractRequirementInheritance(input.runDir);
|
|
1274
|
+
const executionGroups = collectFrontendExecutionGroups([...contractInheritance].map(([id, r]) => ({ id, ...r })));
|
|
1251
1275
|
const stringArray = Type.Array(Type.String({}));
|
|
1252
1276
|
const optionalString = Type.Optional(Type.String({}));
|
|
1253
1277
|
const optionalStringArray = Type.Optional(stringArray);
|
|
@@ -1282,9 +1306,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1282
1306
|
verificationTargetIds: stringArray,
|
|
1283
1307
|
}, { additionalProperties: false });
|
|
1284
1308
|
const mockEndpointSchema = Type.Object({
|
|
1285
|
-
method: Type.
|
|
1286
|
-
description: "GET | POST | PUT | PATCH | DELETE | HEAD | OPTIONS",
|
|
1287
|
-
}),
|
|
1309
|
+
method: Type.Enum({ GET: "GET", POST: "POST", PUT: "PUT", PATCH: "PATCH", DELETE: "DELETE", HEAD: "HEAD", OPTIONS: "OPTIONS" }),
|
|
1288
1310
|
path: Type.String({}),
|
|
1289
1311
|
fixture: optionalString,
|
|
1290
1312
|
consumer: optionalString,
|
|
@@ -1346,9 +1368,9 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1346
1368
|
specReference: Type.Optional(Type.Object({
|
|
1347
1369
|
path: Type.String({}),
|
|
1348
1370
|
section: Type.String({}),
|
|
1349
|
-
line: Type.Optional(Type.
|
|
1371
|
+
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
1350
1372
|
}, { additionalProperties: false })),
|
|
1351
|
-
rationale: Type.String({}),
|
|
1373
|
+
rationale: Type.Optional(Type.String({ minLength: 1 })),
|
|
1352
1374
|
covers: Type.Optional(Type.Array(Type.String({}), {
|
|
1353
1375
|
description: "UI state and/or interaction names this single component choice covers (one choice may cover many ids).",
|
|
1354
1376
|
})),
|
|
@@ -1805,13 +1827,14 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1805
1827
|
label: "record_data_flow",
|
|
1806
1828
|
description: "Record interaction/endpoint data flow as an origin=plan data-flow fact. Example: {\"interactions\": [\"<interaction name>\"], \"endpoints\": [\"GET <path>\"]}",
|
|
1807
1829
|
promptSnippet: "Record the plan data-flow fact.",
|
|
1808
|
-
parameters: Type.Object({ interactions: stringArray, endpoints: stringArray }, { additionalProperties: false }),
|
|
1830
|
+
parameters: Type.Object({ interactions: stringArray, endpoints: stringArray, replace: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
1809
1831
|
async execute(_toolCallId, params) {
|
|
1832
|
+
const previous = params.replace ? undefined : readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "data-flow").at(-1)?.fact;
|
|
1810
1833
|
const result = await adoptPlanFact("data-flow", `${attemptId}:record_data_flow:${randomUUID()}`, {
|
|
1811
1834
|
kind: "data-flow",
|
|
1812
1835
|
origin: "plan",
|
|
1813
|
-
interactions: stringList(params?.interactions),
|
|
1814
|
-
endpoints: stringList(params?.endpoints),
|
|
1836
|
+
interactions: [...new Set([...stringList(previous?.interactions), ...stringList(params?.interactions)])],
|
|
1837
|
+
endpoints: [...new Set([...stringList(previous?.endpoints), ...stringList(params?.endpoints)])],
|
|
1815
1838
|
});
|
|
1816
1839
|
return planToolReceipt(result);
|
|
1817
1840
|
},
|
|
@@ -1839,6 +1862,16 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1839
1862
|
return planToolReceipt(result);
|
|
1840
1863
|
},
|
|
1841
1864
|
});
|
|
1865
|
+
const recordMockEndpointTool = defineTool({
|
|
1866
|
+
name: "record_mock_endpoint", label: "record_mock_endpoint",
|
|
1867
|
+
description: "Record one Mock/API endpoint. First record_mock_api with the policy and endpoints: []; then submit each endpoint separately. Never regenerate the whole endpoint collection. Use replace:true to revise an existing method/path, or replace:true plus remove:true to withdraw it.",
|
|
1868
|
+
parameters: Type.Object({ endpoint: mockEndpointSchema, remove: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
1869
|
+
async execute(_callId, params) {
|
|
1870
|
+
if (!readCommittedEvents(store, attemptId).some(r => r.fact.kind === "mock-api"))
|
|
1871
|
+
return planToolReceipt({ ok: false, kind: "mock-endpoint", code: "MOCK_POLICY_MISSING", error: "Record the mock policy before its endpoints" });
|
|
1872
|
+
return planToolReceipt(await adoptPlanFact("mock-endpoint", `${attemptId}:endpoint:${randomUUID()}`, { kind: "mock-endpoint", origin: "plan", endpoint: params.endpoint, ...(params.remove ? { removed: true } : {}) }));
|
|
1873
|
+
},
|
|
1874
|
+
});
|
|
1842
1875
|
const recordDesignDeviationTool = defineTool({
|
|
1843
1876
|
name: "record_design_deviation",
|
|
1844
1877
|
label: "record_design_deviation",
|
|
@@ -1914,6 +1947,8 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1914
1947
|
// canonical-coverage gate rejects with no in-node cure. Reject here
|
|
1915
1948
|
// and name the allowed ids.
|
|
1916
1949
|
const id = typeof entry.id === "string" ? entry.id : "";
|
|
1950
|
+
if (activeRequirementScope.length && !activeRequirementScope.includes(id))
|
|
1951
|
+
return planToolReceipt({ ok: false, kind: "plan-requirement", code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: `Requirement ${id} is outside this session` });
|
|
1917
1952
|
if (id &&
|
|
1918
1953
|
input.requirementIds &&
|
|
1919
1954
|
input.requirementIds.length > 0 &&
|
|
@@ -1963,6 +1998,29 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1963
1998
|
return planToolReceipt(result);
|
|
1964
1999
|
},
|
|
1965
2000
|
});
|
|
2001
|
+
const recordPlanGroupCoverageTool = defineTool({
|
|
2002
|
+
name: "record_plan_group_coverage", label: "record_plan_group_coverage",
|
|
2003
|
+
description: "Submit shared implementation/verification references for one declared execution group. Runtime expands to every canonical member and retains its full outcome and source bindings. A shared VT must actually verify each independent condition. Use per-requirement records for differences; never create UI for constraints or exclusions. replace:true explicitly revises the group.",
|
|
2004
|
+
parameters: Type.Object({ id: Type.String({ minLength: 1 }), implementationTargets: stringArray, verificationTargetIds: stringArray, replace: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
2005
|
+
async execute(callId, params, signal, onUpdate, ctx) {
|
|
2006
|
+
const group = executionGroups.find(g => g.id === params.id && g.kind !== "unclassified");
|
|
2007
|
+
if (!group || (activeRequirementScope.length && group.requirementIds.some(id => !activeRequirementScope.includes(id))))
|
|
2008
|
+
return planToolReceipt({ ok: false, kind: "plan-requirement", code: "EXECUTION_GROUP_SCOPE_INVALID", error: "A known complete group must be present in this session; submit individual member records when the group spans scopes" });
|
|
2009
|
+
let last;
|
|
2010
|
+
for (const id of group.requirementIds) {
|
|
2011
|
+
const existing = readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "plan-requirement" && r.fact.entry?.id === id).at(-1)?.fact.entry;
|
|
2012
|
+
if (existing && !params.replace) {
|
|
2013
|
+
if (JSON.stringify(existing.implementationTargets) !== JSON.stringify(params.implementationTargets) || JSON.stringify(existing.verificationTargetIds) !== JSON.stringify(params.verificationTargetIds))
|
|
2014
|
+
return planToolReceipt({ ok: false, kind: "plan-requirement", code: "FACT_IDENTITY_CONFLICT", error: `${id}: existing coverage differs; use replace:true to revise explicitly` });
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
last = await recordPlanRequirementTool.execute(`${callId}:${id}`, { entry: { id, implementationTargets: params.implementationTargets, verificationTargetIds: params.verificationTargetIds }, ...(params.replace ? { replace: true } : {}) }, signal, onUpdate, ctx);
|
|
2018
|
+
if (!last.details?.ok)
|
|
2019
|
+
return last;
|
|
2020
|
+
}
|
|
2021
|
+
return planToolReceipt(await adoptPlanFact("plan-group-coverage", `${attemptId}:group:${randomUUID()}`, { kind: "plan-group-coverage", origin: "plan", id: group.id, requirementIds: group.requirementIds }));
|
|
2022
|
+
},
|
|
2023
|
+
});
|
|
1966
2024
|
const recordPlanVerificationTargetTool = defineTool({
|
|
1967
2025
|
name: "record_plan_verification_target",
|
|
1968
2026
|
label: "record_plan_verification_target",
|
|
@@ -2182,7 +2240,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2182
2240
|
const finalizePlanTool = defineTool({
|
|
2183
2241
|
name: "finalize_plan",
|
|
2184
2242
|
label: "finalize_plan",
|
|
2185
|
-
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.
|
|
2243
|
+
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. A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
|
|
2186
2244
|
promptSnippet: "Commit the finalize_plan terminal (ledger fields + optional residualRisks / realIntegrationGap).",
|
|
2187
2245
|
parameters: Type.Object({
|
|
2188
2246
|
residualRisks: optionalStringArray,
|
|
@@ -2197,6 +2255,9 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2197
2255
|
// when the plan did not re-declare them. The contract node is the
|
|
2198
2256
|
// sole synthesis point; the plan inherits by requirement id.
|
|
2199
2257
|
const contractInheritance = await loadContractRequirementInheritance(input.runDir);
|
|
2258
|
+
const missingData = collectFrontendPlanPhaseMissingFacts({ phase: "global-mock-data", requirementIds: input.requirementIds ?? [...contractInheritance.keys()], committedFacts: committed }).filter(f => f.kind === "data-flow");
|
|
2259
|
+
if (missingData.length)
|
|
2260
|
+
return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_DATA_FLOW_INCOMPLETE", error: missingData.map(f => f.reason).join("; ") });
|
|
2200
2261
|
const fragment = assemblePlanPatchFromCommittedFacts(committed, contractInheritance) ?? {};
|
|
2201
2262
|
const patch = {
|
|
2202
2263
|
...fragment,
|
|
@@ -2352,23 +2413,31 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2352
2413
|
}
|
|
2353
2414
|
},
|
|
2354
2415
|
});
|
|
2355
|
-
|
|
2356
|
-
|
|
2416
|
+
const durable = await createDurableFrontendTools({
|
|
2417
|
+
file: path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), attemptId, store: input.store,
|
|
2418
|
+
setWorkingStore: next => { store = next; },
|
|
2419
|
+
binding: { sourceBinding: input.sourceBinding, skeleton: input.skeleton, requirementIds: input.requirementIds, writeSet: input.writeSetPatterns, declaredUiStateIds: input.declaredUiStateIds, citations: input.componentNewSourceReferences, contractInheritance },
|
|
2420
|
+
tools: [
|
|
2357
2421
|
recordRouteSelectionTool,
|
|
2358
2422
|
recordComponentChoiceTool,
|
|
2359
2423
|
recordStateRegistryTool,
|
|
2360
2424
|
recordStateFlowTool,
|
|
2361
2425
|
recordDataFlowTool,
|
|
2362
2426
|
recordMockApiTool,
|
|
2427
|
+
recordMockEndpointTool,
|
|
2363
2428
|
recordDesignDeviationTool,
|
|
2364
2429
|
recordDependencyTool,
|
|
2365
2430
|
recordPlanRequirementTool,
|
|
2431
|
+
recordPlanGroupCoverageTool,
|
|
2366
2432
|
recordPlanVerificationTargetTool,
|
|
2367
2433
|
recordPlanEvidenceGapTool,
|
|
2368
2434
|
adoptStagedFactTool,
|
|
2369
2435
|
finalizePlanTool,
|
|
2370
2436
|
],
|
|
2371
|
-
|
|
2437
|
+
});
|
|
2438
|
+
return {
|
|
2439
|
+
customTools: durable.customTools,
|
|
2440
|
+
adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
|
|
2372
2441
|
for (const record of records) {
|
|
2373
2442
|
if (record.phase !== "committed")
|
|
2374
2443
|
continue;
|
|
@@ -2427,16 +2496,13 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2427
2496
|
throw new Error(`frontend plan shard fact merge failed: ${result.error}`);
|
|
2428
2497
|
}
|
|
2429
2498
|
}
|
|
2430
|
-
},
|
|
2499
|
+
}),
|
|
2431
2500
|
setActiveRequirementScope: (requirementIds) => {
|
|
2432
2501
|
activeRequirementScope = [
|
|
2433
2502
|
...new Set(requirementIds.filter((id) => id.trim().length > 0)),
|
|
2434
2503
|
];
|
|
2435
2504
|
},
|
|
2436
|
-
flush:
|
|
2437
|
-
const committed = readCommittedEvents(store, attemptId);
|
|
2438
|
-
await writeTypedEventStoreJsonl(path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), committed);
|
|
2439
|
-
},
|
|
2505
|
+
flush: durable.flush,
|
|
2440
2506
|
committedFactCount: () => readCommittedEvents(store, attemptId).length,
|
|
2441
2507
|
committedRequirementIds: () => {
|
|
2442
2508
|
const ids = new Set();
|
|
@@ -2538,8 +2604,11 @@ export async function createFrontendContractTools(input) {
|
|
|
2538
2604
|
]);
|
|
2539
2605
|
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2540
2606
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
2541
|
-
|
|
2607
|
+
let store = input.store;
|
|
2542
2608
|
const attemptId = input.attemptId;
|
|
2609
|
+
let activeScope = null;
|
|
2610
|
+
const committedRequirementIds = () => new Set(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "requirement").map(r => String(r.fact.id)));
|
|
2611
|
+
const completedScopeRequirementIds = () => new Set(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "contract-scope-completed").flatMap(r => Array.isArray(r.fact.requirementIds) ? r.fact.requirementIds.filter((id) => typeof id === "string") : []));
|
|
2543
2612
|
const receipt = (details) => ({
|
|
2544
2613
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
2545
2614
|
details,
|
|
@@ -2589,12 +2658,24 @@ export async function createFrontendContractTools(input) {
|
|
|
2589
2658
|
label: name,
|
|
2590
2659
|
description: `Commit an origin=contract ${kind} fact. IMPORTANT: submit incrementally — batch up to 5 record_* calls per message, starting from the FIRST message; never attempt to emit the whole contract in one response (a single large dump will be truncated and rejected). Every message must make progress by committing at least one record_* fact.`,
|
|
2591
2660
|
promptSnippet: `Commit 1-5 origin=contract ${kind} facts (up to 5 per message).`,
|
|
2592
|
-
parameters: Type.Object({
|
|
2661
|
+
parameters: Type.Object({
|
|
2662
|
+
text: Type.String({ minLength: 1 }),
|
|
2663
|
+
requirementIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
2664
|
+
sourceFragmentIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
2665
|
+
...(kind === "constraint" ? { category: Type.Optional(Type.Enum({ constraint: "constraint", "non-goal": "non-goal", risk: "risk" })) } : {}),
|
|
2666
|
+
...(kind === "handoff-intent" ? { taskKind: Type.Literal("frontend-test"), blocking: Type.Optional(Type.Boolean()) } : {}),
|
|
2667
|
+
}, { additionalProperties: false }),
|
|
2593
2668
|
async execute(_toolCallId, params) {
|
|
2669
|
+
const data = params;
|
|
2670
|
+
if (!data.text?.trim())
|
|
2671
|
+
return receipt({ ok: false, code: "TOOL_SCHEMA_INVALID", error: `${name}: text must be non-empty` });
|
|
2672
|
+
const knownFragments = new Set([...(input.canonicalRequirements?.values() ?? [])].flatMap(r => r.sourceFragmentIds));
|
|
2673
|
+
if (data.requirementIds?.some(id => !input.canonicalRequirements?.has(id)) || data.sourceFragmentIds?.some(id => !knownFragments.has(id)))
|
|
2674
|
+
return receipt({ ok: false, code: "CONTRACT_REFERENCE_UNKNOWN", error: `${name}: reference is outside the frozen source inventory` });
|
|
2594
2675
|
const result = await adoptContractFact(kind, {
|
|
2676
|
+
...(params ?? {}),
|
|
2595
2677
|
kind,
|
|
2596
2678
|
origin: "contract",
|
|
2597
|
-
...(params ?? {}),
|
|
2598
2679
|
});
|
|
2599
2680
|
return receipt(result);
|
|
2600
2681
|
},
|
|
@@ -2608,9 +2689,9 @@ export async function createFrontendContractTools(input) {
|
|
|
2608
2689
|
const recordRequirementTool = defineTool({
|
|
2609
2690
|
name: "record_requirement",
|
|
2610
2691
|
label: "record_requirement",
|
|
2611
|
-
description: 'Confirm one canonical ledger requirement (origin=contract requirement fact). Pass
|
|
2692
|
+
description: 'Confirm one canonical ledger requirement (origin=contract requirement fact). Pass the canonical id and optional execution:{groupId,kind,summary} only. Reuse a group only when its behavior and all permission/threshold/error conditions agree; retain separate groups for differences. Constraints/exclusions do not require invented UI. The canonical id is listed in the <frontend_contract_input> inventory, e.g. {"id": "AC-001"} — the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself: free-form rewrites and JSON-stringified fragment arrays are rejected. IMPORTANT: batch up to 5 record_* calls per message, starting from the FIRST message.',
|
|
2612
2693
|
promptSnippet: "Confirm 1-5 canonical requirements by id (up to 5 per message).",
|
|
2613
|
-
parameters: Type.Object({ id: Type.String({ description: "Canonical ledger requirement id (e.g. AC-001)" }) }, { additionalProperties: false }),
|
|
2694
|
+
parameters: Type.Object({ id: Type.String({ description: "Canonical ledger requirement id (e.g. AC-001)" }), execution: Type.Optional(Type.Object({ groupId: Type.String({ minLength: 1 }), kind: Type.Union([Type.Literal("behavior"), Type.Literal("constraint"), Type.Literal("exclusion")]), summary: Type.String({ minLength: 1 }) }, { additionalProperties: false })) }, { additionalProperties: false }),
|
|
2614
2695
|
async execute(_toolCallId, params) {
|
|
2615
2696
|
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
2616
2697
|
if (!id) {
|
|
@@ -2620,6 +2701,8 @@ export async function createFrontendContractTools(input) {
|
|
|
2620
2701
|
error: "record_requirement requires the canonical requirement id",
|
|
2621
2702
|
});
|
|
2622
2703
|
}
|
|
2704
|
+
if (activeScope && !activeScope.has(id))
|
|
2705
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: `requirement ${id} is outside the complete input scope of this session` });
|
|
2623
2706
|
const canonical = input.canonicalRequirements?.get(id);
|
|
2624
2707
|
if (!canonical) {
|
|
2625
2708
|
const known = [...(input.canonicalRequirements?.keys() ?? [])];
|
|
@@ -2629,6 +2712,14 @@ export async function createFrontendContractTools(input) {
|
|
|
2629
2712
|
error: `record_requirement id "${id}" is not a canonical ledger requirement; canonical ids are: ${known.join(", ") || "(none)"}`,
|
|
2630
2713
|
});
|
|
2631
2714
|
}
|
|
2715
|
+
if (params.execution) {
|
|
2716
|
+
try {
|
|
2717
|
+
collectFrontendExecutionGroups([...readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "requirement" && r.fact.id !== id).map(r => ({ id: String(r.fact.id), execution: r.fact.execution })), { id, execution: params.execution }]);
|
|
2718
|
+
}
|
|
2719
|
+
catch (error) {
|
|
2720
|
+
return receipt({ ok: false, code: "EXECUTION_GROUP_CONFLICT", error: String(error) });
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2632
2723
|
const result = await adoptContractFact("requirement", {
|
|
2633
2724
|
kind: "requirement",
|
|
2634
2725
|
origin: "contract",
|
|
@@ -2636,6 +2727,7 @@ export async function createFrontendContractTools(input) {
|
|
|
2636
2727
|
id,
|
|
2637
2728
|
text: canonical.text,
|
|
2638
2729
|
sourceFragmentIds: canonical.sourceFragmentIds,
|
|
2730
|
+
...(params.execution ? { execution: params.execution } : {}),
|
|
2639
2731
|
});
|
|
2640
2732
|
return receipt(result);
|
|
2641
2733
|
},
|
|
@@ -2715,15 +2807,17 @@ export async function createFrontendContractTools(input) {
|
|
|
2715
2807
|
const recordRequiredDeliverablesTool = defineTool({
|
|
2716
2808
|
name: "record_required_deliverables",
|
|
2717
2809
|
label: "record_required_deliverables",
|
|
2718
|
-
description: 'Declare the complete source-required file deliverables as {items:[{path,requirementId,sourceFragmentId}]}. Interpret obligations from the original source, including lists/tables: permissions (allowedPaths/only allowed to modify), prohibitions, examples and read-only references are NOT delivery obligations. Each path must appear exactly in its frozen requirement-bound source fragment. Submit {items:[]} explicitly if no files are mandatory.
|
|
2719
|
-
parameters: Type.Object({ items: Type.Array(Type.Object({
|
|
2810
|
+
description: 'Declare the complete source-required file deliverables as {items:[{path,requirementId,sourceFragmentId}]}. Interpret obligations from the original source, including lists/tables: permissions (allowedPaths/only allowed to modify), prohibitions, examples and read-only references are NOT delivery obligations. Each path must appear exactly in its frozen requirement-bound source fragment. Submit {items:[]} explicitly if no files are mandatory. Each call appends complete source-bound items; replace:true explicitly replaces the inventory. Required before finalize_contract ready.',
|
|
2811
|
+
parameters: Type.Object({ replace: Type.Optional(Type.Boolean()), items: Type.Array(Type.Object({
|
|
2720
2812
|
path: Type.String(), requirementId: Type.String(), sourceFragmentId: Type.String(),
|
|
2721
2813
|
}, { additionalProperties: false })) }, { additionalProperties: false }),
|
|
2722
2814
|
async execute(_toolCallId, params) {
|
|
2723
2815
|
try {
|
|
2724
|
-
const declaration = validateFrontendRequiredDeliverables(params, input.canonicalRequirements ?? new Map());
|
|
2816
|
+
const declaration = validateFrontendRequiredDeliverables({ items: params.items }, input.canonicalRequirements ?? new Map());
|
|
2817
|
+
const previous = params.replace ? [] : readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "required-deliverables").at(-1)?.fact.items;
|
|
2818
|
+
const items = [...new Map([...(Array.isArray(previous) ? previous : []), ...declaration.items].map(item => [JSON.stringify(item), item])).values()];
|
|
2725
2819
|
return receipt(await adoptContractFact("required-deliverables", {
|
|
2726
|
-
kind: "required-deliverables", origin: "contract",
|
|
2820
|
+
kind: "required-deliverables", origin: "contract", items,
|
|
2727
2821
|
}));
|
|
2728
2822
|
}
|
|
2729
2823
|
catch (error) {
|
|
@@ -2779,7 +2873,7 @@ export async function createFrontendContractTools(input) {
|
|
|
2779
2873
|
const finalizeContractTool = defineTool({
|
|
2780
2874
|
name: "finalize_contract",
|
|
2781
2875
|
label: "finalize_contract",
|
|
2782
|
-
description: "Commit the contract-finalized terminal fact with a disposition of ready | ready-with-assumptions | blocked (blocked requires blockingOwner).
|
|
2876
|
+
description: "Commit the contract-finalized terminal fact with a disposition of ready | ready-with-assumptions | blocked (blocked requires blockingOwner). A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
|
|
2783
2877
|
promptSnippet: "Commit the contract-finalized terminal (disposition + optional blockingOwner).",
|
|
2784
2878
|
parameters: Type.Object({
|
|
2785
2879
|
disposition: Type.Enum({
|
|
@@ -2811,6 +2905,9 @@ export async function createFrontendContractTools(input) {
|
|
|
2811
2905
|
error: "call record_required_deliverables with the complete source-bound inventory (or items:[] when none) before finalizing",
|
|
2812
2906
|
});
|
|
2813
2907
|
}
|
|
2908
|
+
const missing = [...(input.canonicalRequirements?.keys() ?? [])].filter(id => !committedRequirementIds().has(id) || (activeScope !== null && !completedScopeRequirementIds().has(id)));
|
|
2909
|
+
if (disposition !== "blocked" && missing.length)
|
|
2910
|
+
return receipt({ ok: false, code: "CONTRACT_REQUIREMENT_COVERAGE_MISSING", error: `Confirm all complete source obligations before finalizing: ${missing.join(", ")}` });
|
|
2814
2911
|
const blockedOwner = mapContractBlockedOwner({
|
|
2815
2912
|
disposition: disposition ?? "",
|
|
2816
2913
|
blockingOwner,
|
|
@@ -2831,19 +2928,41 @@ export async function createFrontendContractTools(input) {
|
|
|
2831
2928
|
return receipt(result);
|
|
2832
2929
|
},
|
|
2833
2930
|
});
|
|
2834
|
-
|
|
2835
|
-
|
|
2931
|
+
const completeScopeTool = defineTool({
|
|
2932
|
+
name: "complete_contract_scope", label: "complete_contract_scope",
|
|
2933
|
+
description: "After recording all requirements AND their evidence, constraints, questions and deliverables for this session, mark the scope complete. Confirming an ID alone does not complete its analysis. Do this before finalize_contract.",
|
|
2934
|
+
parameters: Type.Object({ requirementIds: Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }) }, { additionalProperties: false }),
|
|
2935
|
+
async execute(_id, params) {
|
|
2936
|
+
const ids = params.requirementIds;
|
|
2937
|
+
if (activeScope === null || ids.length !== activeScope.size || ids.some(id => !activeScope?.has(id) || !committedRequirementIds().has(id)))
|
|
2938
|
+
return receipt({ ok: false, code: "CONTRACT_SCOPE_INCOMPLETE", error: "Complete exactly the active scope after confirming all its obligations" });
|
|
2939
|
+
return receipt(await adoptContractFact("contract-scope-completed", { kind: "contract-scope-completed", origin: "contract", requirementIds: ids }));
|
|
2940
|
+
},
|
|
2941
|
+
});
|
|
2942
|
+
const durable = await createDurableFrontendTools({
|
|
2943
|
+
file: path.join(input.runDir, input.nodeId, "contract-typed-facts.jsonl"), attemptId, store: input.store,
|
|
2944
|
+
setWorkingStore: next => { store = next; }, binding: { canonicalRequirements: input.canonicalRequirements, sourceDigest: input.sourceDigest },
|
|
2945
|
+
tools: [
|
|
2836
2946
|
...recordTools,
|
|
2837
2947
|
recordRequirementTool,
|
|
2838
2948
|
recordEvidenceExpectationTool,
|
|
2839
2949
|
recordUiStateTool,
|
|
2840
2950
|
recordRequiredDeliverablesTool,
|
|
2841
2951
|
recordOpenspecSelectionTool,
|
|
2952
|
+
completeScopeTool,
|
|
2842
2953
|
finalizeContractTool,
|
|
2843
2954
|
],
|
|
2955
|
+
});
|
|
2956
|
+
return {
|
|
2957
|
+
customTools: durable.customTools,
|
|
2958
|
+
inputRequirements: () => [...(input.canonicalRequirements ?? [])].map(([id, value]) => ({ id, text: value.text, sourceFragmentIds: [...value.sourceFragmentIds] })),
|
|
2959
|
+
completedScopeRequirementIds,
|
|
2960
|
+
setActiveRequirementScope: ids => { activeScope = ids === null ? null : new Set(ids); },
|
|
2961
|
+
committedRequirementIds,
|
|
2962
|
+
committedFacts: () => readCommittedEvents(input.store, attemptId),
|
|
2844
2963
|
flush: async () => {
|
|
2845
|
-
|
|
2846
|
-
|
|
2964
|
+
await durable.flush();
|
|
2965
|
+
const committed = readCommittedEvents(input.store, attemptId);
|
|
2847
2966
|
await writeJsonAtomic(path.join(input.runDir, input.nodeId, "frontend-task-contract-vNext.json"), buildFrontendTaskContractVNext(committed));
|
|
2848
2967
|
},
|
|
2849
2968
|
};
|
|
@@ -2895,7 +3014,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2895
3014
|
]);
|
|
2896
3015
|
const { readCommittedEvents, writeTypedEventStoreJsonl } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
2897
3016
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../workflows/dag/frontend-typed-event-transaction.js");
|
|
2898
|
-
|
|
3017
|
+
let store = input.store;
|
|
2899
3018
|
const attemptId = input.attemptId;
|
|
2900
3019
|
const stringArray = Type.Array(Type.String({}));
|
|
2901
3020
|
const optionalString = Type.Optional(Type.String({}));
|
|
@@ -2909,6 +3028,18 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2909
3028
|
});
|
|
2910
3029
|
const sourceDeclaredPaths = (input.sourceDeclaredPaths ?? []).map((value) => value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""));
|
|
2911
3030
|
const hasSourceDeclarations = input.sourceDeclaredPaths !== undefined;
|
|
3031
|
+
let activeScope;
|
|
3032
|
+
const scopeIdentity = (ids) => createHash("sha256").update(JSON.stringify([...ids].sort())).digest("hex");
|
|
3033
|
+
const latestScopes = () => {
|
|
3034
|
+
const byRequirement = new Map();
|
|
3035
|
+
for (const record of readCommittedEvents(store, attemptId))
|
|
3036
|
+
if (record.fact.kind === "scout-scope" && Array.isArray(record.fact.requirementIds)) {
|
|
3037
|
+
for (const id of record.fact.requirementIds)
|
|
3038
|
+
if (typeof id === "string")
|
|
3039
|
+
byRequirement.set(id, record.fact);
|
|
3040
|
+
}
|
|
3041
|
+
return byRequirement;
|
|
3042
|
+
};
|
|
2912
3043
|
const isSourceDeclared = (candidate) => {
|
|
2913
3044
|
const normalized = candidate.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
2914
3045
|
return sourceDeclaredPaths.some((declared) => declared === normalized || declared.startsWith(`${normalized}/`));
|
|
@@ -3001,6 +3132,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3001
3132
|
description: "Commit an origin=scout target-surface fact with complete/blocked discovery status. A complete surface needs a proven target path and no unresolved paths; blocked surfaces name the unresolved paths instead of guessing. Example: {\"completeness\": \"complete\", \"entrypoint\": \"<file>\", \"implementationPaths\": [\"<dir or file>\"], \"testPaths\": [\"<file>\"], \"allowedPathConflicts\": [], \"unresolvedPaths\": []}",
|
|
3002
3133
|
promptSnippet: "Commit an origin=scout target-surface fact.",
|
|
3003
3134
|
parameters: Type.Object({
|
|
3135
|
+
scopeId: Type.Optional(Type.String({ minLength: 1 })),
|
|
3004
3136
|
completeness: scoutCompleteness,
|
|
3005
3137
|
entrypoint: optionalString,
|
|
3006
3138
|
routeOrMount: optionalString,
|
|
@@ -3011,6 +3143,8 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3011
3143
|
unresolvedPaths: stringArray,
|
|
3012
3144
|
}, { additionalProperties: false }),
|
|
3013
3145
|
async execute(_toolCallId, params) {
|
|
3146
|
+
if (input.requirementIds && (!activeScope?.length || params.scopeId !== scopeIdentity(activeScope)))
|
|
3147
|
+
return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Use exactly the runtime Scout scopeId; discovery may complete only the supplied obligations" });
|
|
3014
3148
|
const implementationPaths = params?.implementationPaths ?? [];
|
|
3015
3149
|
const testPaths = params?.testPaths ?? [];
|
|
3016
3150
|
const pathEvidence = await enrichScoutPathEvidence([
|
|
@@ -3018,7 +3152,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3018
3152
|
...implementationPaths,
|
|
3019
3153
|
...testPaths,
|
|
3020
3154
|
]);
|
|
3021
|
-
const
|
|
3155
|
+
const surface = {
|
|
3022
3156
|
kind: "target-surface",
|
|
3023
3157
|
origin: "scout",
|
|
3024
3158
|
completeness: params?.completeness ?? "blocked",
|
|
@@ -3031,7 +3165,27 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3031
3165
|
unresolvedPaths: params?.unresolvedPaths ?? [],
|
|
3032
3166
|
...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}),
|
|
3033
3167
|
...(pathEvidence.length > 0 ? { pathEvidence } : {}),
|
|
3034
|
-
}
|
|
3168
|
+
};
|
|
3169
|
+
if (activeScope) {
|
|
3170
|
+
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
3171
|
+
if (surface.completeness === "complete") {
|
|
3172
|
+
const check = readCompleteScoutTargetSurface([{ phase: "committed", fact: surface }]);
|
|
3173
|
+
if (!check.ok)
|
|
3174
|
+
return receipt({ ok: false, code: "SCOUT_SCOPE_INCOMPLETE", error: check.reason });
|
|
3175
|
+
}
|
|
3176
|
+
const saved = await adoptScoutFact("scout-scope", { kind: "scout-scope", origin: "scout", id: params.scopeId, requirementIds: activeScope, surface });
|
|
3177
|
+
if (!saved.ok)
|
|
3178
|
+
return receipt(saved);
|
|
3179
|
+
const current = latestScopes();
|
|
3180
|
+
if (input.requirementIds?.every(id => current.get(id)?.surface?.completeness === "complete")) {
|
|
3181
|
+
const surfaces = [...new Set(input.requirementIds.map(id => current.get(id)))].map(f => f.surface);
|
|
3182
|
+
const union = (key) => [...new Set(surfaces.flatMap(s => Array.isArray(s[key]) ? s[key] : []))];
|
|
3183
|
+
const entries = [...new Set(surfaces.map(s => String(s.entrypoint ?? "")).filter(Boolean))];
|
|
3184
|
+
return receipt(await adoptScoutFact("target-surface", { kind: "target-surface", origin: "scout", completeness: "complete", entrypoint: entries[0] ?? "", implementationPaths: [...new Set([...entries, ...union("implementationPaths")])], testPaths: union("testPaths"), allowedPathConflicts: union("allowedPathConflicts"), unresolvedPaths: union("unresolvedPaths"), routeOrMount: [...new Set(surfaces.map(s => s.routeOrMount).filter(Boolean))].join("\n"), dataSource: [...new Set(surfaces.map(s => s.dataSource).filter(Boolean))].join("\n"), pathEvidence: surfaces.flatMap(s => s.pathEvidence ?? []), ...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}) }));
|
|
3185
|
+
}
|
|
3186
|
+
return receipt(saved);
|
|
3187
|
+
}
|
|
3188
|
+
const result = await adoptScoutFact("target-surface", surface);
|
|
3035
3189
|
return receipt(result);
|
|
3036
3190
|
},
|
|
3037
3191
|
});
|
|
@@ -3055,10 +3209,30 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3055
3209
|
return receipt(result);
|
|
3056
3210
|
},
|
|
3057
3211
|
});
|
|
3212
|
+
const durable = await createDurableFrontendTools({
|
|
3213
|
+
file: path.join(input.runDir, input.nodeId, "scout-typed-facts.jsonl"), attemptId, store: input.store,
|
|
3214
|
+
setWorkingStore: next => { store = next; }, binding: { requirementIds: input.requirementIds, sourceDeclaredPaths: input.sourceDeclaredPaths, sourceDigest: input.sourceDigest, workspaceRoot: input.workspaceRoot },
|
|
3215
|
+
tools: [recordTargetSurfaceTool, recordDesignEvidenceTool],
|
|
3216
|
+
validateRestored: async (records) => {
|
|
3217
|
+
if (!input.workspaceRoot)
|
|
3218
|
+
return;
|
|
3219
|
+
for (const record of records) {
|
|
3220
|
+
const evidence = record.fact.kind === "scout-scope" ? record.fact.surface?.pathEvidence : record.fact.pathEvidence;
|
|
3221
|
+
if (!Array.isArray(evidence))
|
|
3222
|
+
continue;
|
|
3223
|
+
for (const previous of evidence) {
|
|
3224
|
+
if (!isRecordObject(previous) || typeof previous.path !== "string")
|
|
3225
|
+
throw Error("scout path evidence is malformed");
|
|
3226
|
+
const current = (await enrichScoutPathEvidence([previous.path]))[0];
|
|
3227
|
+
if (!current || current.sha256 !== previous.sha256 || current.fresh !== previous.fresh)
|
|
3228
|
+
throw Error(`scout evidence drift: ${previous.path}; refresh Scout before reusing facts`);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
},
|
|
3232
|
+
});
|
|
3058
3233
|
return {
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
adoptCommittedFacts: async (records) => {
|
|
3234
|
+
...durable,
|
|
3235
|
+
adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
|
|
3062
3236
|
for (const record of records) {
|
|
3063
3237
|
if (record.phase !== "committed")
|
|
3064
3238
|
continue;
|
|
@@ -3072,11 +3246,16 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
3072
3246
|
throw new Error(`frontend scout shard fact merge failed: ${result.error}`);
|
|
3073
3247
|
}
|
|
3074
3248
|
}
|
|
3249
|
+
}),
|
|
3250
|
+
setActiveScope: ids => {
|
|
3251
|
+
if (!ids.length || ids.some(id => !input.requirementIds?.includes(id)))
|
|
3252
|
+
throw Error("FRONTEND_INPUT_SCOPE_VIOLATION");
|
|
3253
|
+
activeScope = [...new Set(ids)];
|
|
3254
|
+
return scopeIdentity(activeScope);
|
|
3075
3255
|
},
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
},
|
|
3256
|
+
completedRequirementIds: () => new Set([...latestScopes()].filter(([, fact]) => fact.surface?.completeness === "complete").map(([id]) => id)),
|
|
3257
|
+
completedScopeFacts: () => [...new Set(latestScopes().values())].filter(f => f.surface?.completeness === "complete"),
|
|
3258
|
+
committedFacts: () => readCommittedEvents(store, attemptId),
|
|
3080
3259
|
};
|
|
3081
3260
|
}
|
|
3082
3261
|
export function buildDagPiUserMessage(task, persona, step) {
|
|
@@ -3327,15 +3506,14 @@ async function runFrontendReviewTerminalShadow(input) {
|
|
|
3327
3506
|
return input.mapped;
|
|
3328
3507
|
const { compareTypedReviewToLegacyJsonVerdict } = await import("../workflows/dag/frontend-review-context.js");
|
|
3329
3508
|
const { parseJsonReviewVerdict } = await import("../workflows/dag/output-protocol.js");
|
|
3330
|
-
const sessionEventsPath = path.join(input.meta.runDir, input.task.id, "session-events.jsonl");
|
|
3331
3509
|
let typedKinds = [];
|
|
3332
3510
|
try {
|
|
3333
|
-
|
|
3334
|
-
|
|
3511
|
+
await input.tools?.flush();
|
|
3512
|
+
input.tools?.scopeProtocol?.assertComplete();
|
|
3513
|
+
typedKinds = input.tools?.scopeProtocol?.committedFacts().filter(r => ["approve_review", "request_review_changes"].includes(String(r.fact.kind))).map(r => String(r.fact.kind)) ?? [];
|
|
3335
3514
|
}
|
|
3336
|
-
catch {
|
|
3337
|
-
|
|
3338
|
-
typedKinds = [];
|
|
3515
|
+
catch (error) {
|
|
3516
|
+
return { ...input.mapped, ok: false, failureCategory: "frontend-ledger-invalid", stderr: `FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}` };
|
|
3339
3517
|
}
|
|
3340
3518
|
let legacyVerdict;
|
|
3341
3519
|
try {
|
|
@@ -3361,13 +3539,7 @@ async function runFrontendReviewTerminalShadow(input) {
|
|
|
3361
3539
|
reason: `typed review equivalence comparison crashed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3362
3540
|
};
|
|
3363
3541
|
}
|
|
3364
|
-
//
|
|
3365
|
-
try {
|
|
3366
|
-
await input.tools?.flush?.();
|
|
3367
|
-
}
|
|
3368
|
-
catch {
|
|
3369
|
-
// best-effort
|
|
3370
|
-
}
|
|
3542
|
+
// The durable ledger was validated above; this artifact is audit-only.
|
|
3371
3543
|
try {
|
|
3372
3544
|
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-review-status.json", {
|
|
3373
3545
|
schemaVersion: 1,
|
|
@@ -3409,23 +3581,16 @@ async function runFrontendDesignTerminalShadow(input) {
|
|
|
3409
3581
|
// diagnosed as an omitted terminal tool call.
|
|
3410
3582
|
if (!input.mapped.ok)
|
|
3411
3583
|
return input.mapped;
|
|
3412
|
-
const sessionEventsPath = path.join(input.meta.runDir, input.task.id, "session-events.jsonl");
|
|
3413
3584
|
let typedKinds = [];
|
|
3414
3585
|
try {
|
|
3415
|
-
|
|
3416
|
-
|
|
3586
|
+
await input.tools?.flush();
|
|
3587
|
+
input.tools?.scopeProtocol?.assertComplete();
|
|
3588
|
+
typedKinds = input.tools?.scopeProtocol?.committedFacts().filter(r => ["approve_design", "request_design_changes"].includes(String(r.fact.kind))).map(r => String(r.fact.kind)) ?? [];
|
|
3417
3589
|
}
|
|
3418
|
-
catch {
|
|
3419
|
-
|
|
3420
|
-
typedKinds = [];
|
|
3421
|
-
}
|
|
3422
|
-
// Audit-only flush + artifact. Neither blocks the node.
|
|
3423
|
-
try {
|
|
3424
|
-
await input.tools?.flush?.();
|
|
3425
|
-
}
|
|
3426
|
-
catch {
|
|
3427
|
-
// best-effort
|
|
3590
|
+
catch (error) {
|
|
3591
|
+
return { ...input.mapped, ok: false, failureCategory: "frontend-ledger-invalid", stderr: `FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${error instanceof Error ? error.message : String(error)}` };
|
|
3428
3592
|
}
|
|
3593
|
+
// The durable ledger was validated above; this artifact is audit-only.
|
|
3429
3594
|
try {
|
|
3430
3595
|
await writeDagNodeJsonArtifact(input.meta.runDir, input.task.id, "fact-design-status.json", {
|
|
3431
3596
|
schemaVersion: 1,
|
|
@@ -3458,6 +3623,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3458
3623
|
id: "coverage",
|
|
3459
3624
|
toolNames: new Set([
|
|
3460
3625
|
"record_plan_requirement",
|
|
3626
|
+
"record_plan_group_coverage",
|
|
3461
3627
|
"record_plan_verification_target",
|
|
3462
3628
|
"record_plan_evidence_gap",
|
|
3463
3629
|
"adopt_staged_fact",
|
|
@@ -3474,13 +3640,14 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3474
3640
|
toolNames: new Set(["record_state_registry", "adopt_staged_fact"]),
|
|
3475
3641
|
instruction: [
|
|
3476
3642
|
"PLAN PHASE — global UX vocabulary.",
|
|
3477
|
-
"
|
|
3643
|
+
"Bootstrap the global UX vocabulary from the execution-group index and authoritative declared states. This is navigation, not permission to decide unseen behavior. Detailed complete scopes may extend the registry with replace:true while preserving live names. UI states use declaredUiStates ids when present. Interaction names are stable kebab-case behavior domains; merge requirements that describe the same behavior instead of renaming it per AC slice. Empty arrays explicitly declare that no UX vocabulary applies. Do not record component choices or state-flow details in this phase.",
|
|
3478
3644
|
"Do not call finalize_plan; it is not available in this phase.",
|
|
3479
3645
|
].join(" "),
|
|
3480
3646
|
},
|
|
3481
3647
|
{
|
|
3482
3648
|
id: "ux-local",
|
|
3483
3649
|
toolNames: new Set([
|
|
3650
|
+
"record_state_registry",
|
|
3484
3651
|
"record_component_choice",
|
|
3485
3652
|
"record_state_flow",
|
|
3486
3653
|
"record_plan_verification_target",
|
|
@@ -3488,7 +3655,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3488
3655
|
]),
|
|
3489
3656
|
instruction: [
|
|
3490
3657
|
"PLAN PHASE — global UX decisions.",
|
|
3491
|
-
"Requirements and verification targets are already committed in the ledger. Review the complete
|
|
3658
|
+
"Requirements and verification targets are already committed in the ledger; do not re-record unchanged facts. Review the current complete execution-group scope and the committed global UX registry together, then record each component choice, UI state and interaction exactly once. Bind each applicable state to its verificationTargetIds; the runtime derives the reverse VT.uiStates relation. If a VT requires correction, record_plan_verification_target with replace:true is available after declaring its states; preserve its requirement coverage. Multiple requirements describing one behavior share one registry name and state-flow entry; never repeat or rename it per AC. Cross-cutting data flow belongs to the global Mock/data phase. Do not record routes, Mock/API policy, dependencies, or design deviations here.",
|
|
3492
3659
|
"Do not call finalize_plan; it is not available in this phase.",
|
|
3493
3660
|
].join(" "),
|
|
3494
3661
|
},
|
|
@@ -3502,7 +3669,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3502
3669
|
},
|
|
3503
3670
|
{
|
|
3504
3671
|
id: "global-mock-data",
|
|
3505
|
-
toolNames: new Set(["record_data_flow", "record_mock_api", "adopt_staged_fact"]),
|
|
3672
|
+
toolNames: new Set(["record_data_flow", "record_mock_api", "record_mock_endpoint", "adopt_staged_fact"]),
|
|
3506
3673
|
instruction: [
|
|
3507
3674
|
"PLAN PHASE — global Mock/API and data policy.",
|
|
3508
3675
|
"Record the cross-cutting interaction-to-endpoint data flow and Mock/API strategy only. Keep this decision set separate from route, component, state, dependency, and deviation facts. Do not call finalize_plan.",
|
|
@@ -3668,7 +3835,8 @@ export function collectFrontendPlanPhaseMissingFacts(input) {
|
|
|
3668
3835
|
return missing;
|
|
3669
3836
|
}
|
|
3670
3837
|
const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
|
|
3671
|
-
const
|
|
3838
|
+
const allInteractions = collectCanonicalStateFlowNames(input.committedFacts).interactionNames;
|
|
3839
|
+
const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, new Set(input.requirementIds)))).interactionNames].filter(name => allInteractions.has(name)));
|
|
3672
3840
|
const coveredInteractions = new Set(facts
|
|
3673
3841
|
.filter((fact) => fact.kind === "data-flow")
|
|
3674
3842
|
.flatMap((fact) => planFactStringList(fact.interactions)));
|
|
@@ -3754,21 +3922,15 @@ async function mapWithConcurrency(items, limit, worker) {
|
|
|
3754
3922
|
}));
|
|
3755
3923
|
return results;
|
|
3756
3924
|
}
|
|
3757
|
-
function compactPromptString(value,
|
|
3758
|
-
|
|
3759
|
-
return undefined;
|
|
3760
|
-
const normalized = value.trim();
|
|
3761
|
-
return normalized.length <= maxChars
|
|
3762
|
-
? normalized
|
|
3763
|
-
: `${normalized.slice(0, maxChars - 1)}…`;
|
|
3925
|
+
function compactPromptString(value, _maxChars) {
|
|
3926
|
+
return typeof value === "string" && value.trim().length ? value.trim() : undefined;
|
|
3764
3927
|
}
|
|
3765
|
-
function compactPromptStringArray(value,
|
|
3928
|
+
function compactPromptStringArray(value, _maxEntries = 12, maxChars = 180) {
|
|
3766
3929
|
if (!Array.isArray(value))
|
|
3767
3930
|
return [];
|
|
3768
3931
|
return value
|
|
3769
3932
|
.map((item) => compactPromptString(item, maxChars))
|
|
3770
|
-
.filter((item) => item !== undefined)
|
|
3771
|
-
.slice(0, maxEntries);
|
|
3933
|
+
.filter((item) => item !== undefined);
|
|
3772
3934
|
}
|
|
3773
3935
|
function countFrontendPlanTargetSurfaces(basePrompt) {
|
|
3774
3936
|
const match = /<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/.exec(basePrompt);
|
|
@@ -3841,7 +4003,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3841
4003
|
}
|
|
3842
4004
|
}
|
|
3843
4005
|
if (!payload || !Array.isArray(payload.requirements))
|
|
3844
|
-
|
|
4006
|
+
throw Error("FRONTEND_INPUT_INVALID: plan inventory is not parseable");
|
|
3845
4007
|
const requirementsById = new Map();
|
|
3846
4008
|
for (const value of payload.requirements) {
|
|
3847
4009
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -3853,7 +4015,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3853
4015
|
}
|
|
3854
4016
|
const requirements = slice.map((id) => requirementsById.get(id));
|
|
3855
4017
|
if (requirements.some((requirement) => requirement === undefined)) {
|
|
3856
|
-
|
|
4018
|
+
throw Error(`FRONTEND_INPUT_SCOPE_MISSING: ${slice.filter(id => !requirementsById.has(id)).join(", ")}`);
|
|
3857
4019
|
}
|
|
3858
4020
|
const compactRequirements = requirements.map((requirement) => ({
|
|
3859
4021
|
id: compactPromptString(requirement.id, 80),
|
|
@@ -3961,6 +4123,9 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3961
4123
|
? payload.committedUx
|
|
3962
4124
|
: undefined;
|
|
3963
4125
|
const compactPayload = {
|
|
4126
|
+
inputManifest: { ...projectFrontendInputScope({ ...payload, requirements: [...requirementsById.values()] }, slice).inputManifest, semantics: options.includeRequirementText === false ? "navigation-only" : "full" },
|
|
4127
|
+
constraints: payload.constraints,
|
|
4128
|
+
executionGroups: Array.isArray(payload.executionGroups) ? payload.executionGroups.filter(g => isRecordObject(g) && Array.isArray(g.requirementIds) && g.requirementIds.some(id => slice.includes(String(id)))) : [],
|
|
3964
4129
|
requirements: compactRequirements,
|
|
3965
4130
|
requiredDeliverables: Array.isArray(payload.requiredDeliverables) ? payload.requiredDeliverables : [],
|
|
3966
4131
|
...(compactTargetSurface.length > 0
|
|
@@ -4082,7 +4247,7 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4082
4247
|
covers: compactPromptStringArray(item.covers, 40, 120),
|
|
4083
4248
|
evidencePath: compactPromptString(item.evidencePath, 180),
|
|
4084
4249
|
}];
|
|
4085
|
-
})
|
|
4250
|
+
})
|
|
4086
4251
|
: [];
|
|
4087
4252
|
if (choices.length > 0)
|
|
4088
4253
|
compactFacts.push({ kind: fact.kind, uiComponentChoices: choices });
|
|
@@ -4099,8 +4264,8 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4099
4264
|
if (fact.kind === "state-flow") {
|
|
4100
4265
|
compactFacts.push({
|
|
4101
4266
|
kind: fact.kind,
|
|
4102
|
-
uiStates: Array.isArray(fact.uiStates) ? fact.uiStates
|
|
4103
|
-
interactions: Array.isArray(fact.interactions) ? fact.interactions
|
|
4267
|
+
uiStates: Array.isArray(fact.uiStates) ? fact.uiStates : [],
|
|
4268
|
+
interactions: Array.isArray(fact.interactions) ? fact.interactions : [],
|
|
4104
4269
|
removeUiStateNames: compactPromptStringArray(fact.removeUiStateNames, 24, 100),
|
|
4105
4270
|
removeInteractionNames: compactPromptStringArray(fact.removeInteractionNames, 24, 100),
|
|
4106
4271
|
});
|
|
@@ -4123,7 +4288,7 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4123
4288
|
mockApi: {
|
|
4124
4289
|
strategy: compactPromptString(mockApi.strategy, 40),
|
|
4125
4290
|
activation: compactPromptString(mockApi.activation, 180),
|
|
4126
|
-
endpoints: Array.isArray(mockApi.endpoints) ? mockApi.endpoints
|
|
4291
|
+
endpoints: Array.isArray(mockApi.endpoints) ? mockApi.endpoints : [],
|
|
4127
4292
|
},
|
|
4128
4293
|
});
|
|
4129
4294
|
continue;
|
|
@@ -4145,9 +4310,9 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4145
4310
|
const priorityFacts = compactFacts.filter((fact) => fact.kind === "plan-requirement" || fact.kind === "plan-verification-target");
|
|
4146
4311
|
const otherFacts = compactFacts.filter((fact) => fact.kind !== "plan-requirement" && fact.kind !== "plan-verification-target");
|
|
4147
4312
|
const boundedFacts = [
|
|
4148
|
-
...priorityFacts
|
|
4149
|
-
...otherFacts
|
|
4150
|
-
]
|
|
4313
|
+
...priorityFacts,
|
|
4314
|
+
...otherFacts,
|
|
4315
|
+
];
|
|
4151
4316
|
return [
|
|
4152
4317
|
"<frontend_plan_ledger>",
|
|
4153
4318
|
"Committed plan facts from earlier sessions. Treat these as authoritative; correct them only with the allowed replacement/removal fields.",
|
|
@@ -4155,6 +4320,208 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
4155
4320
|
"</frontend_plan_ledger>",
|
|
4156
4321
|
].join("\n");
|
|
4157
4322
|
}
|
|
4323
|
+
export async function runFrontendReviewSegmentedSessions(input) {
|
|
4324
|
+
const protocol = input.tools.scopeProtocol;
|
|
4325
|
+
const terminalKinds = input.phase === "review" ? ["approve_review", "request_review_changes"] : ["approve_design", "request_design_changes"];
|
|
4326
|
+
const terminal = () => protocol.committedFacts().some(r => terminalKinds.includes(String(r.fact.kind)));
|
|
4327
|
+
const targetBytes = input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4328
|
+
const queue = packFrontendInputUnits(input.inventory.scopes.filter(s => !protocol.completedScopeIds().has(s.id)), { targetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits }).map(scopes => ({ scopes, repairs: 0 }));
|
|
4329
|
+
if (!queue.length)
|
|
4330
|
+
queue.push({ scopes: [], repairs: 0 });
|
|
4331
|
+
let calls = 0;
|
|
4332
|
+
let last = { ok: true, assistantText: "", command: [], durationMs: 0, exitCode: 0, failureCategory: "success", modelDisplay: "unknown", parsedEvents: 0, stderr: "", stdout: "", timedOut: false, attemptedModels: [], fallbackUsed: false, tokensUsed: 0 };
|
|
4333
|
+
for (let index = 0; index < queue.length; index++) {
|
|
4334
|
+
await input.tools.flush();
|
|
4335
|
+
await input.inventory.validate();
|
|
4336
|
+
if (terminal()) {
|
|
4337
|
+
protocol.assertComplete();
|
|
4338
|
+
return last;
|
|
4339
|
+
}
|
|
4340
|
+
const item = queue[index];
|
|
4341
|
+
const scopes = item.scopes.filter(s => !protocol.completedScopeIds().has(s.id));
|
|
4342
|
+
protocol.setActiveScope(scopes.map(s => s.id));
|
|
4343
|
+
const finalScope = input.inventory.scopes.every(s => protocol.completedScopeIds().has(s.id) || scopes.some(current => current.id === s.id));
|
|
4344
|
+
const customTools = finalScope ? input.customTools : input.customTools.filter(t => !terminalKinds.includes(String(t.name)));
|
|
4345
|
+
const prompt = `${input.basePrompt}\n<frontend_review_scope>\n${JSON.stringify({ semantics: "full", inventoryDigest: input.inventory.digest, scopes, previouslyCompleted: [...protocol.completedScopeIds()], savedFindings: protocol.committedFacts().filter(r => String(r.fact.kind).endsWith("-finding")).map(r => ({ id: r.fact.id, finding: r.fact.finding })) })}\n</frontend_review_scope>\nReview the complete supplied scopes, saving each finding immediately. Call complete_review_scope for each exact id only after all its independent permissions, thresholds, errors and evidence have been checked. ${finalScope ? "After every scope is complete, make one independent overall approve/request_changes decision; persisted findings cannot be omitted." : "More scopes remain. Do not finalize or reread already completed scopes unless resolving a cross-scope issue."}`;
|
|
4346
|
+
if (Buffer.byteLength(prompt) > targetBytes && scopes.length > 1) {
|
|
4347
|
+
const at = Math.ceil(scopes.length / 2);
|
|
4348
|
+
queue.splice(index, 1, { scopes: scopes.slice(0, at), repairs: 0 }, { scopes: scopes.slice(at), repairs: 0 });
|
|
4349
|
+
index--;
|
|
4350
|
+
continue;
|
|
4351
|
+
}
|
|
4352
|
+
if (++calls > 128)
|
|
4353
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_RECOVERY_EXHAUSTED: session quota reached" };
|
|
4354
|
+
last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage: input.sessionOptions.userMessage, customTools,
|
|
4355
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
4356
|
+
committedCount: () => protocol.committedFacts().length, durableCommittedCount: () => protocol.committedFacts().length,
|
|
4357
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4358
|
+
await input.tools.flush();
|
|
4359
|
+
await input.inventory.validate();
|
|
4360
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4361
|
+
return { ...last, ok: false };
|
|
4362
|
+
const capacity = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4363
|
+
const missing = scopes.filter(s => !protocol.completedScopeIds().has(s.id));
|
|
4364
|
+
if (capacity && (missing.length || !terminal() && finalScope)) {
|
|
4365
|
+
if (missing.length === 1 && scopes.length === 1 || !missing.length && !scopes.length)
|
|
4366
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_INPUT_UNIT_TOO_LARGE: complete review unit or terminal exhausted" };
|
|
4367
|
+
const at = Math.ceil(missing.length / 2);
|
|
4368
|
+
const smaller = missing.length ? [missing.slice(0, at), missing.slice(at)].filter(s => s.length) : [[]];
|
|
4369
|
+
queue.splice(index, 1, ...smaller.map(scopes => ({ scopes, repairs: 0 })));
|
|
4370
|
+
index--;
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
if (!last.ok && !capacity)
|
|
4374
|
+
return last;
|
|
4375
|
+
if (missing.length || finalScope && !terminal()) {
|
|
4376
|
+
if (item.repairs >= 1)
|
|
4377
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: required checkpoint or verdict missing" };
|
|
4378
|
+
queue.splice(index, 1, { scopes: missing, repairs: item.repairs + 1 });
|
|
4379
|
+
index--;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
protocol.assertComplete();
|
|
4383
|
+
return terminal() ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: missing independent verdict" };
|
|
4384
|
+
}
|
|
4385
|
+
export async function runFrontendScoutSegmentedSessions(input) {
|
|
4386
|
+
const inventory = parseFrontendInputBlock(input.basePrompt, "scout");
|
|
4387
|
+
if (!inventory)
|
|
4388
|
+
throw Error("FRONTEND_INPUT_MISSING: Scout compiled inventory unavailable");
|
|
4389
|
+
const units = collectFrontendExecutionGroups(inventory.payload.requirements).map(group => ({ ...group, id: `${group.kind === "unclassified" ? "requirement" : "group"}:${group.id}`, requirements: inventory.payload.requirements.filter(r => group.requirementIds.includes(r.id)) }));
|
|
4390
|
+
const targetBytes = input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
4391
|
+
const queue = packFrontendInputUnits(units, { targetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits }).map(batch => ({ groups: batch, repairs: 0 }));
|
|
4392
|
+
let last = { ok: true, assistantText: "", command: [], durationMs: 0, exitCode: 0, failureCategory: "success", modelDisplay: input.sessionOptions.modelConfig?.model ?? "unknown", parsedEvents: 0, stderr: "", stdout: "", timedOut: false, attemptedModels: [], fallbackUsed: false, tokensUsed: 0 };
|
|
4393
|
+
let calls = 0;
|
|
4394
|
+
for (let index = 0; index < queue.length; index++) {
|
|
4395
|
+
await input.tools.flush();
|
|
4396
|
+
const item = queue[index];
|
|
4397
|
+
const completed = input.tools.completedRequirementIds();
|
|
4398
|
+
const groups = item.groups.filter(g => g.requirementIds.some(id => !completed.has(id)));
|
|
4399
|
+
if (!groups.length)
|
|
4400
|
+
continue;
|
|
4401
|
+
const ids = groups.flatMap(g => g.requirementIds);
|
|
4402
|
+
const scopeId = input.tools.setActiveScope(ids);
|
|
4403
|
+
const prompt = input.basePrompt.replace(inventory.block, `<frontend_scout_input>\n${JSON.stringify(projectFrontendInputScope(inventory.payload, ids))}\n</frontend_scout_input>`) +
|
|
4404
|
+
`\nSCOUT SCOPE ${scopeId}: discover the related surfaces for exactly these complete obligations: ${ids.join(", ")}. Reuse proven paths from completed scope navigation; do not reread their content unless relevant new evidence is needed. Submit record_target_surface with scopeId="${scopeId}" after all discovery/design evidence for this scope. completeness=complete closes only this scope; runtime merges all scopes.\n` +
|
|
4405
|
+
JSON.stringify({ completedScopePaths: input.tools.completedScopeFacts().map(f => ({ id: f.id, requirementIds: f.requirementIds, paths: f.surface?.implementationPaths })) });
|
|
4406
|
+
const envelopeBytes = Buffer.byteLength(prompt + input.sessionOptions.userMessage + JSON.stringify(input.customTools.map(t => { const tool = t; return { name: tool.name, description: tool.description, parameters: tool.parameters }; })));
|
|
4407
|
+
if (envelopeBytes > targetBytes && groups.length > 1) {
|
|
4408
|
+
const at = Math.ceil(groups.length / 2);
|
|
4409
|
+
queue.splice(index, 1, { groups: groups.slice(0, at), repairs: 0 }, { groups: groups.slice(at), repairs: 0 });
|
|
4410
|
+
index--;
|
|
4411
|
+
continue;
|
|
4412
|
+
}
|
|
4413
|
+
if (++calls > 128)
|
|
4414
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "SCOUT_RECOVERY_EXHAUSTED: shared session quota reached" };
|
|
4415
|
+
last = await observeFrontendSession({ ...input.observation, phase: "scout/scope", scopeIds: ids, prompt, userMessage: input.sessionOptions.userMessage, customTools: input.customTools,
|
|
4416
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
4417
|
+
committedCount: () => input.tools.committedFacts().length, durableCommittedCount: () => input.tools.committedFacts().length,
|
|
4418
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools: input.customTools } }));
|
|
4419
|
+
await input.tools.flush();
|
|
4420
|
+
const missing = groups.filter(g => g.requirementIds.some(id => !input.tools.completedRequirementIds().has(id)));
|
|
4421
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4422
|
+
return { ...last, ok: false };
|
|
4423
|
+
const capacity = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4424
|
+
if (capacity && missing.length) {
|
|
4425
|
+
if (missing.length === 1 && groups.length === 1)
|
|
4426
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: Scout ${missing[0].id}; refine the complete source unit; unchanged retries disabled` };
|
|
4427
|
+
const at = Math.ceil(missing.length / 2);
|
|
4428
|
+
queue.splice(index, 1, ...[missing.slice(0, at), missing.slice(at)].filter(batch => batch.length).map(batch => ({ groups: batch, repairs: 0 })));
|
|
4429
|
+
index--;
|
|
4430
|
+
continue;
|
|
4431
|
+
}
|
|
4432
|
+
if (!last.ok && !capacity)
|
|
4433
|
+
return last;
|
|
4434
|
+
if (missing.length) {
|
|
4435
|
+
if (item.repairs >= 1)
|
|
4436
|
+
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "SCOUT_SCOPE_INCOMPLETE: unresolved discovery remains after local correction" };
|
|
4437
|
+
queue.splice(index, 1, { groups: missing, repairs: item.repairs + 1 });
|
|
4438
|
+
index--;
|
|
4439
|
+
continue;
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
await input.tools.flush();
|
|
4443
|
+
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|
|
4444
|
+
const closure = readCompleteScoutTargetSurface(input.tools.committedFacts());
|
|
4445
|
+
return closure.ok ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: closure.reason };
|
|
4446
|
+
}
|
|
4447
|
+
export async function runFrontendContractSegmentedSessions(input) {
|
|
4448
|
+
// Build from the frozen runtime inventory if the caller has not rendered it yet.
|
|
4449
|
+
const basePrompt = parseFrontendInputBlock(input.basePrompt, "contract") ? input.basePrompt : input.basePrompt +
|
|
4450
|
+
`\n<frontend_contract_input>\nFrozen complete source obligations.\n${JSON.stringify({ requirements: input.tools.inputRequirements() })}\n</frontend_contract_input>`;
|
|
4451
|
+
const inventory = parseFrontendInputBlock(basePrompt, "contract");
|
|
4452
|
+
const pending = inventory.payload.requirements.filter(r => !input.tools.completedScopeRequirementIds().has(r.id));
|
|
4453
|
+
const batches = packFrontendInputUnits(pending, { targetBytes: input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits });
|
|
4454
|
+
if (!batches.length)
|
|
4455
|
+
batches.push([]);
|
|
4456
|
+
let last;
|
|
4457
|
+
let invocation = 0;
|
|
4458
|
+
const maxSessions = batches.length * 3 + 2;
|
|
4459
|
+
const terminal = () => input.tools.committedFacts().some(r => r.fact.kind === "contract-finalized");
|
|
4460
|
+
try {
|
|
4461
|
+
for (let index = 0; index < batches.length; index += 1) {
|
|
4462
|
+
let scopeIds = batches[index].map(r => r.id);
|
|
4463
|
+
const finalScope = index === batches.length - 1;
|
|
4464
|
+
for (let repair = 0; repair < 2; repair += 1) {
|
|
4465
|
+
// Confirmed IDs remain visible until the model commits a scope checkpoint.
|
|
4466
|
+
const completed = input.tools.completedScopeRequirementIds();
|
|
4467
|
+
scopeIds = scopeIds.filter(id => !completed.has(id));
|
|
4468
|
+
input.tools.setActiveRequirementScope(scopeIds);
|
|
4469
|
+
const groupIndex = collectFrontendExecutionGroups(input.tools.committedFacts().filter(r => r.fact.kind === "requirement").map(r => ({ id: String(r.fact.id), execution: r.fact.execution })));
|
|
4470
|
+
const shared = input.tools.committedFacts().filter(r => !["requirement", "contract-finalized", "contract-scope-completed"].includes(String(r.fact.kind))).map(r => r.fact);
|
|
4471
|
+
const prompt = projectFrontendContractPrompt(basePrompt, scopeIds) +
|
|
4472
|
+
`\nCONTRACT SCOPE: analyze only ${scopeIds.join(", ") || "(all scopes complete; verify global facts and terminal)"}. Each obligation is complete. Submit small records immediately, then call complete_contract_scope after ALL decisions for this scope. ` +
|
|
4473
|
+
(finalScope ? "After complete scope coverage and source-bound deliverables, call finalize_contract. Correct rejected calls and retry." : "Do not finalize; subsequent complete scopes remain.") +
|
|
4474
|
+
`\n<committed_contract_facts>\n${JSON.stringify({ facts: shared, executionGroups: groupIndex })}\n</committed_contract_facts>`;
|
|
4475
|
+
const customTools = input.tools.customTools.filter(t => finalScope || t.name !== "finalize_contract");
|
|
4476
|
+
invocation += 1;
|
|
4477
|
+
if (invocation > maxSessions)
|
|
4478
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: "CONTRACT_RECOVERY_EXHAUSTED: session quota exceeded" };
|
|
4479
|
+
last = await observeFrontendSession({
|
|
4480
|
+
...input.observation, phase: "contract/scope", scopeIds, prompt, userMessage: input.sessionOptions.userMessage, customTools,
|
|
4481
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocation}.json` : undefined,
|
|
4482
|
+
committedCount: () => input.tools.committedFacts().length, durableCommittedCount: () => input.tools.committedFacts().length,
|
|
4483
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4484
|
+
await input.tools.flush();
|
|
4485
|
+
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
4486
|
+
return { ...last, ok: false };
|
|
4487
|
+
const capacityExhausted = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
4488
|
+
if (capacityExhausted && !last.timedOut) {
|
|
4489
|
+
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
4490
|
+
if (!missing.length) {
|
|
4491
|
+
if (finalScope && !terminal()) {
|
|
4492
|
+
if (!scopeIds.length)
|
|
4493
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: contract terminal exhausted; unchanged retry disabled` };
|
|
4494
|
+
batches.push([]);
|
|
4495
|
+
}
|
|
4496
|
+
break;
|
|
4497
|
+
}
|
|
4498
|
+
if (missing.length === 1 && scopeIds.length > 1) {
|
|
4499
|
+
batches.splice(index, 1, inventory.payload.requirements.filter(r => missing.includes(r.id)));
|
|
4500
|
+
index -= 1;
|
|
4501
|
+
break;
|
|
4502
|
+
}
|
|
4503
|
+
if (missing.length <= 1)
|
|
4504
|
+
return { ...last, ok: false, stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${missing[0] ?? "contract terminal"}; atom could not complete; refine the source without dropping conditions` };
|
|
4505
|
+
const smaller = packFrontendInputUnits(inventory.payload.requirements.filter(r => missing.includes(r.id)), { maxUnits: Math.ceil(missing.length / 2) });
|
|
4506
|
+
batches.splice(index, 1, ...smaller);
|
|
4507
|
+
index -= 1;
|
|
4508
|
+
break;
|
|
4509
|
+
}
|
|
4510
|
+
if (!last.ok)
|
|
4511
|
+
return last;
|
|
4512
|
+
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
4513
|
+
if (!missing.length && (!finalScope || terminal()))
|
|
4514
|
+
break;
|
|
4515
|
+
if (repair === 1)
|
|
4516
|
+
return { ...last, ok: false, failureCategory: "invalid-output", stderr: `CONTRACT_SCOPE_INCOMPLETE: ${missing.join(", ") || "missing finalize_contract"}` };
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
return last;
|
|
4520
|
+
}
|
|
4521
|
+
finally {
|
|
4522
|
+
input.tools.setActiveRequirementScope(null);
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4158
4525
|
export async function runFrontendPlanSegmentedSessions(input) {
|
|
4159
4526
|
const queue = [];
|
|
4160
4527
|
const coverageSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "coverage");
|
|
@@ -4163,10 +4530,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4163
4530
|
const globalMockDataSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "global-mock-data");
|
|
4164
4531
|
const finalizeSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "finalize");
|
|
4165
4532
|
const allRequirementIds = input.requirementIds ?? [];
|
|
4166
|
-
const buildPhasePrompt = (segment, missing = []) => {
|
|
4533
|
+
const buildPhasePrompt = (segment, missing = [], scopeIds = allRequirementIds) => {
|
|
4167
4534
|
const compact = allRequirementIds.length > 0
|
|
4168
|
-
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt,
|
|
4169
|
-
includeRequirementText: segment.id === "global-mock-data"
|
|
4535
|
+
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, scopeIds, {
|
|
4536
|
+
includeRequirementText: segment.id === "global-mock-data",
|
|
4170
4537
|
includeVerificationTargets: false,
|
|
4171
4538
|
includeDesignEvidence: segment.id === "global-dependency-deviation" || segment.id === "finalize",
|
|
4172
4539
|
includeChecklist: false,
|
|
@@ -4182,8 +4549,9 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4182
4549
|
const ledger = input.committedFacts
|
|
4183
4550
|
? compactFrontendPlanLedgerContext({
|
|
4184
4551
|
committedFacts: input.committedFacts(),
|
|
4185
|
-
requirementIds:
|
|
4552
|
+
requirementIds: scopeIds,
|
|
4186
4553
|
kinds,
|
|
4554
|
+
...(segment.id === "global-mock-data" ? { scopedKinds: ["state-flow"] } : {}),
|
|
4187
4555
|
})
|
|
4188
4556
|
: "";
|
|
4189
4557
|
return [
|
|
@@ -4221,13 +4589,13 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4221
4589
|
"component-choice",
|
|
4222
4590
|
"state-flow",
|
|
4223
4591
|
],
|
|
4224
|
-
scopedKinds: ["
|
|
4592
|
+
scopedKinds: ["state-flow"],
|
|
4225
4593
|
})
|
|
4226
4594
|
: "";
|
|
4227
4595
|
return [
|
|
4228
4596
|
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, slice),
|
|
4229
4597
|
uxSegment.instruction,
|
|
4230
|
-
`
|
|
4598
|
+
`UX SCOPE: process these complete behavior groups together: ${slice.join(", ")}. Reuse shared registry/component ownership; do not rename a behavior by requirement id. Constraint/exclusion groups do not create UI; preserve genuine verification gaps.`,
|
|
4231
4599
|
ledger,
|
|
4232
4600
|
...(missing.length > 0
|
|
4233
4601
|
? [
|
|
@@ -4251,6 +4619,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4251
4619
|
: "";
|
|
4252
4620
|
const compact = allRequirementIds.length > 0
|
|
4253
4621
|
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, allRequirementIds, {
|
|
4622
|
+
includeRequirementText: false,
|
|
4254
4623
|
includeVerificationTargets: false,
|
|
4255
4624
|
includeDesignEvidence: false,
|
|
4256
4625
|
includeChecklist: false,
|
|
@@ -4296,7 +4665,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4296
4665
|
: []),
|
|
4297
4666
|
].filter(Boolean).join("\n\n");
|
|
4298
4667
|
};
|
|
4299
|
-
const compactFinalizeInstruction = "This is a small-request compact pass. Reconcile the committed local facts with route, data-flow, Mock/API, dependency and deviation policy, then call finalize_plan exactly
|
|
4668
|
+
const compactFinalizeInstruction = "This is a small-request compact pass. Reconcile the committed local facts with route, data-flow, Mock/API, dependency and deviation policy, then call finalize_plan; correct rejected facts and retry until exactly one successful terminal commit.";
|
|
4300
4669
|
const buildCompactFinalizePrompt = (missing = []) => [buildPhasePrompt(finalizeSegment, missing), compactFinalizeInstruction].join("\n\n");
|
|
4301
4670
|
const mapPlannerExhaustion = (r, committedAnyFacts) => isPlannerThinkingExhausted(r, committedAnyFacts)
|
|
4302
4671
|
? {
|
|
@@ -4340,7 +4709,24 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4340
4709
|
tokensUsed: 0,
|
|
4341
4710
|
};
|
|
4342
4711
|
}
|
|
4343
|
-
const
|
|
4712
|
+
const compiledInput = parseFrontendInputBlock(input.basePrompt, "plan")?.payload;
|
|
4713
|
+
const fullUnits = new Map(compiledInput?.requirements.map(r => [r.id, r]) ?? []);
|
|
4714
|
+
const declaredGroups = Array.isArray(compiledInput?.executionGroups) ? compiledInput.executionGroups.filter(isRecordObject) : [];
|
|
4715
|
+
const workGroups = declaredGroups.length ? declaredGroups.map(g => ({ id: String(g.id), kind: String(g.kind), requirementIds: Array.isArray(g.requirementIds) ? g.requirementIds.filter((id) => typeof id === "string" && allRequirementIds.includes(id)) : [] })).filter(g => g.requirementIds.length) : allRequirementIds.map(id => ({ id, kind: "unclassified", requirementIds: [id] }));
|
|
4716
|
+
const groupedIds = new Set(workGroups.flatMap(g => g.requirementIds));
|
|
4717
|
+
for (const id of allRequirementIds)
|
|
4718
|
+
if (!groupedIds.has(id))
|
|
4719
|
+
workGroups.push({ id, kind: "unclassified", requirementIds: [id] });
|
|
4720
|
+
const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
|
|
4721
|
+
const buildWorkBatches = (ids) => {
|
|
4722
|
+
const work = workGroups.flatMap((g, index) => {
|
|
4723
|
+
const members = g.requirementIds.filter(id => ids.includes(id));
|
|
4724
|
+
return members.length ? [{ id: `${index}:${g.id}`, requirementIds: members, requirements: members.map(id => fullUnits.get(id) ?? { id }), estimatedCalls: workCost(members) }] : [];
|
|
4725
|
+
});
|
|
4726
|
+
const scaffoldBytes = Buffer.byteLength(input.basePrompt.replace(/<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/, JSON.stringify({ ...compiledInput, requirements: [] }))) + Buffer.byteLength(JSON.stringify(input.segmentCustomTools(coverageSegment.toolNames)));
|
|
4727
|
+
return packFrontendInputUnits(work, { targetBytes: Math.max(1, (input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES) - scaffoldBytes), maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits ?? 4, maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS, cost: g => g.estimatedCalls }).map(batch => batch.flatMap(g => g.requirementIds));
|
|
4728
|
+
};
|
|
4729
|
+
const estimatedCalls = workGroups.reduce((total, group) => total + workCost(group.requirementIds), 0);
|
|
4344
4730
|
const targetSurfaceCount = countFrontendPlanTargetSurfaces(input.basePrompt);
|
|
4345
4731
|
// Small, single-surface requests do not benefit from six isolated Pi
|
|
4346
4732
|
// sessions. Keep the typed ledger as the authority, but let one local
|
|
@@ -4350,13 +4736,14 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4350
4736
|
const useCompactSmallPlan = requirementIdsProvided &&
|
|
4351
4737
|
input.compactSmallPlan === true &&
|
|
4352
4738
|
input.requirementCosts !== undefined &&
|
|
4353
|
-
|
|
4354
|
-
(input.
|
|
4739
|
+
workGroups.length <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
|
|
4740
|
+
Buffer.byteLength(input.basePrompt) + Buffer.byteLength(JSON.stringify(input.segmentCustomTools(null))) <= (input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES) * 2 &&
|
|
4355
4741
|
estimatedCalls <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
|
|
4356
4742
|
targetSurfaceCount === 1;
|
|
4357
4743
|
if (useCompactSmallPlan) {
|
|
4358
4744
|
const compactLocalTools = new Set([
|
|
4359
4745
|
"record_plan_requirement",
|
|
4746
|
+
"record_plan_group_coverage",
|
|
4360
4747
|
"record_plan_verification_target",
|
|
4361
4748
|
"record_plan_evidence_gap",
|
|
4362
4749
|
"record_state_registry",
|
|
@@ -4384,13 +4771,8 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4384
4771
|
});
|
|
4385
4772
|
}
|
|
4386
4773
|
else if (coverageWorkIds.length > 0) {
|
|
4387
|
-
const
|
|
4388
|
-
|
|
4389
|
-
maxEstimatedRecordCalls: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS,
|
|
4390
|
-
maxRequirements: 4,
|
|
4391
|
-
requirementCosts: input.requirementCosts,
|
|
4392
|
-
});
|
|
4393
|
-
coverageBatches.forEach((slice, batchIndex) => queue.push({
|
|
4774
|
+
const completeBatches = buildWorkBatches(coverageWorkIds);
|
|
4775
|
+
completeBatches.forEach((slice, batchIndex) => queue.push({
|
|
4394
4776
|
id: `coverage-batch-${batchIndex + 1}`,
|
|
4395
4777
|
toolNames: coverageSegment.toolNames,
|
|
4396
4778
|
coverageSlice: slice,
|
|
@@ -4409,28 +4791,27 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4409
4791
|
prompt: buildUxRegistryPrompt(),
|
|
4410
4792
|
});
|
|
4411
4793
|
}
|
|
4412
|
-
const
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
: new Set([...(uxSegment.toolNames ?? []), "record_state_registry"]),
|
|
4421
|
-
...(uxSlice.length > 0 ? { requirementSlice: uxSlice } : {}),
|
|
4422
|
-
prompt: uxSlice.length > 0
|
|
4423
|
-
? buildUxPrompt(uxSlice)
|
|
4424
|
-
: buildPhasePrompt(uxSegment),
|
|
4425
|
-
});
|
|
4794
|
+
const uxWorkIds = workGroups.filter(g => !["constraint", "exclusion"].includes(g.kind) || g.requirementIds.some(id => input.behaviorRequiredRequirementIds?.includes(id))).flatMap(g => g.requirementIds);
|
|
4795
|
+
const uxBatches = requirementIdsProvided ? buildWorkBatches(uxWorkIds) : [[]];
|
|
4796
|
+
uxBatches.forEach((slice, batchIndex) => queue.push({
|
|
4797
|
+
id: `ux-local-${batchIndex + 1}`,
|
|
4798
|
+
toolNames: requirementIdsProvided ? uxSegment.toolNames : new Set([...(uxSegment.toolNames ?? []), "record_state_registry"]),
|
|
4799
|
+
...(slice.length ? { requirementSlice: slice } : {}),
|
|
4800
|
+
prompt: slice.length ? buildUxPrompt(slice) : buildPhasePrompt(uxSegment),
|
|
4801
|
+
}));
|
|
4426
4802
|
for (const segment of FRONTEND_PLAN_SEGMENTS) {
|
|
4427
4803
|
if (["coverage", "ux-registry", "ux-local", "finalize"].includes(segment.id))
|
|
4428
4804
|
continue;
|
|
4429
|
-
|
|
4430
|
-
id: segment.
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4805
|
+
if (segment.id === "global-mock-data" && requirementIdsProvided) {
|
|
4806
|
+
buildWorkBatches(allRequirementIds).forEach((slice, i) => queue.push({ id: `global-mock-data-${i + 1}`, toolNames: segment.toolNames, requirementSlice: slice, prompt: buildPhasePrompt(segment, [], slice) }));
|
|
4807
|
+
}
|
|
4808
|
+
else {
|
|
4809
|
+
queue.push({
|
|
4810
|
+
id: segment.id,
|
|
4811
|
+
toolNames: segment.toolNames,
|
|
4812
|
+
prompt: buildPhasePrompt(segment),
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4434
4815
|
}
|
|
4435
4816
|
queue.push({
|
|
4436
4817
|
id: finalizeSegment.id,
|
|
@@ -4441,6 +4822,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4441
4822
|
let accumulated;
|
|
4442
4823
|
let index = 0;
|
|
4443
4824
|
let invocationCount = 0;
|
|
4825
|
+
let lastDurableCount = input.committedFactCount();
|
|
4444
4826
|
while (index < queue.length) {
|
|
4445
4827
|
const session = queue[index];
|
|
4446
4828
|
const remaining = (session.coverageOnly ? session.coverageSlice ?? [] : []).filter((id) => !input.committedRequirementIds?.().has(id));
|
|
@@ -4460,7 +4842,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4460
4842
|
}
|
|
4461
4843
|
let prompt = session.prompt;
|
|
4462
4844
|
if (session.coverageOnly && session.coverageSlice) {
|
|
4463
|
-
const promptSlice = remaining.
|
|
4845
|
+
const promptSlice = [...new Set([...remaining, ...preexistingMissing.flatMap(item => item.requirementIds)])];
|
|
4464
4846
|
prompt = buildCoveragePrompt(promptSlice, session.missingFacts ?? preexistingMissing);
|
|
4465
4847
|
}
|
|
4466
4848
|
else if (session.id === "compact-local") {
|
|
@@ -4480,45 +4862,61 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4480
4862
|
// Global phases and finalize also consume the latest committed ledger;
|
|
4481
4863
|
// constructing their prompt only when the session starts prevents a
|
|
4482
4864
|
// stale queue entry from dropping facts written by earlier phases.
|
|
4483
|
-
const segment = FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === session.id);
|
|
4865
|
+
const segment = FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === session.id || (candidate.id === "global-mock-data" && session.id.startsWith("global-mock-data-")));
|
|
4484
4866
|
if (segment) {
|
|
4485
4867
|
prompt =
|
|
4486
4868
|
session.id === "finalize" && useCompactSmallPlan
|
|
4487
4869
|
? buildCompactFinalizePrompt(session.missingFacts)
|
|
4488
|
-
: buildPhasePrompt(segment, session.missingFacts);
|
|
4870
|
+
: buildPhasePrompt(segment, session.missingFacts, session.requirementSlice ?? allRequirementIds);
|
|
4489
4871
|
}
|
|
4490
4872
|
}
|
|
4491
4873
|
const committedBefore = input.committedFactCount();
|
|
4492
|
-
input.setActiveRequirementScope?.(session.coverageOnly
|
|
4493
|
-
session.id === "compact-local" ||
|
|
4494
|
-
session.id.startsWith("ux-local-")
|
|
4495
|
-
? session.coverageSlice ?? session.requirementSlice ?? []
|
|
4496
|
-
: []);
|
|
4874
|
+
input.setActiveRequirementScope?.(session.coverageOnly ? session.coverageSlice ?? [] : session.requirementSlice ?? []);
|
|
4497
4875
|
if (invocationCount >= FRONTEND_PLAN_BATCH_MAX_SESSIONS)
|
|
4498
4876
|
break;
|
|
4499
4877
|
invocationCount += 1;
|
|
4500
4878
|
const customTools = input.segmentCustomTools(session.toolNames);
|
|
4501
|
-
const
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4879
|
+
const scopeForPacking = session.coverageSlice ?? session.requirementSlice;
|
|
4880
|
+
const envelopeBytes = Buffer.byteLength(prompt) + Buffer.byteLength(input.sessionOptions.userMessage ?? "") + Buffer.byteLength(JSON.stringify(customTools));
|
|
4881
|
+
if (envelopeBytes > (input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES) && scopeForPacking && session.id !== "compact-local") {
|
|
4882
|
+
const groups = workGroups.map(g => g.requirementIds.filter(id => scopeForPacking.includes(id))).filter(g => g.length);
|
|
4883
|
+
if (groups.length > 1) {
|
|
4884
|
+
const half = Math.ceil(groups.length / 2);
|
|
4885
|
+
queue.splice(index, 1, ...[groups.slice(0, half).flat(), groups.slice(half).flat()].map(slice => ({ ...session, ...(session.coverageOnly ? { coverageSlice: slice } : { requirementSlice: slice }) })));
|
|
4886
|
+
invocationCount -= 1;
|
|
4887
|
+
continue;
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
const result = await observeFrontendSession({
|
|
4891
|
+
...input.observation, phase: `plan/${session.id}`, scopeIds: session.requirementSlice ?? session.coverageSlice ?? allRequirementIds,
|
|
4892
|
+
prompt, userMessage: input.sessionOptions.userMessage, customTools, committedCount: input.committedFactCount, durableCommittedCount: () => lastDurableCount,
|
|
4893
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocationCount}.json` : undefined,
|
|
4894
|
+
}, async (observer) => {
|
|
4895
|
+
const result = await input.piStepFn({
|
|
4896
|
+
...input.sessionOptions,
|
|
4897
|
+
onAttemptObservation: observer,
|
|
4898
|
+
prompt,
|
|
4899
|
+
...(customTools.length > 0
|
|
4900
|
+
? {
|
|
4901
|
+
writerToolPolicy: {
|
|
4902
|
+
requireSdk: true,
|
|
4903
|
+
customTools,
|
|
4904
|
+
},
|
|
4905
|
+
}
|
|
4906
|
+
: {}),
|
|
4907
|
+
});
|
|
4908
|
+
try {
|
|
4909
|
+
await input.flushLedger();
|
|
4910
|
+
lastDurableCount = input.committedFactCount();
|
|
4911
|
+
}
|
|
4912
|
+
catch {
|
|
4913
|
+
// best-effort: the node-level flush runs again after the attempt
|
|
4914
|
+
}
|
|
4915
|
+
return result;
|
|
4512
4916
|
});
|
|
4513
4917
|
accumulated = accumulated
|
|
4514
4918
|
? combineSequentialPiResults(accumulated, result)
|
|
4515
4919
|
: result;
|
|
4516
|
-
try {
|
|
4517
|
-
await input.flushLedger();
|
|
4518
|
-
}
|
|
4519
|
-
catch {
|
|
4520
|
-
// best-effort: the node-level flush runs again after the attempt
|
|
4521
|
-
}
|
|
4522
4920
|
const committedAfter = input.committedFactCount();
|
|
4523
4921
|
const committedFactsOnlySuccess = session.id !== "finalize" &&
|
|
4524
4922
|
!(result.assistantText ?? "").trim() &&
|
|
@@ -4565,10 +4963,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4565
4963
|
committedFacts: input.committedFacts(),
|
|
4566
4964
|
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
4567
4965
|
})
|
|
4568
|
-
: session.id
|
|
4966
|
+
: session.id.startsWith("global-mock-data") && allRequirementIds.length > 0 && input.committedFacts
|
|
4569
4967
|
? collectFrontendPlanPhaseMissingFacts({
|
|
4570
4968
|
phase: "global-mock-data",
|
|
4571
|
-
requirementIds: allRequirementIds,
|
|
4969
|
+
requirementIds: session.requirementSlice ?? allRequirementIds,
|
|
4572
4970
|
committedFacts: input.committedFacts(),
|
|
4573
4971
|
})
|
|
4574
4972
|
: [];
|
|
@@ -4605,7 +5003,58 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4605
5003
|
? buildCompactFinalizePrompt(missing)
|
|
4606
5004
|
: isUxLocalSession
|
|
4607
5005
|
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
4608
|
-
: buildPhasePrompt(globalMockDataSegment, missing);
|
|
5006
|
+
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
|
|
5007
|
+
if (result.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(result.failureCategory))
|
|
5008
|
+
return { ...result, ok: false };
|
|
5009
|
+
const capacityExhausted = readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory);
|
|
5010
|
+
if (capacityExhausted && !result.timedOut) {
|
|
5011
|
+
const failure = () => mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; no smaller complete scope can finish; unchanged retries are disabled` }, committedAfter > committedBefore);
|
|
5012
|
+
const missingCoverageIds = input.committedFacts ? [...new Set(collectFrontendPlanMissingFacts({ requirementIds: session.coverageSlice ?? session.requirementSlice ?? allRequirementIds, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds))] : [...(session.coverageSlice ?? session.requirementSlice ?? allRequirementIds)];
|
|
5013
|
+
const splitScope = (ids, allowMemberSplit) => {
|
|
5014
|
+
const groups = workGroups.map(g => g.requirementIds.filter(id => ids.includes(id))).filter(g => g.length);
|
|
5015
|
+
if (groups.length > 1) {
|
|
5016
|
+
const half = Math.ceil(groups.length / 2);
|
|
5017
|
+
return [groups.slice(0, half).flat(), groups.slice(half).flat()];
|
|
5018
|
+
}
|
|
5019
|
+
if (allowMemberSplit && ids.length > 1) {
|
|
5020
|
+
const half = Math.ceil(ids.length / 2);
|
|
5021
|
+
return [ids.slice(0, half), ids.slice(half)];
|
|
5022
|
+
}
|
|
5023
|
+
return [];
|
|
5024
|
+
};
|
|
5025
|
+
if (isCompactLocalSession) {
|
|
5026
|
+
let scopes = splitScope(missingCoverageIds, true);
|
|
5027
|
+
if (!scopes.length && missingCoverageIds.length) {
|
|
5028
|
+
if (missingCoverageIds.length === allRequirementIds.length && committedAfter === committedBefore)
|
|
5029
|
+
return failure();
|
|
5030
|
+
scopes = [missingCoverageIds];
|
|
5031
|
+
}
|
|
5032
|
+
// UX ownership is independent of coverage completion. Preserve all groups.
|
|
5033
|
+
const recovery = scopes.map((slice, i) => ({ id: `coverage-capacity-${invocationCount}-${i}`, coverageOnly: true, coverageSlice: slice, toolNames: coverageSegment.toolNames, prompt: buildCoveragePrompt(slice) }));
|
|
5034
|
+
recovery.push({ id: "ux-registry", toolNames: uxRegistrySegment.toolNames, prompt: buildUxRegistryPrompt() });
|
|
5035
|
+
recovery.push(...buildWorkBatches([...allRequirementIds]).map((slice, i) => ({ id: `ux-local-capacity-${invocationCount}-${i}`, requirementSlice: slice, toolNames: uxSegment.toolNames, prompt: buildUxPrompt(slice) })));
|
|
5036
|
+
queue.splice(index, 1, ...recovery);
|
|
5037
|
+
continue;
|
|
5038
|
+
}
|
|
5039
|
+
if (session.coverageOnly || isUxLocalSession) {
|
|
5040
|
+
const missingIds = session.coverageOnly ? missingCoverageIds : [...new Set(missingPhase.flatMap(f => f.requirementIds))];
|
|
5041
|
+
if (!missingIds.length) {
|
|
5042
|
+
index += 1;
|
|
5043
|
+
continue;
|
|
5044
|
+
}
|
|
5045
|
+
const scopes = splitScope(missingIds, Boolean(session.coverageOnly));
|
|
5046
|
+
if (scopes.length) {
|
|
5047
|
+
queue.splice(index, 1, ...scopes.map(slice => ({ ...session, ...(session.coverageOnly ? { coverageSlice: slice } : { requirementSlice: slice }), missingFacts: missingPhaseFacts.filter(f => f.requirementIds.some(id => slice.includes(id))), retryCount: 0 })));
|
|
5048
|
+
continue;
|
|
5049
|
+
}
|
|
5050
|
+
const originalIds = session.coverageSlice ?? session.requirementSlice ?? [];
|
|
5051
|
+
if ((missingIds.length < originalIds.length || committedAfter > committedBefore) && (session.retryCount ?? 0) < 2) {
|
|
5052
|
+
queue[index] = { ...session, ...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }), missingFacts: missingPhaseFacts, retryCount: (session.retryCount ?? 0) + 1 };
|
|
5053
|
+
continue;
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
return failure();
|
|
5057
|
+
}
|
|
4609
5058
|
if (result.ok) {
|
|
4610
5059
|
if (missingPhaseFacts.length === 0) {
|
|
4611
5060
|
index += 1;
|
|
@@ -4649,24 +5098,6 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4649
5098
|
index += 1;
|
|
4650
5099
|
continue;
|
|
4651
5100
|
}
|
|
4652
|
-
// A length-stopped, fact-less compact session is the planner variant of
|
|
4653
|
-
// writer-thinking-exhausted. Give the same scope exactly one tool-first
|
|
4654
|
-
// retry before the split below re-batches the requirements, because one
|
|
4655
|
-
// reinforced full-scope pass is cheaper than re-planning split halves.
|
|
4656
|
-
const plannerThinkingBurn = isCompactLocalSession &&
|
|
4657
|
-
committedAfter === committedBefore &&
|
|
4658
|
-
!(result.assistantText ?? "").trim() &&
|
|
4659
|
-
!result.stderr.trim() &&
|
|
4660
|
-
!result.timedOut &&
|
|
4661
|
-
readWriterThinkingExhaustionEvidence(result).stopReason === "length";
|
|
4662
|
-
if (plannerThinkingBurn && (session.retryCount ?? 0) < 1) {
|
|
4663
|
-
queue[index] = {
|
|
4664
|
-
...session,
|
|
4665
|
-
retryCount: (session.retryCount ?? 0) + 1,
|
|
4666
|
-
prompt: buildCompactLocalPrompt(),
|
|
4667
|
-
};
|
|
4668
|
-
continue;
|
|
4669
|
-
}
|
|
4670
5101
|
// Option 5: a multi-requirement coverage batch that failed with ZERO
|
|
4671
5102
|
// new facts and no provider stderr is the upfront-reasoning burn —
|
|
4672
5103
|
// halve the slice and retry instead of failing the attempt. The compact
|
|
@@ -4681,20 +5112,24 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4681
5112
|
: undefined;
|
|
4682
5113
|
const zeroProgressBurn = coverageSlice !== undefined &&
|
|
4683
5114
|
coverageSlice.length > 1 &&
|
|
4684
|
-
committedAfter === committedBefore &&
|
|
4685
|
-
!(result.assistantText ?? "").trim() &&
|
|
4686
|
-
!result.stderr.trim() &&
|
|
5115
|
+
(readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory) || (committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
|
|
4687
5116
|
!result.timedOut;
|
|
4688
5117
|
if (zeroProgressBurn && coverageSlice) {
|
|
4689
|
-
const
|
|
4690
|
-
const
|
|
4691
|
-
|
|
5118
|
+
const missingIds = input.committedFacts ? new Set(collectFrontendPlanMissingFacts({ requirementIds: coverageSlice, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds)) : undefined;
|
|
5119
|
+
const unfinished = coverageSlice.filter(id => !missingIds || missingIds.has(id));
|
|
5120
|
+
if (unfinished.length <= 1)
|
|
5121
|
+
return mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${unfinished[0] ?? session.id}; no smaller complete scope can finish` }, committedAfter > committedBefore);
|
|
5122
|
+
const half = Math.ceil(unfinished.length / 2);
|
|
5123
|
+
const firstSlice = unfinished.slice(0, half);
|
|
5124
|
+
const secondSlice = unfinished.slice(half);
|
|
4692
5125
|
if (isCompactLocalSession) {
|
|
5126
|
+
queue.splice(index + 1, 0, { id: "ux-registry", toolNames: uxRegistrySegment.toolNames, prompt: buildUxRegistryPrompt() }, ...buildWorkBatches([...firstSlice, ...secondSlice]).map((slice, i) => ({ id: `ux-local-recovery-${i}`, toolNames: uxSegment.toolNames, requirementSlice: slice, prompt: buildUxPrompt(slice) })));
|
|
4693
5127
|
// The split halves leave compact mode: continue them as ordinary
|
|
4694
5128
|
// coverage sessions so every downstream ladder branch applies.
|
|
4695
5129
|
queue.splice(index, 1, {
|
|
4696
5130
|
...session,
|
|
4697
5131
|
id: `coverage-compact-split-1`,
|
|
5132
|
+
toolNames: coverageSegment.toolNames, retryCount: 0,
|
|
4698
5133
|
coverageOnly: true,
|
|
4699
5134
|
coverageSlice: firstSlice,
|
|
4700
5135
|
requirementSlice: firstSlice,
|
|
@@ -4702,6 +5137,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4702
5137
|
}, {
|
|
4703
5138
|
...session,
|
|
4704
5139
|
id: `coverage-compact-split-2`,
|
|
5140
|
+
toolNames: coverageSegment.toolNames, retryCount: 0,
|
|
4705
5141
|
coverageOnly: true,
|
|
4706
5142
|
coverageSlice: secondSlice,
|
|
4707
5143
|
requirementSlice: secondSlice,
|
|
@@ -4720,7 +5156,9 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4720
5156
|
});
|
|
4721
5157
|
continue;
|
|
4722
5158
|
}
|
|
4723
|
-
if ((
|
|
5159
|
+
if (readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory))
|
|
5160
|
+
return mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; refine the remaining complete scope; unchanged retries are disabled` }, committedAfter > committedBefore);
|
|
5161
|
+
if ((session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data")) &&
|
|
4724
5162
|
missingPhaseFacts.length > 0 &&
|
|
4725
5163
|
!result.stderr.trim() &&
|
|
4726
5164
|
!result.timedOut &&
|
|
@@ -4852,14 +5290,27 @@ async function runFrontendScoutParallelSessions(input) {
|
|
|
4852
5290
|
...input.sessionOptions,
|
|
4853
5291
|
sessionEventsPath: path.join(input.runDir, shardNodeId, "session-events.jsonl"),
|
|
4854
5292
|
};
|
|
4855
|
-
|
|
5293
|
+
const prompt = `${input.basePrompt}\n\n${shard.instruction}`;
|
|
5294
|
+
const tools = [...customTools, ...(input.readBudgetTools ?? [])];
|
|
5295
|
+
shardResult = await observeFrontendSession({
|
|
5296
|
+
...input.observation,
|
|
5297
|
+
phase: `scout/parallel/${shard.id}`,
|
|
5298
|
+
scopeIds: [shard.id],
|
|
5299
|
+
prompt,
|
|
5300
|
+
userMessage: sessionOptions.userMessage,
|
|
5301
|
+
customTools: tools,
|
|
5302
|
+
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${shard.id}.json` : undefined,
|
|
5303
|
+
committedCount: () => shardTools?.committedFacts().length ?? 0,
|
|
5304
|
+
durableCommittedCount: () => shardTools?.committedFacts().length ?? 0,
|
|
5305
|
+
}, observer => input.piStepFn({
|
|
4856
5306
|
...sessionOptions,
|
|
4857
|
-
|
|
5307
|
+
onAttemptObservation: observer,
|
|
5308
|
+
prompt,
|
|
4858
5309
|
writerToolPolicy: {
|
|
4859
5310
|
requireSdk: true,
|
|
4860
|
-
customTools:
|
|
5311
|
+
customTools: tools,
|
|
4861
5312
|
},
|
|
4862
|
-
});
|
|
5313
|
+
}));
|
|
4863
5314
|
await shardTools.flush();
|
|
4864
5315
|
return { shard, result: shardResult, tools: shardTools };
|
|
4865
5316
|
}
|
|
@@ -5035,6 +5486,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5035
5486
|
let writerToolPolicy;
|
|
5036
5487
|
let reviewTerminalTools;
|
|
5037
5488
|
let designTerminalTools;
|
|
5489
|
+
let reviewInventory;
|
|
5490
|
+
let designInventory;
|
|
5038
5491
|
let planLedgerTools;
|
|
5039
5492
|
let contractTools;
|
|
5040
5493
|
let scoutEvidenceTools;
|
|
@@ -5128,6 +5581,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5128
5581
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5129
5582
|
const store = createTypedEventStore();
|
|
5130
5583
|
reviewTerminalTools = await createFrontendReviewTerminalTools({
|
|
5584
|
+
inventory: reviewInventory = await loadFrontendReviewScopes(meta.runDir, "review", meta.spec?.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES),
|
|
5585
|
+
inputDigest: reviewInventory?.digest ?? createHash("sha256").update(input.prompt).digest("hex"),
|
|
5131
5586
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5132
5587
|
store,
|
|
5133
5588
|
runDir: meta.runDir,
|
|
@@ -5153,6 +5608,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5153
5608
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5154
5609
|
const store = createTypedEventStore();
|
|
5155
5610
|
designTerminalTools = await createFrontendDesignTerminalTools({
|
|
5611
|
+
inventory: designInventory = await loadFrontendReviewScopes(meta.runDir, "design", meta.spec?.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES),
|
|
5612
|
+
inputDigest: designInventory?.digest ?? createHash("sha256").update(input.prompt).digest("hex"),
|
|
5156
5613
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5157
5614
|
store,
|
|
5158
5615
|
runDir: meta.runDir,
|
|
@@ -5178,6 +5635,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5178
5635
|
const { createTypedEventStore } = await import("../workflows/dag/frontend-typed-event-store.js");
|
|
5179
5636
|
const store = createTypedEventStore();
|
|
5180
5637
|
contractTools = await createFrontendContractTools({
|
|
5638
|
+
sourceDigest: (meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined),
|
|
5181
5639
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5182
5640
|
store,
|
|
5183
5641
|
runDir: meta.runDir,
|
|
@@ -5211,6 +5669,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5211
5669
|
spec: meta.spec,
|
|
5212
5670
|
});
|
|
5213
5671
|
scoutEvidenceTools = await createFrontendScoutEvidenceTools({
|
|
5672
|
+
requirementIds: parseFrontendInputBlock(input.prompt, "scout")?.payload.requirements.map(r => r.id),
|
|
5673
|
+
sourceDigest: (meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined),
|
|
5214
5674
|
attemptId: `${meta.runId}:${input.task.id}`,
|
|
5215
5675
|
store,
|
|
5216
5676
|
runDir: meta.runDir,
|
|
@@ -5368,6 +5828,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5368
5828
|
activeTools: toolNames,
|
|
5369
5829
|
});
|
|
5370
5830
|
const piSessionOptions = {
|
|
5831
|
+
reserveProviderRequest: input.reserveProviderRequest,
|
|
5832
|
+
frontendExecutionPolicy: meta.spec?.frontendExecutionPolicy,
|
|
5371
5833
|
attachedFiles: [],
|
|
5372
5834
|
modelConfig,
|
|
5373
5835
|
repoRoot: input.cwd,
|
|
@@ -5409,6 +5871,15 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5409
5871
|
workspaceRoot: input.cwd,
|
|
5410
5872
|
sourceDeclaredPaths: scoutSourceDeclaredPaths,
|
|
5411
5873
|
readBudgetTools: readBudgetTools?.customTools,
|
|
5874
|
+
observation: {
|
|
5875
|
+
artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
5876
|
+
runId: meta.runId,
|
|
5877
|
+
nodeId: input.task.id,
|
|
5878
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
5879
|
+
attempt: input.attempt ?? 1,
|
|
5880
|
+
model: input.model,
|
|
5881
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
5882
|
+
},
|
|
5412
5883
|
});
|
|
5413
5884
|
}
|
|
5414
5885
|
else if (isFrontendPlanLedgerNode(input.task) && planLedgerTools) {
|
|
@@ -5448,6 +5919,16 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5448
5919
|
// finalize reducer. This avoids revision races while shortening the
|
|
5449
5920
|
// longest coverage phase for large plans.
|
|
5450
5921
|
const runPlanSessions = (options) => runFrontendPlanSegmentedSessions({
|
|
5922
|
+
observation: options.observation ?? {
|
|
5923
|
+
artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
5924
|
+
runId: meta.runId,
|
|
5925
|
+
nodeId: input.task.id,
|
|
5926
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
5927
|
+
attempt: input.attempt ?? 1,
|
|
5928
|
+
model: input.model,
|
|
5929
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
5930
|
+
contractDigest: meta.spec?.taskContractBinding?.canonicalHash,
|
|
5931
|
+
},
|
|
5451
5932
|
piStepFn,
|
|
5452
5933
|
sessionOptions: options.sessionOptions,
|
|
5453
5934
|
basePrompt: options.basePrompt ?? input.prompt,
|
|
@@ -5527,6 +6008,16 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5527
6008
|
requirementIds: slice,
|
|
5528
6009
|
basePrompt: `${input.prompt}\n\nPARALLEL COVERAGE SHARD ${index + 1}: use the frozen canonical behavior verification-target ids listed in the record_plan_verification_target tool description — do not prefix ids with a shard namespace or invent variant ids; identical cross-shard targets are deduped, divergent ones fail the merge.`,
|
|
5529
6010
|
parallelCoverageOnly: true,
|
|
6011
|
+
observation: {
|
|
6012
|
+
artifactPath: path.join(meta.runDir, input.task.id, "parallel", `coverage-${index + 1}`, "session-budget", `attempt-${input.attempt ?? 1}`),
|
|
6013
|
+
runId: meta.runId,
|
|
6014
|
+
nodeId: `${input.task.id}/parallel/coverage-${index + 1}`,
|
|
6015
|
+
taskId: meta.spec?.sourceBinding?.taskId,
|
|
6016
|
+
attempt: input.attempt ?? 1,
|
|
6017
|
+
model: input.model,
|
|
6018
|
+
sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined,
|
|
6019
|
+
contractDigest: meta.spec?.taskContractBinding?.canonicalHash,
|
|
6020
|
+
},
|
|
5530
6021
|
});
|
|
5531
6022
|
await shardTools.flush();
|
|
5532
6023
|
return { index, result: shardResult, tools: shardTools };
|
|
@@ -5626,12 +6117,31 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5626
6117
|
});
|
|
5627
6118
|
}
|
|
5628
6119
|
}
|
|
6120
|
+
else if (contractTools) {
|
|
6121
|
+
result = await runFrontendContractSegmentedSessions({
|
|
6122
|
+
piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, tools: contractTools,
|
|
6123
|
+
observation: { artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`), runId: meta.runId, nodeId: input.task.id, taskId: meta.spec?.sourceBinding?.taskId, attempt: input.attempt ?? 1, model: input.model, sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined },
|
|
6124
|
+
});
|
|
6125
|
+
}
|
|
6126
|
+
else if ((reviewInventory && reviewTerminalTools) || (designInventory && designTerminalTools)) {
|
|
6127
|
+
result = await runFrontendReviewSegmentedSessions({ piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, inventory: (reviewInventory ?? designInventory), tools: (reviewTerminalTools ?? designTerminalTools), customTools: writerToolPolicy.customTools, phase: reviewInventory ? "review" : "design",
|
|
6128
|
+
observation: { artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`), runId: meta.runId, nodeId: input.task.id, taskId: meta.spec?.sourceBinding?.taskId, attempt: input.attempt ?? 1, model: input.model, contractDigest: (reviewInventory ?? designInventory).digest } });
|
|
6129
|
+
}
|
|
6130
|
+
else if (scoutEvidenceTools && parseFrontendInputBlock(input.prompt, "scout")) {
|
|
6131
|
+
result = await runFrontendScoutSegmentedSessions({ piStepFn, sessionOptions: piSessionOptions, basePrompt: input.prompt, tools: scoutEvidenceTools, customTools: writerToolPolicy.customTools,
|
|
6132
|
+
observation: { artifactPath: path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}`), runId: meta.runId, nodeId: input.task.id, taskId: meta.spec?.sourceBinding?.taskId, attempt: input.attempt ?? 1, model: input.model, sourceDigest: meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined } });
|
|
6133
|
+
}
|
|
5629
6134
|
else {
|
|
5630
|
-
result = await
|
|
6135
|
+
result = await observeFrontendSession({
|
|
6136
|
+
artifactPath: input.task.id.startsWith("frontend-") ? path.join(meta.runDir, input.task.id, "session-budget", `attempt-${input.attempt ?? 1}.json`) : undefined,
|
|
6137
|
+
phase: input.task.id, prompt: input.prompt, userMessage: piSessionOptions.userMessage, customTools: writerToolPolicy?.customTools,
|
|
6138
|
+
runId: meta.runId, nodeId: input.task.id, taskId: meta.spec?.sourceBinding?.taskId, attempt: input.attempt ?? 1, model: input.model, sourceDigest: (meta.spec?.sourceBinding?.schemaVersion === 2 ? meta.spec.sourceBinding.inputDigest : undefined), contractDigest: meta.spec?.taskContractBinding?.canonicalHash,
|
|
6139
|
+
}, observer => piStepFn({
|
|
5631
6140
|
...piSessionOptions,
|
|
6141
|
+
onAttemptObservation: observer,
|
|
5632
6142
|
prompt: input.prompt,
|
|
5633
6143
|
...(writerToolPolicy ? { writerToolPolicy } : {}),
|
|
5634
|
-
});
|
|
6144
|
+
}));
|
|
5635
6145
|
}
|
|
5636
6146
|
try {
|
|
5637
6147
|
const verificationCommandFiles = collectFrontendVerificationCommandFiles(input.prompt);
|
|
@@ -5796,8 +6306,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
5796
6306
|
try {
|
|
5797
6307
|
await scoutEvidenceTools?.flush?.();
|
|
5798
6308
|
}
|
|
5799
|
-
catch {
|
|
5800
|
-
|
|
6309
|
+
catch (error) {
|
|
6310
|
+
return { ...mapped, ok: false, failureCategory: mapped.ok ? "frontend-ledger-invalid" : mapped.failureCategory, stderr: `${mapped.stderr}\nFRONTEND_LEDGER_INTEGRITY_INVALID: ${error instanceof Error ? error.message : String(error)}` };
|
|
5801
6311
|
}
|
|
5802
6312
|
if (mapped.ok) {
|
|
5803
6313
|
const { checkCommittedOriginFacts, readCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
|