@tea-agent/loop-agent 0.42.0-next.13 → 0.42.0-next.15
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 +25 -2
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +617 -94
- package/dist/executors/shell-executor.js +4 -0
- package/dist/worker/console/chat/pi-runtime.js +45 -2
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +22 -11
- package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
- package/dist/worker/console/chat/session-store.js +5 -1
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/tool-preview.js +10 -0
- package/dist/worker/console/chat/tools.js +11 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BZogYNl1.js → abnfDiagram-N423BO3Z-CgXb0EVO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DcJTLe2v.js → arc-DN59MZqN.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B39EQQ9n.js → architectureDiagram-T3A2C74G-BUk3sWpn.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CitCGV56.js → blockDiagram-VBNYF7ZC-IH-cPBFE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-rvWuOHLG.js → c4Diagram-5PPSVZJV-BJWf1nGo.js} +1 -1
- package/dist/worker/console/static/assets/channel-i1DjpDIw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DGXplIJs.js → chunk-2GRJ4B5K-BCPb5H0y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ur9OqjPn.js → chunk-2Q5K7J3B-Cfs3jeW2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-D1KqU_Vi.js → chunk-5RXB4S5H-BhQI1B_T.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BlVZrbtM.js → chunk-5VM5RSS4-Cc_m4gyV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAzz3nQy.js → chunk-6Q2QTUOP-JPqDS3IV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CP_nYuCV.js → chunk-GF5L2VYU-AeGX6EAg.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CWvKQ80X.js → chunk-JWPE2WC7-pEyTjekV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CyUki8wp.js → chunk-KBJHAD2P-Bi5UopKd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-5NYzNKfY.js → chunk-RYQCIY6F-FkGFJxgQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DHdUgjS-.js → chunk-XXDRQBXY-D-7LGrV2.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ANQ5g_t6.js → cose-bilkent-JH36ORCC-CCEDNDaX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CruT_4-0.js → cynefin-VYW2F7L2-DDd-t7fm.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-rbFo77ID.js → cynefinDiagram-MW4NZA55-LWe4Ikzr.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-asstxBQs.js → dagre-VZM6K2ZE-BBCZScc-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DzTyBZwI.js → diagram-7IWD3JNH-B6liRVig.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-gw8ccH3B.js → diagram-B4RE2ZJO-DO_hIrpW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B8RpZzOf.js → diagram-LBJQPF4R-dmCy93uR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B1zJkCS9.js → diagram-Q27KOJAE-D6mFTIxP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-B6JLWZmO.js → diagram-UB23O5K3-DCteVUfY.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-q2iZP-Ps.js → ebnfDiagram-BXEA7PRR-DTv530VK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B3dbbktQ.js → erDiagram-JOGREHBK-DCXDMxCr.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C-zc6-kB.js → flowDiagram-UKHOOZJN-BwEBLOJh.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BSolnot9.js → ganttDiagram-PKOTCBZU-Ck-Vymjm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bo8jGxTk.js → gitGraphDiagram-DS77QQ5N-Hqs6X_3L.js} +1 -1
- package/dist/worker/console/static/assets/{index-DhfjOafu.js → index-BmMi-Bve.js} +82 -82
- package/dist/worker/console/static/assets/{index-CAZ2fC_X.css → index-Dh0Gh4X0.css} +1 -1
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQaZqJOH.js → infoDiagram-6WML65LV-Cjc6M9eg.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DsdX1WnE.js → ishikawaDiagram-WSZJBQD7-CPchYZMl.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CP5EYDZL.js → journeyDiagram-NVQOT4AX-BABdJNNC.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DAm14Dr8.js → kanban-definition-27J2QSJJ-1oXhbM4j.js} +1 -1
- package/dist/worker/console/static/assets/{linear-gZECoLc5.js → linear-PTmQ9LkV.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BRJNIM6p.js → mermaid.core-B6Lxil0V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-Cja3kLkR.js → mindmap-definition-FAOFIHXS-BUqjNGEw.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-22ZGFisL.js → pegDiagram-VL7TDLO6-8D1N-orJ.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BXin0Y1o.js → pieDiagram-7S7Q4E2Y-BlPMN9d9.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cw6T69Yt.js → quadrantDiagram-CIZ2JOQS-BIL5YDes.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-C1-gTieN.js → railroadDiagram-AXF67PYL-Dqmil3ie.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP--H5N2iI1.js → requirementDiagram-LRYGKXZP-C_-Dp4mH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CgWAPeRz.js → sankeyDiagram-W5VNT64P-C0s5-bQS.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CrKqzGvt.js → sequenceDiagram-SI44F4Z6-DiVCHPfi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BB3wG06K.js → sizeCapture-X5ZJPWSS-CxQ9WVvx.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-DjsGgdgY.js → stateDiagram-OKZ733FA-D9mFHA-Y.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqXQxfFC.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DYKEbvAR.js → swimlanes-SLNWSIFB-DDnC5l-M.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CXwWNRJl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-eQOb5k9Y.js → timeline-definition-Z64GVDOM-C8QySPC-.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B2-l9FIu.js → vennDiagram-T6HMQDX7-BeYdDiz6.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BX2Cda3l.js → wardleyDiagram-T6FBY63Y-D8hjFWLH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D9S892ph.js → xychartDiagram-ELKLHX3M-ClqG_JGK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +21 -1
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +99 -14
- package/dist/workflows/dag/frontend-implementation-contract.js +56 -8
- package/dist/workflows/dag/frontend-plan-render.js +13 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
- package/dist/workflows/dag/init-hybrid.js +7 -3
- package/dist/workflows/dag/node-execution.js +54 -16
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/harness.json +1 -1
- package/package.json +2 -3
- package/skills/frontend-plan/SKILL.md +8 -0
- package/skills/frontend-plan/references/decision-contract.md +26 -3
- package/dist/worker/console/static/assets/channel-CXqdP5pW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D9C3agF7.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D9C3agF7.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Tp7V0U4q.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DUZWZWi8.js +0 -8
|
@@ -12,10 +12,13 @@ import { createPiReadBudgetCustomTools, } from "./pi-read-budget-policy.js";
|
|
|
12
12
|
import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND_CAPABILITY_REGISTRY, resolveCaseIdFromWriteSet, resolveEvidenceDirFromWriteSet, } from "./pi-playwright-cli-tool.js";
|
|
13
13
|
import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
|
|
14
14
|
import { parseLedgerJson } from "../task/source-prepare/ledger.js";
|
|
15
|
+
import { splitDocumentIntoFragments, sha256Text, } from "../task/source-prepare/fragment-inventory.js";
|
|
15
16
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
16
17
|
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
17
18
|
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
18
19
|
import { pathMatchesPattern } from "../shared/git-progress.js";
|
|
20
|
+
import { readTypedEventStoreFromJsonl } from "../workflows/dag/frontend-typed-event-store.js";
|
|
21
|
+
import { collectCanonicalStateFlowNames, frontendEvidenceExpectationSchema, resolveFrontendContractRequirements, validateFrontendRequiredDeliverables, } from "../workflows/dag/frontend-contract-facts.js";
|
|
19
22
|
import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
20
23
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestCollectionRepairOutcomeRecoveryCandidate, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
21
24
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
@@ -251,6 +254,8 @@ export const FRONTEND_CONTRACT_RECORD_TOOL_NAMES = [
|
|
|
251
254
|
"record_handoff_intent",
|
|
252
255
|
"record_open_question",
|
|
253
256
|
"record_split_proposal",
|
|
257
|
+
"record_ui_state",
|
|
258
|
+
"record_required_deliverables",
|
|
254
259
|
];
|
|
255
260
|
export const FRONTEND_CONTRACT_TERMINAL_TOOL_NAMES = [
|
|
256
261
|
"finalize_contract",
|
|
@@ -264,6 +269,7 @@ export const FRONTEND_SCOUT_EVIDENCE_TOOL_NAMES = [
|
|
|
264
269
|
export const FRONTEND_PLAN_RECORD_TOOL_NAMES = [
|
|
265
270
|
"record_route_selection",
|
|
266
271
|
"record_component_choice",
|
|
272
|
+
"record_state_registry",
|
|
267
273
|
"record_state_flow",
|
|
268
274
|
"record_data_flow",
|
|
269
275
|
"record_mock_api",
|
|
@@ -907,8 +913,81 @@ async function resolveFrontendPlanNewComponentSourceReferences(input) {
|
|
|
907
913
|
}
|
|
908
914
|
}
|
|
909
915
|
/**
|
|
910
|
-
*
|
|
911
|
-
*
|
|
916
|
+
* Frozen canonical requirements keyed by id, resolved from the source-fidelity
|
|
917
|
+
* ledger before the contract node starts. record_requirement commits these
|
|
918
|
+
* runtime-owned values so the model can never rewrite authoritative requirement
|
|
919
|
+
* text or stringify the fragment bindings.
|
|
920
|
+
*/
|
|
921
|
+
async function resolveFrontendCanonicalRequirements(input) {
|
|
922
|
+
const binding = input.sourceBinding;
|
|
923
|
+
if (!binding || binding.schemaVersion !== 2 || !binding.ledgerPath) {
|
|
924
|
+
return new Map();
|
|
925
|
+
}
|
|
926
|
+
const absolutePath = path.resolve(input.cwd, binding.ledgerPath);
|
|
927
|
+
const workspaceRoot = path.resolve(input.cwd);
|
|
928
|
+
if (absolutePath !== workspaceRoot &&
|
|
929
|
+
!absolutePath.startsWith(`${workspaceRoot}${path.sep}`)) {
|
|
930
|
+
throw new Error("canonical requirement ledger escapes workspace");
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
const raw = await readFile(absolutePath, "utf8");
|
|
934
|
+
if (sha256Text(raw) !== binding.ledgerSha256) {
|
|
935
|
+
throw new Error("source ledger is stale");
|
|
936
|
+
}
|
|
937
|
+
const ledger = parseLedgerJson(raw);
|
|
938
|
+
const sourceFragments = new Map();
|
|
939
|
+
const boundFragments = new Map(ledger.fragments.map((fragment) => [fragment.id, fragment]));
|
|
940
|
+
for (const sourcePath of new Set(ledger.fragments.map((fragment) => fragment.path))) {
|
|
941
|
+
const sourceAbsolute = path.resolve(workspaceRoot, sourcePath);
|
|
942
|
+
if (!sourceAbsolute.startsWith(`${workspaceRoot}${path.sep}`)) {
|
|
943
|
+
throw new Error("source fragment escapes workspace");
|
|
944
|
+
}
|
|
945
|
+
const content = await readFile(sourceAbsolute, "utf8");
|
|
946
|
+
for (const fragment of splitDocumentIntoFragments({ path: sourcePath, content })) {
|
|
947
|
+
const frozen = boundFragments.get(fragment.id);
|
|
948
|
+
if (frozen && frozen.sha256 === sha256Text(fragment.text)) {
|
|
949
|
+
sourceFragments.set(fragment.id, fragment.text);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return new Map(ledger.canonicalRequirements.map((requirement) => [
|
|
954
|
+
requirement.id,
|
|
955
|
+
{
|
|
956
|
+
text: requirement.text,
|
|
957
|
+
sourceFragmentIds: [...(requirement.sourceFragmentIds ?? [])],
|
|
958
|
+
sourceFragments,
|
|
959
|
+
},
|
|
960
|
+
]));
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
throw new Error(`canonical requirement sources unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Authoritative UI state ids declared by the contract node
|
|
968
|
+
* (ui-state-declaration facts). Empty when the source declares no UI-state
|
|
969
|
+
* table, in which case the plan's state vocabulary is registry-only.
|
|
970
|
+
*/
|
|
971
|
+
async function resolveFrontendDeclaredUiStateIds(input) {
|
|
972
|
+
try {
|
|
973
|
+
const records = await readTypedEventStoreFromJsonl(path.join(input.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl"));
|
|
974
|
+
return records.flatMap((record) => {
|
|
975
|
+
const fact = record.fact;
|
|
976
|
+
return record.phase === "committed" &&
|
|
977
|
+
fact.kind === "ui-state-declaration" &&
|
|
978
|
+
typeof fact.id === "string"
|
|
979
|
+
? [fact.id]
|
|
980
|
+
: [];
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
catch {
|
|
984
|
+
// No contract facts (or no declared states): registry-only vocabulary.
|
|
985
|
+
return [];
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* A+B: `frontend-plan-pi` records its decision ledger through incremental
|
|
990
|
+
* `record_*` tools (origin=plan) and closes with exactly one
|
|
912
991
|
* `finalize_plan` terminal. A later attempt may explicitly adopt a quarantined
|
|
913
992
|
* fact via `adopt_staged_fact`. Flush writes `plan-typed-facts.jsonl` for the
|
|
914
993
|
* node validator / compile authority.
|
|
@@ -1042,6 +1121,12 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1042
1121
|
line: Type.Optional(Type.Number({})),
|
|
1043
1122
|
}, { additionalProperties: false })),
|
|
1044
1123
|
rationale: Type.String({}),
|
|
1124
|
+
covers: Type.Optional(Type.Array(Type.String({}), {
|
|
1125
|
+
description: "UI state and/or interaction names this single component choice covers (one choice may cover many ids).",
|
|
1126
|
+
})),
|
|
1127
|
+
evidencePath: Type.Optional(Type.String({
|
|
1128
|
+
description: "REQUIRED for decision=reuse-existing: repo-relative path whose existing file is the reuse evidence. Greenfield paths must use decision=new.",
|
|
1129
|
+
})),
|
|
1045
1130
|
}, { additionalProperties: false });
|
|
1046
1131
|
const stringList = (value) => Array.isArray(value)
|
|
1047
1132
|
? value.filter((item) => typeof item === "string")
|
|
@@ -1120,7 +1205,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1120
1205
|
const recordComponentChoiceTool = defineTool({
|
|
1121
1206
|
name: "record_component_choice",
|
|
1122
1207
|
label: "record_component_choice",
|
|
1123
|
-
description: "Record ONE component choice (origin=plan component-choice fact).
|
|
1208
|
+
description: "Record ONE component choice (origin=plan component-choice fact). One choice may cover MULTIPLE UI states/interactions via covers: [\"<state-or-interaction name>\", ...]; do not emit one row per interaction. For decision=new, pass sourceRequirementIds containing the frozen requirement ID(s) that mandate the component and sourceFragmentId selecting one frozen citation listed in the plan checklist; the runtime validates the relation and derives the exact PRD specReference. Do not read the PRD or invent a path/line. decision=reuse-existing is only for components that already exist in the repo and REQUIRES evidencePath: a repo-relative path to the existing file that proves the reuse — the runtime verifies the file exists (fresh evidence); a path with no existing file is greenfield and must use decision=new instead. Call up to 5 component choices per assistant message; never more than 5 per message. Optionally include stylingStrategy (set it once, on the first call). Example: {\"choice\": {\"purpose\": \"<interaction or UI state name>\", \"component\": \"<component name>\", \"decision\": \"new\", \"covers\": [\"<other interaction names this component also serves>\"]}, \"sourceRequirementIds\": [\"<AC-XXX mandating this component>\"], \"sourceFragmentId\": \"<REQ-SRC-...>\"} or {\"choice\": {\"purpose\": \"FocusQueuePanel\", \"component\": \"FocusQueuePanel\", \"decision\": \"reuse-existing\", \"evidencePath\": \"src/journal.ts\"}}",
|
|
1124
1209
|
promptSnippet: "Record 1-5 component choices (up to 5 per message).",
|
|
1125
1210
|
parameters: Type.Object({
|
|
1126
1211
|
choice: uiComponentChoiceSchema,
|
|
@@ -1171,6 +1256,46 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1171
1256
|
...(citation.line !== undefined ? { line: citation.line } : {}),
|
|
1172
1257
|
};
|
|
1173
1258
|
}
|
|
1259
|
+
if (choice.decision === "reuse-existing") {
|
|
1260
|
+
// reuse-existing must cite a file that actually exists NOW (fresh
|
|
1261
|
+
// existence evidence, same semantics as Scout pathEvidence.fresh).
|
|
1262
|
+
// A path with no file on disk is greenfield: decision=new is the
|
|
1263
|
+
// only honest choice (dogfood run dag-1788504923861-0f7b17a9
|
|
1264
|
+
// claimed 18 reuse-existing conventions in a not-yet-written
|
|
1265
|
+
// src/planner.ts and every gate let it through).
|
|
1266
|
+
const evidencePath = typeof choice.evidencePath === "string"
|
|
1267
|
+
? choice.evidencePath.trim()
|
|
1268
|
+
: "";
|
|
1269
|
+
if (!evidencePath) {
|
|
1270
|
+
return planToolReceipt({
|
|
1271
|
+
ok: false,
|
|
1272
|
+
kind: "component-choice",
|
|
1273
|
+
error: "decision=reuse-existing requires evidencePath naming the existing repo file that proves the reuse; if the file does not exist yet, use decision=new",
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (input.workspaceRoot) {
|
|
1277
|
+
const absolute = path.resolve(input.workspaceRoot, evidencePath);
|
|
1278
|
+
const workspaceRoot = path.resolve(input.workspaceRoot);
|
|
1279
|
+
const contained = absolute === workspaceRoot ||
|
|
1280
|
+
absolute.startsWith(`${workspaceRoot}${path.sep}`);
|
|
1281
|
+
let exists = false;
|
|
1282
|
+
if (contained) {
|
|
1283
|
+
try {
|
|
1284
|
+
exists = (await stat(absolute)).isFile();
|
|
1285
|
+
}
|
|
1286
|
+
catch {
|
|
1287
|
+
exists = false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (!exists) {
|
|
1291
|
+
return planToolReceipt({
|
|
1292
|
+
ok: false,
|
|
1293
|
+
kind: "component-choice",
|
|
1294
|
+
error: `decision=reuse-existing evidencePath "${evidencePath}" has no fresh existence evidence (file not found in the workspace); reuse requires an existing file — use decision=new for greenfield paths`,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1174
1299
|
const components = typeof choice.component === "string" ? [choice.component] : [];
|
|
1175
1300
|
const result = await adoptPlanFact("component-choice", `${attemptId}:record_component_choice:${randomUUID()}`, {
|
|
1176
1301
|
kind: "component-choice",
|
|
@@ -1194,10 +1319,86 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1194
1319
|
return planToolReceipt(echo);
|
|
1195
1320
|
},
|
|
1196
1321
|
});
|
|
1322
|
+
// Global UX vocabulary: one compact registry committed BEFORE any state
|
|
1323
|
+
// flow details. Coverage may slice by AC; UX must not — the registry is
|
|
1324
|
+
// the anti-duplication anchor that keeps every later slice on the same
|
|
1325
|
+
// named concepts instead of re-inventing them per AC chunk.
|
|
1326
|
+
const recordStateRegistryTool = defineTool({
|
|
1327
|
+
name: "record_state_registry",
|
|
1328
|
+
label: "record_state_registry",
|
|
1329
|
+
description: "Commit the GLOBAL UX vocabulary (origin=plan state-registry fact) BEFORE any record_state_flow call: uiStateNames (use the contract's declared authoritative state ids when provided) and interactionNames (stable behavior-domain kebab-case names, e.g. planner-task-edit / focus-queue-move — one name per behavior domain, never one per AC). Empty arrays explicitly mean the request has no UI state or interaction vocabulary. Re-record the full vocabulary to correct it; the latest commit wins, but it may not remove names still referenced by committed state-flow facts. Example: {\"uiStateNames\": [\"planner-empty\"], \"interactionNames\": [\"planner-task-create\", \"focus-queue-move\"]}",
|
|
1330
|
+
promptSnippet: "Record the global UI-state/interaction vocabulary once, before any state flow.",
|
|
1331
|
+
parameters: Type.Object({
|
|
1332
|
+
uiStateNames: stringArray,
|
|
1333
|
+
interactionNames: stringArray,
|
|
1334
|
+
}, { additionalProperties: false }),
|
|
1335
|
+
async execute(_toolCallId, params) {
|
|
1336
|
+
const uiStateNames = [...new Set(stringList(params?.uiStateNames).map((name) => name.trim()))];
|
|
1337
|
+
const interactionNames = [
|
|
1338
|
+
...new Set(stringList(params?.interactionNames).map((name) => name.trim())),
|
|
1339
|
+
];
|
|
1340
|
+
if (uiStateNames.some((name) => name.length === 0) || interactionNames.some((name) => name.length === 0)) {
|
|
1341
|
+
return planToolReceipt({
|
|
1342
|
+
ok: false,
|
|
1343
|
+
kind: "state-registry",
|
|
1344
|
+
error: "record_state_registry names must be non-empty strings",
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
const invalidInteractionNames = interactionNames.filter((name) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name));
|
|
1348
|
+
if (invalidInteractionNames.length > 0) {
|
|
1349
|
+
return planToolReceipt({
|
|
1350
|
+
ok: false,
|
|
1351
|
+
kind: "state-registry",
|
|
1352
|
+
error: `record_state_registry interactionNames must be stable kebab-case behavior-domain names: ${invalidInteractionNames.join(", ")}`,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
if (input.declaredUiStateIds &&
|
|
1356
|
+
input.declaredUiStateIds.length > 0) {
|
|
1357
|
+
const declared = new Set(input.declaredUiStateIds);
|
|
1358
|
+
const undeclared = uiStateNames.filter((name) => !declared.has(name));
|
|
1359
|
+
if (undeclared.length > 0) {
|
|
1360
|
+
return planToolReceipt({
|
|
1361
|
+
ok: false,
|
|
1362
|
+
kind: "state-registry",
|
|
1363
|
+
error: `record_state_registry uiStateNames are not declared by the contract's authoritative UI-state table: ${undeclared.join(", ")}; use the declared ids (${input.declaredUiStateIds.join(", ")})`,
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
const missing = input.declaredUiStateIds.filter((name) => !uiStateNames.includes(name));
|
|
1367
|
+
if (missing.length > 0) {
|
|
1368
|
+
return planToolReceipt({
|
|
1369
|
+
ok: false,
|
|
1370
|
+
kind: "state-registry",
|
|
1371
|
+
error: `record_state_registry must include every state from the contract's authoritative UI-state table; missing: ${missing.join(", ")}`,
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
// A correction is deterministic only when the new last-wins registry
|
|
1376
|
+
// still contains every live name already committed by state-flow facts.
|
|
1377
|
+
// This permits adding a missed concept, while preventing a registry edit
|
|
1378
|
+
// from retroactively orphaning earlier slices.
|
|
1379
|
+
const liveNames = collectCanonicalStateFlowNames(readCommittedEvents(store, attemptId));
|
|
1380
|
+
const orphanedUiStates = [...liveNames.uiStateNames].filter((name) => !uiStateNames.includes(name));
|
|
1381
|
+
const orphanedInteractions = [...liveNames.interactionNames].filter((name) => !interactionNames.includes(name));
|
|
1382
|
+
if (orphanedUiStates.length > 0 || orphanedInteractions.length > 0) {
|
|
1383
|
+
return planToolReceipt({
|
|
1384
|
+
ok: false,
|
|
1385
|
+
kind: "state-registry",
|
|
1386
|
+
error: `record_state_registry cannot remove names still referenced by committed state-flow facts (uiStates: ${orphanedUiStates.join(", ") || "none"}; interactions: ${orphanedInteractions.join(", ") || "none"}); first correct/remove those state-flow entries, then re-record the full registry`,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
const result = await adoptPlanFact("state-registry", `${attemptId}:record_state_registry:${randomUUID()}`, {
|
|
1390
|
+
kind: "state-registry",
|
|
1391
|
+
origin: "plan",
|
|
1392
|
+
uiStateNames,
|
|
1393
|
+
interactionNames,
|
|
1394
|
+
});
|
|
1395
|
+
return planToolReceipt(result);
|
|
1396
|
+
},
|
|
1397
|
+
});
|
|
1197
1398
|
const recordStateFlowTool = defineTool({
|
|
1198
1399
|
name: "record_state_flow",
|
|
1199
1400
|
label: "record_state_flow",
|
|
1200
|
-
description: "Record UI states and interactions as an origin=plan state-flow fact. To correct stale named entries from an earlier UX batch, include removeUiStateNames and/or removeInteractionNames. Example: {\"uiStates\": [{\"name\": \"<state>\", \"applicable\": true, \"expectedBehavior\": \"<behavior>\", \"implementationTargets\": [\"<file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}], \"interactions\": [{\"name\": \"<interaction>\", \"trigger\": \"<user event>\", \"expectedBehavior\": \"<behavior>\", \"implementationTargets\": [\"<file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}]}",
|
|
1401
|
+
description: "Record UI states and interactions as an origin=plan state-flow fact. REQUIRES a committed record_state_registry vocabulary first, and every name here must be in that registry; uiState names must also be contract-declared authoritative ids when the contract declares them. To correct stale named entries from an earlier UX batch, include removeUiStateNames and/or removeInteractionNames. Example: {\"uiStates\": [{\"name\": \"<state>\", \"applicable\": true, \"expectedBehavior\": \"<behavior>\", \"implementationTargets\": [\"<file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}], \"interactions\": [{\"name\": \"<interaction>\", \"trigger\": \"<user event>\", \"expectedBehavior\": \"<behavior>\", \"implementationTargets\": [\"<file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}]}",
|
|
1201
1402
|
promptSnippet: "Record the plan state-flow fact.",
|
|
1202
1403
|
parameters: Type.Object({
|
|
1203
1404
|
uiStates: Type.Array(uiStateSchema),
|
|
@@ -1301,9 +1502,63 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1301
1502
|
}
|
|
1302
1503
|
interactions.push({ ...interaction, name: resolvedName });
|
|
1303
1504
|
}
|
|
1505
|
+
// UX vocabulary gate: every recorded state/interaction name must be
|
|
1506
|
+
// declared in the committed global registry first. This keeps UX out
|
|
1507
|
+
// of AC-number slicing — the model commits one compact vocabulary
|
|
1508
|
+
// (record_state_registry), then fills behavior-domain details, and a
|
|
1509
|
+
// later slice cannot silently rename an earlier concept (dogfood
|
|
1510
|
+
// dag-1788504923861-0f7b17a9: 5 renames of "prioritize" + 7 of
|
|
1511
|
+
// "focus queue" across AC chunks).
|
|
1304
1512
|
const states = uiStates
|
|
1305
1513
|
.map((state) => (typeof state?.name === "string" ? state.name : ""))
|
|
1306
1514
|
.filter(Boolean);
|
|
1515
|
+
const committedRegistry = readCommittedEvents(store, attemptId)
|
|
1516
|
+
.map((event) => event.fact)
|
|
1517
|
+
.filter((fact) => isRecordObject(fact) && fact.kind === "state-registry")
|
|
1518
|
+
.at(-1);
|
|
1519
|
+
if (!committedRegistry) {
|
|
1520
|
+
return planToolReceipt({
|
|
1521
|
+
ok: false,
|
|
1522
|
+
kind: "state-flow",
|
|
1523
|
+
error: "record_state_flow requires a committed UX registry first: call record_state_registry with the full uiStateNames/interactionNames vocabulary, then record state flows against it",
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
const registryUiStateNames = new Set(stringList(committedRegistry.uiStateNames));
|
|
1527
|
+
const registryInteractionNames = new Set(stringList(committedRegistry.interactionNames));
|
|
1528
|
+
const unknownStates = states.filter((name) => !registryUiStateNames.has(name));
|
|
1529
|
+
if (unknownStates.length > 0) {
|
|
1530
|
+
return planToolReceipt({
|
|
1531
|
+
ok: false,
|
|
1532
|
+
kind: "state-flow",
|
|
1533
|
+
error: `record_state_flow uiState names not in the committed registry: ${unknownStates.join(", ")}; re-record record_state_registry with the complete vocabulary first (registered: ${[...registryUiStateNames].join(", ") || "(none)"})`,
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
const unknownInteractions = interactions
|
|
1537
|
+
.map((interaction) => typeof interaction?.name === "string" ? interaction.name : "")
|
|
1538
|
+
.filter((name) => name && !registryInteractionNames.has(name));
|
|
1539
|
+
if (unknownInteractions.length > 0) {
|
|
1540
|
+
return planToolReceipt({
|
|
1541
|
+
ok: false,
|
|
1542
|
+
kind: "state-flow",
|
|
1543
|
+
error: `record_state_flow interaction names not in the committed registry: ${unknownInteractions.join(", ")}; re-record record_state_registry with the complete vocabulary first (registered: ${[...registryInteractionNames].join(", ") || "(none)"})`,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
// Authoritative UI states: when the contract node declared the
|
|
1547
|
+
// source's UI-state table, planner states must bind those ids — no
|
|
1548
|
+
// invented variants (planner-empty/create-invalid/no-results/
|
|
1549
|
+
// editing/focus-full/storage-unavailable, not "planner-list").
|
|
1550
|
+
if (input.declaredUiStateIds &&
|
|
1551
|
+
input.declaredUiStateIds.length > 0) {
|
|
1552
|
+
const declared = new Set(input.declaredUiStateIds);
|
|
1553
|
+
const undeclaredStates = states.filter((name) => !declared.has(name));
|
|
1554
|
+
if (undeclaredStates.length > 0) {
|
|
1555
|
+
return planToolReceipt({
|
|
1556
|
+
ok: false,
|
|
1557
|
+
kind: "state-flow",
|
|
1558
|
+
error: `record_state_flow uiState names are not declared by the contract's authoritative UI-state table: ${undeclaredStates.join(", ")}; use the declared ids (${input.declaredUiStateIds.join(", ")}), or record a design deviation if the source table is genuinely incomplete`,
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1307
1562
|
const result = await adoptPlanFact("state-flow", `${attemptId}:record_state_flow:${randomUUID()}`, {
|
|
1308
1563
|
kind: "state-flow",
|
|
1309
1564
|
origin: "plan",
|
|
@@ -1672,6 +1927,29 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1672
1927
|
? { realIntegrationGap: params.realIntegrationGap }
|
|
1673
1928
|
: {}),
|
|
1674
1929
|
};
|
|
1930
|
+
const latestRegistry = [...committed]
|
|
1931
|
+
.reverse()
|
|
1932
|
+
.map((record) => record.fact)
|
|
1933
|
+
.find((fact) => isRecordObject(fact) && fact.kind === "state-registry");
|
|
1934
|
+
if (latestRegistry) {
|
|
1935
|
+
const registryUiStates = new Set(stringList(latestRegistry.uiStateNames));
|
|
1936
|
+
const registryInteractions = new Set(stringList(latestRegistry.interactionNames));
|
|
1937
|
+
const live = collectCanonicalStateFlowNames(committed);
|
|
1938
|
+
const missingUiStates = [...registryUiStates].filter((name) => !live.uiStateNames.has(name));
|
|
1939
|
+
const missingInteractions = [...registryInteractions].filter((name) => !live.interactionNames.has(name));
|
|
1940
|
+
const undeclaredUiStates = [...live.uiStateNames].filter((name) => !registryUiStates.has(name));
|
|
1941
|
+
const undeclaredInteractions = [...live.interactionNames].filter((name) => !registryInteractions.has(name));
|
|
1942
|
+
if (missingUiStates.length > 0 ||
|
|
1943
|
+
missingInteractions.length > 0 ||
|
|
1944
|
+
undeclaredUiStates.length > 0 ||
|
|
1945
|
+
undeclaredInteractions.length > 0) {
|
|
1946
|
+
return planToolReceipt({
|
|
1947
|
+
ok: false,
|
|
1948
|
+
kind: "finalize_plan",
|
|
1949
|
+
error: `finalize_plan UX registry mismatch: every registered name must have one live state-flow entry and every live entry must be registered (missing uiStates: ${missingUiStates.join(", ") || "none"}; missing interactions: ${missingInteractions.join(", ") || "none"}; undeclared uiStates: ${undeclaredUiStates.join(", ") || "none"}; undeclared interactions: ${undeclaredInteractions.join(", ") || "none"})`,
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1675
1953
|
// Front-load the node's compile + policy gates into the finalize
|
|
1676
1954
|
// receipt (same pipeline the design-policy shell and the node
|
|
1677
1955
|
// self-check run: merge the patch onto the runtime skeleton,
|
|
@@ -1698,9 +1976,10 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1698
1976
|
catch (error) {
|
|
1699
1977
|
if (error instanceof PlanPolicyPrecheckFailure) {
|
|
1700
1978
|
// Template the fix: every uncovered interaction / state
|
|
1701
|
-
// maps to a
|
|
1702
|
-
//
|
|
1703
|
-
//
|
|
1979
|
+
// maps to a record_component_choice skeleton. Reuse is
|
|
1980
|
+
// never implied: the operator/model must fill an existing
|
|
1981
|
+
// evidencePath or switch the choice to decision=new with
|
|
1982
|
+
// the required frozen source citation.
|
|
1704
1983
|
const suggestions = error.findings
|
|
1705
1984
|
.filter((finding) => finding.code === "ui-design-coverage-missing" &&
|
|
1706
1985
|
finding.path)
|
|
@@ -1711,6 +1990,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1711
1990
|
purpose: finding.path,
|
|
1712
1991
|
component: "<name the existing or new component>",
|
|
1713
1992
|
decision: "reuse-existing",
|
|
1993
|
+
evidencePath: "<existing repo file that proves this reuse>",
|
|
1714
1994
|
},
|
|
1715
1995
|
},
|
|
1716
1996
|
}));
|
|
@@ -1796,6 +2076,7 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
1796
2076
|
customTools: [
|
|
1797
2077
|
recordRouteSelectionTool,
|
|
1798
2078
|
recordComponentChoiceTool,
|
|
2079
|
+
recordStateRegistryTool,
|
|
1799
2080
|
recordStateFlowTool,
|
|
1800
2081
|
recordDataFlowTool,
|
|
1801
2082
|
recordMockApiTool,
|
|
@@ -1845,6 +2126,10 @@ function contractFactSourceSpan(fact) {
|
|
|
1845
2126
|
if (Array.isArray(fact.sourceRefs) && fact.sourceRefs.length > 0) {
|
|
1846
2127
|
return fact.sourceRefs;
|
|
1847
2128
|
}
|
|
2129
|
+
if (Array.isArray(fact.sourceFragmentIds) &&
|
|
2130
|
+
fact.sourceFragmentIds.some((value) => typeof value === "string" && value.trim().length > 0)) {
|
|
2131
|
+
return fact.sourceFragmentIds;
|
|
2132
|
+
}
|
|
1848
2133
|
if (typeof fact.source === "string" && fact.source.trim().length > 0) {
|
|
1849
2134
|
return fact.source;
|
|
1850
2135
|
}
|
|
@@ -1894,6 +2179,7 @@ export function buildFrontendTaskContractVNext(records) {
|
|
|
1894
2179
|
requirements,
|
|
1895
2180
|
constraints: byKind("constraint"),
|
|
1896
2181
|
evidenceExpectations: byKind("evidence-expectation"),
|
|
2182
|
+
deliverableDeclarations: byKind("required-deliverables").at(-1)?.items ?? [],
|
|
1897
2183
|
handoffIntents: byKind("handoff-intent"),
|
|
1898
2184
|
openQuestions: byKind("open-question"),
|
|
1899
2185
|
splitProposals: byKind("split-proposal"),
|
|
@@ -1901,7 +2187,7 @@ export function buildFrontendTaskContractVNext(records) {
|
|
|
1901
2187
|
};
|
|
1902
2188
|
}
|
|
1903
2189
|
/**
|
|
1904
|
-
* A+B: `frontend-contract-pi` incremental contract tools.
|
|
2190
|
+
* A+B: `frontend-contract-pi` incremental contract tools. The `record_*` tools
|
|
1905
2191
|
* commit origin=contract facts and `finalize_contract` commits the terminal
|
|
1906
2192
|
* disposition (ready | ready-with-assumptions | blocked + blockingOwner).
|
|
1907
2193
|
*/
|
|
@@ -1953,9 +2239,7 @@ export async function createFrontendContractTools(input) {
|
|
|
1953
2239
|
}
|
|
1954
2240
|
}
|
|
1955
2241
|
const recordKinds = {
|
|
1956
|
-
record_requirement: "requirement",
|
|
1957
2242
|
record_constraint: "constraint",
|
|
1958
|
-
record_evidence_expectation: "evidence-expectation",
|
|
1959
2243
|
record_handoff_intent: "handoff-intent",
|
|
1960
2244
|
record_open_question: "open-question",
|
|
1961
2245
|
record_split_proposal: "split-proposal",
|
|
@@ -1975,6 +2259,141 @@ export async function createFrontendContractTools(input) {
|
|
|
1975
2259
|
return receipt(result);
|
|
1976
2260
|
},
|
|
1977
2261
|
}));
|
|
2262
|
+
// record_requirement is runtime-owned by design: the requirement TEXT and
|
|
2263
|
+
// sourceFragmentIds come from the frozen source-fidelity ledger, never from
|
|
2264
|
+
// model-authored prose. The model only names the canonical id it confirms.
|
|
2265
|
+
// This closes the free-shape hole (additionalProperties:true accepted
|
|
2266
|
+
// `statement` rewrites and JSON-stringified `sourceFragmentIds` arrays,
|
|
2267
|
+
// which then reached the planner as empty text + dead fragment bindings).
|
|
2268
|
+
const recordRequirementTool = defineTool({
|
|
2269
|
+
name: "record_requirement",
|
|
2270
|
+
label: "record_requirement",
|
|
2271
|
+
description: 'Confirm one canonical ledger requirement (origin=contract requirement fact). Pass ONLY the canonical id 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.',
|
|
2272
|
+
promptSnippet: "Confirm 1-5 canonical requirements by id (up to 5 per message).",
|
|
2273
|
+
parameters: Type.Object({ id: Type.String({ description: "Canonical ledger requirement id (e.g. AC-001)" }) }, { additionalProperties: false }),
|
|
2274
|
+
async execute(_toolCallId, params) {
|
|
2275
|
+
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
2276
|
+
if (!id) {
|
|
2277
|
+
return receipt({
|
|
2278
|
+
ok: false,
|
|
2279
|
+
kind: "requirement",
|
|
2280
|
+
error: "record_requirement requires the canonical requirement id",
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
const canonical = input.canonicalRequirements?.get(id);
|
|
2284
|
+
if (!canonical) {
|
|
2285
|
+
const known = [...(input.canonicalRequirements?.keys() ?? [])];
|
|
2286
|
+
return receipt({
|
|
2287
|
+
ok: false,
|
|
2288
|
+
kind: "requirement",
|
|
2289
|
+
error: `record_requirement id "${id}" is not a canonical ledger requirement; canonical ids are: ${known.join(", ") || "(none)"}`,
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
const result = await adoptContractFact("requirement", {
|
|
2293
|
+
kind: "requirement",
|
|
2294
|
+
origin: "contract",
|
|
2295
|
+
disposition: "explicit",
|
|
2296
|
+
id,
|
|
2297
|
+
text: canonical.text,
|
|
2298
|
+
sourceFragmentIds: canonical.sourceFragmentIds,
|
|
2299
|
+
});
|
|
2300
|
+
return receipt(result);
|
|
2301
|
+
},
|
|
2302
|
+
});
|
|
2303
|
+
// Authoritative UI state declarations: the contract node extracts the
|
|
2304
|
+
// PRD/reference UI-state table into structured facts so the planner binds
|
|
2305
|
+
// uiStates to declared ids instead of inventing names (dogfood
|
|
2306
|
+
// dag-1788504923861-0f7b17a9: 10 invented list-visibility variants, 0 of
|
|
2307
|
+
// the 7 PRD states).
|
|
2308
|
+
const recordUiStateTool = defineTool({
|
|
2309
|
+
name: "record_ui_state",
|
|
2310
|
+
label: "record_ui_state",
|
|
2311
|
+
description: 'Declare ONE authoritative UI state extracted from the task source\'s UI-state table (origin=contract ui-state-declaration fact). Call once per declared state, exactly as the source names it: {"id": "<state id from the source table>", "trigger": "<when this state applies>", "observableOutcome": "<what the user can observe>"}. The planner must bind these ids later; do not rename or invent states.',
|
|
2312
|
+
promptSnippet: "Declare 1-5 authoritative UI states (up to 5 per message).",
|
|
2313
|
+
parameters: Type.Object({
|
|
2314
|
+
id: Type.String({ description: "UI state id exactly as the source table declares it" }),
|
|
2315
|
+
trigger: Type.String({ description: "When this state applies" }),
|
|
2316
|
+
observableOutcome: Type.String({ description: "Observable result for the user" }),
|
|
2317
|
+
}, { additionalProperties: false }),
|
|
2318
|
+
async execute(_toolCallId, params) {
|
|
2319
|
+
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
2320
|
+
const trigger = typeof params?.trigger === "string" ? params.trigger.trim() : "";
|
|
2321
|
+
const observableOutcome = typeof params?.observableOutcome === "string"
|
|
2322
|
+
? params.observableOutcome.trim()
|
|
2323
|
+
: "";
|
|
2324
|
+
if (!id || !trigger || !observableOutcome) {
|
|
2325
|
+
return receipt({
|
|
2326
|
+
ok: false,
|
|
2327
|
+
kind: "ui-state-declaration",
|
|
2328
|
+
error: "record_ui_state requires non-empty id, trigger, and observableOutcome",
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
const result = await adoptContractFact("ui-state-declaration", {
|
|
2332
|
+
kind: "ui-state-declaration",
|
|
2333
|
+
origin: "contract",
|
|
2334
|
+
id,
|
|
2335
|
+
trigger,
|
|
2336
|
+
observableOutcome,
|
|
2337
|
+
});
|
|
2338
|
+
return receipt(result);
|
|
2339
|
+
},
|
|
2340
|
+
});
|
|
2341
|
+
const evidenceStatus = Type.Enum({
|
|
2342
|
+
required: "required", optional: "optional", "not-applicable": "not-applicable",
|
|
2343
|
+
});
|
|
2344
|
+
const recordEvidenceExpectationTool = defineTool({
|
|
2345
|
+
name: "record_evidence_expectation",
|
|
2346
|
+
label: "record_evidence_expectation",
|
|
2347
|
+
description: 'Record evidence requirements as {requirementId:"<canonical id>",evidence:{static:"required|optional|not-applicable",behavior:"required|optional|not-applicable",mock:"required|optional|not-applicable","real-integration":"required|optional|not-applicable"}}. Declare at least one lane; later calls replace only the lanes they name.',
|
|
2348
|
+
parameters: Type.Object({
|
|
2349
|
+
requirementId: Type.String(),
|
|
2350
|
+
evidence: Type.Object({
|
|
2351
|
+
static: Type.Optional(evidenceStatus),
|
|
2352
|
+
behavior: Type.Optional(evidenceStatus),
|
|
2353
|
+
mock: Type.Optional(evidenceStatus),
|
|
2354
|
+
"real-integration": Type.Optional(evidenceStatus),
|
|
2355
|
+
}, { additionalProperties: false }),
|
|
2356
|
+
}, { additionalProperties: false }),
|
|
2357
|
+
async execute(_toolCallId, params) {
|
|
2358
|
+
try {
|
|
2359
|
+
const fact = frontendEvidenceExpectationSchema.parse(params);
|
|
2360
|
+
if (!input.canonicalRequirements?.has(fact.requirementId)) {
|
|
2361
|
+
throw new Error(`unknown canonical requirement ${fact.requirementId}`);
|
|
2362
|
+
}
|
|
2363
|
+
return receipt(await adoptContractFact("evidence-expectation", {
|
|
2364
|
+
kind: "evidence-expectation", origin: "contract", ...fact,
|
|
2365
|
+
}));
|
|
2366
|
+
}
|
|
2367
|
+
catch (error) {
|
|
2368
|
+
return receipt({
|
|
2369
|
+
ok: false, kind: "evidence-expectation",
|
|
2370
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
},
|
|
2374
|
+
});
|
|
2375
|
+
const recordRequiredDeliverablesTool = defineTool({
|
|
2376
|
+
name: "record_required_deliverables",
|
|
2377
|
+
label: "record_required_deliverables",
|
|
2378
|
+
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. A correction replaces the whole inventory. Required before finalize_contract ready.',
|
|
2379
|
+
parameters: Type.Object({ items: Type.Array(Type.Object({
|
|
2380
|
+
path: Type.String(), requirementId: Type.String(), sourceFragmentId: Type.String(),
|
|
2381
|
+
}, { additionalProperties: false })) }, { additionalProperties: false }),
|
|
2382
|
+
async execute(_toolCallId, params) {
|
|
2383
|
+
try {
|
|
2384
|
+
const declaration = validateFrontendRequiredDeliverables(params, input.canonicalRequirements ?? new Map());
|
|
2385
|
+
return receipt(await adoptContractFact("required-deliverables", {
|
|
2386
|
+
kind: "required-deliverables", origin: "contract", ...declaration,
|
|
2387
|
+
}));
|
|
2388
|
+
}
|
|
2389
|
+
catch (error) {
|
|
2390
|
+
return receipt({
|
|
2391
|
+
ok: false, kind: "required-deliverables",
|
|
2392
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
},
|
|
2396
|
+
});
|
|
1978
2397
|
// OpenSpec selection committed as individual typed facts (one path per
|
|
1979
2398
|
// call) so a large candidate set never exceeds a single model output
|
|
1980
2399
|
// budget: each tool call carries exactly one {path, disposition,
|
|
@@ -2045,6 +2464,13 @@ export async function createFrontendContractTools(input) {
|
|
|
2045
2464
|
error: "blocked disposition requires blockingOwner (blocked-human | blocked-external)",
|
|
2046
2465
|
});
|
|
2047
2466
|
}
|
|
2467
|
+
if (disposition !== "blocked" && input.canonicalRequirements?.size &&
|
|
2468
|
+
!readCommittedEvents(store, attemptId).some((record) => record.fact.kind === "required-deliverables")) {
|
|
2469
|
+
return receipt({
|
|
2470
|
+
ok: false, kind: "finalize_contract",
|
|
2471
|
+
error: "call record_required_deliverables with the complete source-bound inventory (or items:[] when none) before finalizing",
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2048
2474
|
const blockedOwner = mapContractBlockedOwner({
|
|
2049
2475
|
disposition: disposition ?? "",
|
|
2050
2476
|
blockingOwner,
|
|
@@ -2068,6 +2494,10 @@ export async function createFrontendContractTools(input) {
|
|
|
2068
2494
|
return {
|
|
2069
2495
|
customTools: [
|
|
2070
2496
|
...recordTools,
|
|
2497
|
+
recordRequirementTool,
|
|
2498
|
+
recordEvidenceExpectationTool,
|
|
2499
|
+
recordUiStateTool,
|
|
2500
|
+
recordRequiredDeliverablesTool,
|
|
2071
2501
|
recordOpenspecSelectionTool,
|
|
2072
2502
|
finalizeContractTool,
|
|
2073
2503
|
],
|
|
@@ -2682,6 +3112,15 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
2682
3112
|
"If a requirement genuinely cannot have a verification target, record a non-empty record_plan_evidence_gap. Do not call finalize_plan; it is not available in this phase.",
|
|
2683
3113
|
].join(" "),
|
|
2684
3114
|
},
|
|
3115
|
+
{
|
|
3116
|
+
id: "ux-registry",
|
|
3117
|
+
toolNames: new Set(["record_state_registry", "adopt_staged_fact"]),
|
|
3118
|
+
instruction: [
|
|
3119
|
+
"PLAN PHASE — global UX vocabulary.",
|
|
3120
|
+
"Review ALL frozen requirements together and call record_state_registry exactly once with the complete UI-state and interaction vocabulary. 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.",
|
|
3121
|
+
"Do not call finalize_plan; it is not available in this phase.",
|
|
3122
|
+
].join(" "),
|
|
3123
|
+
},
|
|
2685
3124
|
{
|
|
2686
3125
|
id: "ux-local",
|
|
2687
3126
|
toolNames: new Set([
|
|
@@ -2690,8 +3129,8 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
2690
3129
|
"adopt_staged_fact",
|
|
2691
3130
|
]),
|
|
2692
3131
|
instruction: [
|
|
2693
|
-
"PLAN PHASE —
|
|
2694
|
-
"Requirements and verification targets are already committed in the ledger (do NOT re-record them; duplicates are rejected).
|
|
3132
|
+
"PLAN PHASE — global UX decisions.",
|
|
3133
|
+
"Requirements and verification targets are already committed in the ledger (do NOT re-record them; duplicates are rejected). Review the complete requirement set and the committed global UX registry together, then record each component choice, UI state and interaction exactly once. Multiple requirements that describe one behavior share one registry name and one 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.",
|
|
2695
3134
|
"Do not call finalize_plan; it is not available in this phase.",
|
|
2696
3135
|
].join(" "),
|
|
2697
3136
|
},
|
|
@@ -2749,40 +3188,6 @@ function planFactStringList(value) {
|
|
|
2749
3188
|
function planFactScopeIntersects(fact, requirementIds) {
|
|
2750
3189
|
return planFactStringList(fact.scopeRequirementIds).some((id) => requirementIds.has(id));
|
|
2751
3190
|
}
|
|
2752
|
-
/** Apply state-flow replacement/removal semantics exactly as the plan compiler does. */
|
|
2753
|
-
function collectCanonicalStateFlowNames(committedFacts) {
|
|
2754
|
-
const uiStateNames = new Set();
|
|
2755
|
-
const interactionNames = new Set();
|
|
2756
|
-
for (const value of committedFacts) {
|
|
2757
|
-
const fact = committedFactFromPlanRecord(value);
|
|
2758
|
-
if (!fact || fact.origin !== "plan" || fact.kind !== "state-flow")
|
|
2759
|
-
continue;
|
|
2760
|
-
for (const name of planFactStringList(fact.removeUiStateNames)) {
|
|
2761
|
-
uiStateNames.delete(name);
|
|
2762
|
-
}
|
|
2763
|
-
for (const name of planFactStringList(fact.removeInteractionNames)) {
|
|
2764
|
-
interactionNames.delete(name);
|
|
2765
|
-
}
|
|
2766
|
-
for (const state of Array.isArray(fact.uiStates) ? fact.uiStates : []) {
|
|
2767
|
-
if (!state || typeof state !== "object" || Array.isArray(state))
|
|
2768
|
-
continue;
|
|
2769
|
-
const name = state.name;
|
|
2770
|
-
if (typeof name === "string" && name.trim())
|
|
2771
|
-
uiStateNames.add(name);
|
|
2772
|
-
}
|
|
2773
|
-
for (const interaction of Array.isArray(fact.interactions)
|
|
2774
|
-
? fact.interactions
|
|
2775
|
-
: []) {
|
|
2776
|
-
if (!interaction || typeof interaction !== "object" || Array.isArray(interaction)) {
|
|
2777
|
-
continue;
|
|
2778
|
-
}
|
|
2779
|
-
const name = interaction.name;
|
|
2780
|
-
if (typeof name === "string" && name.trim())
|
|
2781
|
-
interactionNames.add(name);
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
return { uiStateNames, interactionNames };
|
|
2785
|
-
}
|
|
2786
3191
|
/** Compute the authoritative coverage queue from the committed plan ledger. */
|
|
2787
3192
|
export function collectFrontendPlanMissingFacts(input) {
|
|
2788
3193
|
const requirements = new Map();
|
|
@@ -2864,6 +3269,17 @@ export function collectFrontendPlanPhaseMissingFacts(input) {
|
|
|
2864
3269
|
const facts = input.committedFacts
|
|
2865
3270
|
.map(committedFactFromPlanRecord)
|
|
2866
3271
|
.filter((fact) => Boolean(fact && fact.origin === "plan"));
|
|
3272
|
+
if (input.phase === "ux-registry") {
|
|
3273
|
+
return facts.some((fact) => fact.kind === "state-registry")
|
|
3274
|
+
? []
|
|
3275
|
+
: [
|
|
3276
|
+
{
|
|
3277
|
+
kind: "state-registry",
|
|
3278
|
+
requirementIds: [...input.requirementIds],
|
|
3279
|
+
reason: "global UX vocabulary phase has no committed state-registry fact",
|
|
3280
|
+
},
|
|
3281
|
+
];
|
|
3282
|
+
}
|
|
2867
3283
|
if (input.phase === "ux-local") {
|
|
2868
3284
|
const needsUx = input.requirementIds.some((id) => input.behaviorRequiredRequirementIds?.includes(id));
|
|
2869
3285
|
if (!needsUx)
|
|
@@ -2958,10 +3374,9 @@ export function batchFrontendPlanRequirements(input) {
|
|
|
2958
3374
|
return batches;
|
|
2959
3375
|
}
|
|
2960
3376
|
const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
|
|
2961
|
-
|
|
2962
|
-
//
|
|
2963
|
-
//
|
|
2964
|
-
// legitimate large plan.
|
|
3377
|
+
// A large requirement set creates coverage shards; UX remains one global
|
|
3378
|
+
// decision session so behavior names and component/state facts are not
|
|
3379
|
+
// reinvented per AC batch. Keep a safety bound for adaptive coverage retries.
|
|
2965
3380
|
const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
|
|
2966
3381
|
const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
|
|
2967
3382
|
const FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS = 24;
|
|
@@ -3147,8 +3562,33 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3147
3562
|
}];
|
|
3148
3563
|
})
|
|
3149
3564
|
: [];
|
|
3565
|
+
const compactDeclaredUiStates = Array.isArray(payload.declaredUiStates)
|
|
3566
|
+
? payload.declaredUiStates.flatMap((value) => {
|
|
3567
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3568
|
+
return [];
|
|
3569
|
+
const state = value;
|
|
3570
|
+
const id = compactPromptString(state.id, 80);
|
|
3571
|
+
if (!id)
|
|
3572
|
+
return [];
|
|
3573
|
+
return [{
|
|
3574
|
+
id,
|
|
3575
|
+
...(compactPromptString(state.trigger, 180)
|
|
3576
|
+
? { trigger: compactPromptString(state.trigger, 180) }
|
|
3577
|
+
: {}),
|
|
3578
|
+
...(compactPromptString(state.observableOutcome, 240)
|
|
3579
|
+
? { observableOutcome: compactPromptString(state.observableOutcome, 240) }
|
|
3580
|
+
: {}),
|
|
3581
|
+
}];
|
|
3582
|
+
})
|
|
3583
|
+
: [];
|
|
3584
|
+
const committedUx = payload.committedUx &&
|
|
3585
|
+
typeof payload.committedUx === "object" &&
|
|
3586
|
+
!Array.isArray(payload.committedUx)
|
|
3587
|
+
? payload.committedUx
|
|
3588
|
+
: undefined;
|
|
3150
3589
|
const compactPayload = {
|
|
3151
3590
|
requirements: compactRequirements,
|
|
3591
|
+
requiredDeliverables: Array.isArray(payload.requiredDeliverables) ? payload.requiredDeliverables : [],
|
|
3152
3592
|
...(compactTargetSurface.length > 0
|
|
3153
3593
|
? { targetSurface: compactTargetSurface }
|
|
3154
3594
|
: {}),
|
|
@@ -3158,6 +3598,17 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
|
|
|
3158
3598
|
...(compactDesignEvidence.length > 0
|
|
3159
3599
|
? { designEvidence: compactDesignEvidence }
|
|
3160
3600
|
: {}),
|
|
3601
|
+
...(compactDeclaredUiStates.length > 0
|
|
3602
|
+
? { declaredUiStates: compactDeclaredUiStates }
|
|
3603
|
+
: {}),
|
|
3604
|
+
...(committedUx
|
|
3605
|
+
? {
|
|
3606
|
+
committedUx: {
|
|
3607
|
+
uiStateNames: compactPromptStringArray(committedUx.uiStateNames, 40, 100),
|
|
3608
|
+
interactionNames: compactPromptStringArray(committedUx.interactionNames, 60, 120),
|
|
3609
|
+
},
|
|
3610
|
+
}
|
|
3611
|
+
: {}),
|
|
3161
3612
|
};
|
|
3162
3613
|
const compactBlock = [
|
|
3163
3614
|
"<frontend_plan_input>",
|
|
@@ -3254,6 +3705,8 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
3254
3705
|
purpose: compactPromptString(item.purpose, 120),
|
|
3255
3706
|
component: compactPromptString(item.component, 120),
|
|
3256
3707
|
decision: compactPromptString(item.decision, 40),
|
|
3708
|
+
covers: compactPromptStringArray(item.covers, 40, 120),
|
|
3709
|
+
evidencePath: compactPromptString(item.evidencePath, 180),
|
|
3257
3710
|
}];
|
|
3258
3711
|
}).slice(0, 24)
|
|
3259
3712
|
: [];
|
|
@@ -3261,6 +3714,14 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
3261
3714
|
compactFacts.push({ kind: fact.kind, uiComponentChoices: choices });
|
|
3262
3715
|
continue;
|
|
3263
3716
|
}
|
|
3717
|
+
if (fact.kind === "state-registry") {
|
|
3718
|
+
compactFacts.push({
|
|
3719
|
+
kind: fact.kind,
|
|
3720
|
+
uiStateNames: compactPromptStringArray(fact.uiStateNames, 40, 100),
|
|
3721
|
+
interactionNames: compactPromptStringArray(fact.interactionNames, 60, 120),
|
|
3722
|
+
});
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3264
3725
|
if (fact.kind === "state-flow") {
|
|
3265
3726
|
compactFacts.push({
|
|
3266
3727
|
kind: fact.kind,
|
|
@@ -3323,6 +3784,7 @@ function compactFrontendPlanLedgerContext(input) {
|
|
|
3323
3784
|
export async function runFrontendPlanSegmentedSessions(input) {
|
|
3324
3785
|
const queue = [];
|
|
3325
3786
|
const coverageSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "coverage");
|
|
3787
|
+
const uxRegistrySegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "ux-registry");
|
|
3326
3788
|
const uxSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "ux-local");
|
|
3327
3789
|
const globalMockDataSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "global-mock-data");
|
|
3328
3790
|
const finalizeSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "finalize");
|
|
@@ -3342,7 +3804,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3342
3804
|
? ["plan-requirement", "plan-verification-target", "state-flow", "data-flow", "mock-api"]
|
|
3343
3805
|
: segment.id === "global-dependency-deviation"
|
|
3344
3806
|
? ["dependency", "design-deviation"]
|
|
3345
|
-
: ["plan-requirement", "plan-verification-target", "component-choice", "state-flow", "data-flow", "mock-api", "design-deviation", "dependency", "target-surface"];
|
|
3807
|
+
: ["plan-requirement", "plan-verification-target", "state-registry", "component-choice", "state-flow", "data-flow", "mock-api", "design-deviation", "dependency", "target-surface"];
|
|
3346
3808
|
const ledger = input.committedFacts
|
|
3347
3809
|
? compactFrontendPlanLedgerContext({
|
|
3348
3810
|
committedFacts: input.committedFacts(),
|
|
@@ -3378,14 +3840,20 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3378
3840
|
? compactFrontendPlanLedgerContext({
|
|
3379
3841
|
committedFacts: input.committedFacts(),
|
|
3380
3842
|
requirementIds: slice,
|
|
3381
|
-
kinds: [
|
|
3843
|
+
kinds: [
|
|
3844
|
+
"plan-requirement",
|
|
3845
|
+
"plan-verification-target",
|
|
3846
|
+
"state-registry",
|
|
3847
|
+
"component-choice",
|
|
3848
|
+
"state-flow",
|
|
3849
|
+
],
|
|
3382
3850
|
scopedKinds: ["component-choice", "state-flow"],
|
|
3383
3851
|
})
|
|
3384
3852
|
: "";
|
|
3385
3853
|
return [
|
|
3386
3854
|
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, slice),
|
|
3387
3855
|
uxSegment.instruction,
|
|
3388
|
-
`UX
|
|
3856
|
+
`GLOBAL UX SCOPE: process the complete requirement set together: ${slice.join(", ")}. Do not split or rename one behavior by requirement id.`,
|
|
3389
3857
|
ledger,
|
|
3390
3858
|
...(missing.length > 0
|
|
3391
3859
|
? [
|
|
@@ -3395,6 +3863,37 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3395
3863
|
: []),
|
|
3396
3864
|
].filter(Boolean).join("\n\n");
|
|
3397
3865
|
};
|
|
3866
|
+
const buildUxRegistryPrompt = (missing = []) => {
|
|
3867
|
+
const ledger = input.committedFacts
|
|
3868
|
+
? compactFrontendPlanLedgerContext({
|
|
3869
|
+
committedFacts: input.committedFacts(),
|
|
3870
|
+
requirementIds: allRequirementIds,
|
|
3871
|
+
kinds: [
|
|
3872
|
+
"plan-requirement",
|
|
3873
|
+
"plan-verification-target",
|
|
3874
|
+
"state-registry",
|
|
3875
|
+
],
|
|
3876
|
+
})
|
|
3877
|
+
: "";
|
|
3878
|
+
const compact = allRequirementIds.length > 0
|
|
3879
|
+
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, allRequirementIds, {
|
|
3880
|
+
includeVerificationTargets: false,
|
|
3881
|
+
includeDesignEvidence: false,
|
|
3882
|
+
includeChecklist: false,
|
|
3883
|
+
})
|
|
3884
|
+
: input.basePrompt;
|
|
3885
|
+
return [
|
|
3886
|
+
compact,
|
|
3887
|
+
uxRegistrySegment.instruction,
|
|
3888
|
+
ledger,
|
|
3889
|
+
...(missing.length > 0
|
|
3890
|
+
? [
|
|
3891
|
+
"MISSING-FACT QUEUE: commit the global UX registry, then re-check this phase:",
|
|
3892
|
+
...missing.map((item) => `- ${item.reason}`),
|
|
3893
|
+
]
|
|
3894
|
+
: []),
|
|
3895
|
+
].filter(Boolean).join("\n\n");
|
|
3896
|
+
};
|
|
3398
3897
|
const buildCompactLocalPrompt = (missing = []) => {
|
|
3399
3898
|
const ledger = input.committedFacts
|
|
3400
3899
|
? compactFrontendPlanLedgerContext({
|
|
@@ -3403,6 +3902,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3403
3902
|
kinds: [
|
|
3404
3903
|
"plan-requirement",
|
|
3405
3904
|
"plan-verification-target",
|
|
3905
|
+
"state-registry",
|
|
3406
3906
|
"component-choice",
|
|
3407
3907
|
"state-flow",
|
|
3408
3908
|
],
|
|
@@ -3411,7 +3911,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3411
3911
|
return [
|
|
3412
3912
|
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, allRequirementIds),
|
|
3413
3913
|
"PLAN PHASE — compact local planning for a small frontend request.",
|
|
3414
|
-
"
|
|
3914
|
+
"Review all listed requirements together and record_state_registry first with one global UX vocabulary. Then record every plan-requirement and verification-target fact, followed by the component-choice and state-flow facts needed by the observable UX. Do not read the repository or task source; use only the committed input above. Do not call finalize_plan in this session.",
|
|
3415
3915
|
"TOOL-FIRST: your first assistant actions must be record_* tool calls, at most 2-3 facts per message. Do not draft the whole analysis before recording; if a fact is uncertain, record it with an evidence gap instead of reasoning longer.",
|
|
3416
3916
|
ledger,
|
|
3417
3917
|
...(missing.length > 0
|
|
@@ -3462,6 +3962,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3462
3962
|
"record_plan_requirement",
|
|
3463
3963
|
"record_plan_verification_target",
|
|
3464
3964
|
"record_plan_evidence_gap",
|
|
3965
|
+
"record_state_registry",
|
|
3465
3966
|
"record_component_choice",
|
|
3466
3967
|
"record_state_flow",
|
|
3467
3968
|
"adopt_staged_fact",
|
|
@@ -3504,28 +4005,29 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3504
4005
|
// Compact mode already queued both sessions above.
|
|
3505
4006
|
}
|
|
3506
4007
|
else {
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
4008
|
+
if (requirementIdsProvided) {
|
|
4009
|
+
queue.push({
|
|
4010
|
+
id: uxRegistrySegment.id,
|
|
4011
|
+
toolNames: uxRegistrySegment.toolNames,
|
|
4012
|
+
prompt: buildUxRegistryPrompt(),
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
const uxSlice = requirementIdsProvided ? [...allRequirementIds] : [];
|
|
4016
|
+
queue.push({
|
|
4017
|
+
id: "ux-local-1",
|
|
4018
|
+
// Compatibility path for an unreadable requirement inventory: there is
|
|
4019
|
+
// no safe all-requirements registry phase, so the unscoped UX session
|
|
4020
|
+
// must establish its registry before recording state flow.
|
|
4021
|
+
toolNames: requirementIdsProvided
|
|
4022
|
+
? uxSegment.toolNames
|
|
4023
|
+
: new Set([...(uxSegment.toolNames ?? []), "record_state_registry"]),
|
|
4024
|
+
...(uxSlice.length > 0 ? { requirementSlice: uxSlice } : {}),
|
|
4025
|
+
prompt: uxSlice.length > 0
|
|
4026
|
+
? buildUxPrompt(uxSlice)
|
|
3525
4027
|
: buildPhasePrompt(uxSegment),
|
|
3526
|
-
})
|
|
4028
|
+
});
|
|
3527
4029
|
for (const segment of FRONTEND_PLAN_SEGMENTS) {
|
|
3528
|
-
if (["coverage", "ux-local", "finalize"].includes(segment.id))
|
|
4030
|
+
if (["coverage", "ux-registry", "ux-local", "finalize"].includes(segment.id))
|
|
3529
4031
|
continue;
|
|
3530
4032
|
queue.push({
|
|
3531
4033
|
id: segment.id,
|
|
@@ -3567,6 +4069,9 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3567
4069
|
else if (session.id === "compact-local") {
|
|
3568
4070
|
prompt = buildCompactLocalPrompt(session.missingFacts);
|
|
3569
4071
|
}
|
|
4072
|
+
else if (session.id === "ux-registry") {
|
|
4073
|
+
prompt = buildUxRegistryPrompt(session.missingFacts);
|
|
4074
|
+
}
|
|
3570
4075
|
else if (session.id.startsWith("ux-local-")) {
|
|
3571
4076
|
// Build this at execution time: coverage facts are committed by the
|
|
3572
4077
|
// preceding sessions and must be visible to the UX-local model.
|
|
@@ -3626,6 +4131,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3626
4131
|
})
|
|
3627
4132
|
: [];
|
|
3628
4133
|
const isCompactLocalSession = session.id === "compact-local";
|
|
4134
|
+
const isUxRegistrySession = session.id === "ux-registry";
|
|
3629
4135
|
const isUxLocalSession = session.id.startsWith("ux-local-");
|
|
3630
4136
|
const missingPhase = isCompactLocalSession && input.committedFacts
|
|
3631
4137
|
? [
|
|
@@ -3633,6 +4139,11 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3633
4139
|
requirementIds: allRequirementIds,
|
|
3634
4140
|
committedFacts: input.committedFacts(),
|
|
3635
4141
|
}),
|
|
4142
|
+
...collectFrontendPlanPhaseMissingFacts({
|
|
4143
|
+
phase: "ux-registry",
|
|
4144
|
+
requirementIds: allRequirementIds,
|
|
4145
|
+
committedFacts: input.committedFacts(),
|
|
4146
|
+
}),
|
|
3636
4147
|
...collectFrontendPlanPhaseMissingFacts({
|
|
3637
4148
|
phase: "ux-local",
|
|
3638
4149
|
requirementIds: allRequirementIds,
|
|
@@ -3640,20 +4151,26 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3640
4151
|
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
3641
4152
|
}),
|
|
3642
4153
|
]
|
|
3643
|
-
:
|
|
4154
|
+
: isUxRegistrySession && input.committedFacts
|
|
3644
4155
|
? collectFrontendPlanPhaseMissingFacts({
|
|
3645
|
-
phase: "ux-
|
|
3646
|
-
requirementIds:
|
|
4156
|
+
phase: "ux-registry",
|
|
4157
|
+
requirementIds: allRequirementIds,
|
|
3647
4158
|
committedFacts: input.committedFacts(),
|
|
3648
|
-
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
3649
4159
|
})
|
|
3650
|
-
:
|
|
4160
|
+
: isUxLocalSession && session.requirementSlice && input.committedFacts
|
|
3651
4161
|
? collectFrontendPlanPhaseMissingFacts({
|
|
3652
|
-
phase: "
|
|
3653
|
-
requirementIds:
|
|
4162
|
+
phase: "ux-local",
|
|
4163
|
+
requirementIds: session.requirementSlice,
|
|
3654
4164
|
committedFacts: input.committedFacts(),
|
|
4165
|
+
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
3655
4166
|
})
|
|
3656
|
-
:
|
|
4167
|
+
: session.id === "global-mock-data" && allRequirementIds.length > 0 && input.committedFacts
|
|
4168
|
+
? collectFrontendPlanPhaseMissingFacts({
|
|
4169
|
+
phase: "global-mock-data",
|
|
4170
|
+
requirementIds: allRequirementIds,
|
|
4171
|
+
committedFacts: input.committedFacts(),
|
|
4172
|
+
})
|
|
4173
|
+
: [];
|
|
3657
4174
|
const missingPhaseFacts = [...missingCoverage, ...missingPhase];
|
|
3658
4175
|
// Frozen verification commands must operate on files the planner has
|
|
3659
4176
|
// committed verification targets for; otherwise the admission writeSet
|
|
@@ -3681,11 +4198,13 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3681
4198
|
? buildCoveragePrompt(session.coverageSlice ?? [], missing)
|
|
3682
4199
|
: isCompactLocalSession
|
|
3683
4200
|
? buildCompactLocalPrompt(missing)
|
|
3684
|
-
:
|
|
3685
|
-
?
|
|
3686
|
-
:
|
|
3687
|
-
?
|
|
3688
|
-
:
|
|
4201
|
+
: isUxRegistrySession
|
|
4202
|
+
? buildUxRegistryPrompt(missing)
|
|
4203
|
+
: session.id === "finalize" && useCompactSmallPlan
|
|
4204
|
+
? buildCompactFinalizePrompt(missing)
|
|
4205
|
+
: isUxLocalSession
|
|
4206
|
+
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
4207
|
+
: buildPhasePrompt(globalMockDataSegment, missing);
|
|
3689
4208
|
if (result.ok) {
|
|
3690
4209
|
if (missingPhaseFacts.length === 0) {
|
|
3691
4210
|
index += 1;
|
|
@@ -3800,7 +4319,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3800
4319
|
});
|
|
3801
4320
|
continue;
|
|
3802
4321
|
}
|
|
3803
|
-
if ((session.coverageOnly || isUxLocalSession || session.id === "global-mock-data") &&
|
|
4322
|
+
if ((session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id === "global-mock-data") &&
|
|
3804
4323
|
missingPhaseFacts.length > 0 &&
|
|
3805
4324
|
!result.stderr.trim() &&
|
|
3806
4325
|
!result.timedOut &&
|
|
@@ -4083,6 +4602,10 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
4083
4602
|
store,
|
|
4084
4603
|
runDir: meta.runDir,
|
|
4085
4604
|
nodeId: input.task.id,
|
|
4605
|
+
canonicalRequirements: await resolveFrontendCanonicalRequirements({
|
|
4606
|
+
cwd: input.cwd,
|
|
4607
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
4608
|
+
}),
|
|
4086
4609
|
});
|
|
4087
4610
|
writerToolPolicy = {
|
|
4088
4611
|
requireSdk: true,
|
|
@@ -4145,6 +4668,10 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
4145
4668
|
cwd: input.cwd,
|
|
4146
4669
|
sourceBinding: meta.spec.sourceBinding,
|
|
4147
4670
|
}),
|
|
4671
|
+
declaredUiStateIds: await resolveFrontendDeclaredUiStateIds({
|
|
4672
|
+
runDir: meta.runDir,
|
|
4673
|
+
}),
|
|
4674
|
+
workspaceRoot: input.cwd,
|
|
4148
4675
|
});
|
|
4149
4676
|
writerToolPolicy = {
|
|
4150
4677
|
requireSdk: true,
|
|
@@ -4306,12 +4833,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
4306
4833
|
?.kind === "requirement")
|
|
4307
4834
|
.map((record) => record.fact?.id)
|
|
4308
4835
|
.filter((id) => typeof id === "string");
|
|
4309
|
-
for (const
|
|
4310
|
-
|
|
4311
|
-
if (fact?.kind !== "requirement" || typeof fact.id !== "string")
|
|
4312
|
-
continue;
|
|
4313
|
-
const evidence = fact.evidence;
|
|
4314
|
-
if (evidence?.behavior === "required")
|
|
4836
|
+
for (const fact of resolveFrontendContractRequirements(contractFacts.map((record) => record.fact))) {
|
|
4837
|
+
if (fact.evidence.behavior === "required")
|
|
4315
4838
|
behaviorRequiredRequirementIds.push(fact.id);
|
|
4316
4839
|
planRequirementCosts.set(fact.id, estimateFrontendPlanRequirementRecordCalls(fact));
|
|
4317
4840
|
}
|