@tea-agent/loop-agent 0.38.1 → 0.39.0-beta.0
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 +115 -13
- package/README.md +2 -0
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init.js +2 -14
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +41 -4
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-sdk-executor.js +7 -26
- package/dist/executors/shell-executor.js +270 -16
- package/dist/governance/manifest-types.js +0 -2
- package/dist/shared/operator/capabilities.js +87 -22
- package/dist/task/config-types.js +22 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/pi-runtime.js +10 -5
- package/dist/worker/console/chat/routes.js +458 -47
- package/dist/worker/console/chat/semantic-activity.js +13 -13
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/harness-pi-model-matrix.js +2 -11
- package/dist/worker/console/operator-actions.js +5 -4
- package/dist/worker/console/pi-readiness.js +5 -4
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
- package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-Bj2M8iO5.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DvBN4JZc.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CUZgsiRC.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
- package/dist/worker/console/static/assets/index-DTZOKgAn.js +404 -0
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DzqWXNO_.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-DdnapKIH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-BVjAT9b8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CcfkA-Dq.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DH8FjnOK.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +0 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +38 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +809 -53
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +319 -13
- package/dist/workflows/dag/frontend-prewrite-gate.js +200 -45
- package/dist/workflows/dag/init-hybrid.js +689 -91
- package/dist/workflows/dag/node-execution.js +71 -18
- package/dist/workflows/dag/prompt.js +68 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +93 -27
- package/dist/workflows/dag/rerun-run.js +3 -0
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/types.js +104 -0
- package/dist/workflows/dag/upstream-artifacts.js +4 -0
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/harness.schema.json +0 -8
- package/harness.json +1 -8
- package/package.json +8 -4
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
- package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
|
@@ -21,6 +21,17 @@ const REVIEW_VERDICT_NODE_ID = "review-verdict-recovery-pi";
|
|
|
21
21
|
const INITIAL_REVIEW_NODE_ID = "initial-review-pi";
|
|
22
22
|
const INITIAL_REVIEW_GATE_NODE_ID = "initial-review-gate-shell";
|
|
23
23
|
const INITIAL_REVIEW_PASS_NODE_ID = "initial-review-pass-shell";
|
|
24
|
+
/**
|
|
25
|
+
* Standard-dag governance verify chain ids. When the convergence chain
|
|
26
|
+
* observes the governance standard gate, a legitimate verify-pi
|
|
27
|
+
* `request-revision` re-enters a bounded implement-repair loop instead of
|
|
28
|
+
* terminating the run at the gate, mirroring the review-chain re-entry
|
|
29
|
+
* semantics (AC3/AC4). Protocol/provider/safety failures on verify-pi keep
|
|
30
|
+
* failing closed; only a safe `request-revision` drives code repair here.
|
|
31
|
+
*/
|
|
32
|
+
const GOVERNANCE_VERIFY_GATE_NODE_ID = "governance-standard-gate-shell";
|
|
33
|
+
const GOVERNANCE_VERIFY_NODE_ID = "verify-pi";
|
|
34
|
+
const GOVERNANCE_VERIFY_WRITER_NODE_ID = "implement-pi";
|
|
24
35
|
// Automatic code repair is authorized only for an ordinary command/test
|
|
25
36
|
// failure. Unknown, protocol, validation, provider, safety, governance, and
|
|
26
37
|
// human-decision categories all fail closed instead of reaching a writer.
|
|
@@ -57,6 +68,15 @@ export async function runConvergencePassController(input) {
|
|
|
57
68
|
}
|
|
58
69
|
const chain = getConvergenceChain(input.spec);
|
|
59
70
|
const observesReview = chain.includes(REVIEW_GATE_NODE_ID);
|
|
71
|
+
const observesGovernanceVerify = chain.includes(GOVERNANCE_VERIFY_GATE_NODE_ID);
|
|
72
|
+
// Standard-dag governance verify convergence: verify-pi drives a bounded
|
|
73
|
+
// implement-repair loop on `request-revision`. This shape has no
|
|
74
|
+
// hard-verify/review nodes; dispatch before the legacy hard-verify logic so
|
|
75
|
+
// the standard governance DAG never falls into `unsupported-dag-shape`/
|
|
76
|
+
// `non-retry-failure` dead ends.
|
|
77
|
+
if (observesGovernanceVerify && !observesReview) {
|
|
78
|
+
return handleGovernanceVerifyConvergence(input);
|
|
79
|
+
}
|
|
60
80
|
if (input.tasksById.has(INITIAL_REVIEW_NODE_ID)) {
|
|
61
81
|
const initialReview = input.state.nodes[INITIAL_REVIEW_NODE_ID];
|
|
62
82
|
const initialGate = input.state.nodes[INITIAL_REVIEW_GATE_NODE_ID];
|
|
@@ -397,6 +417,127 @@ async function handleReviewRequestRevision(input, reviewGate) {
|
|
|
397
417
|
function getConvergenceChain(spec) {
|
|
398
418
|
return spec.convergence?.chainNodeIds ?? DEFAULT_CONVERGENCE_CHAIN_NODE_IDS;
|
|
399
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Legitimate governance verify revision: verify-pi finished and emitted
|
|
422
|
+
* `request-revision`. No secondary verdict-recovery node exists on the
|
|
423
|
+
* standard dag, so the single verify-pi verdict plus a non-transport failure
|
|
424
|
+
* category is the legitimacy bar (protocol-invalid/invalid-output/timeout on
|
|
425
|
+
* verify-pi fail closed instead of driving repair).
|
|
426
|
+
*/
|
|
427
|
+
function isLegitimateGovernanceVerifyRevision(state) {
|
|
428
|
+
const verifyNode = state.nodes[GOVERNANCE_VERIFY_NODE_ID];
|
|
429
|
+
return (verifyNode?.status === "FINISHED" &&
|
|
430
|
+
parseProcessVerdict(verifyNode) === "request-revision");
|
|
431
|
+
}
|
|
432
|
+
function isLegitimateGovernanceVerifyPass(state) {
|
|
433
|
+
const verifyNode = state.nodes[GOVERNANCE_VERIFY_NODE_ID];
|
|
434
|
+
return (verifyNode?.status === "FINISHED" &&
|
|
435
|
+
parseProcessVerdict(verifyNode) === "pass");
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Standard-dag governance verify convergence loop. Terminal pass when
|
|
439
|
+
* verify-pi passes; bounded re-entry (reset implement-pi + verify chain) when
|
|
440
|
+
* verify-pi legitimately requests revision. The reset lets the next
|
|
441
|
+
* implement-pi pass consume the prior verify findings through the structured
|
|
442
|
+
* convergence feedback block (node-execution injects it for implementer
|
|
443
|
+
* roles), so `already-satisfied` no-ops are explicitly contradicted by the
|
|
444
|
+
* preserved verify-pi evidence refs.
|
|
445
|
+
*/
|
|
446
|
+
async function handleGovernanceVerifyConvergence(input) {
|
|
447
|
+
const convergence = input.state.convergence;
|
|
448
|
+
const currentPass = convergence.currentPass || 1;
|
|
449
|
+
const verifyNode = input.state.nodes[GOVERNANCE_VERIFY_NODE_ID];
|
|
450
|
+
const verifyShell = input.state.nodes["verify-shell"];
|
|
451
|
+
// Hard verify commands failed: not a governance verdict issue. Without a
|
|
452
|
+
// supervised repair chain, this stays terminal so the failure-aware
|
|
453
|
+
// closeout reports it truthfully.
|
|
454
|
+
if (verifyShell?.status === "ERROR") {
|
|
455
|
+
const passRecord = await buildConvergencePassRecord({
|
|
456
|
+
pass: currentPass,
|
|
457
|
+
status: "terminal",
|
|
458
|
+
reason: "hard-verify-failed",
|
|
459
|
+
state: input.state,
|
|
460
|
+
runDir: input.runDir,
|
|
461
|
+
spec: input.spec,
|
|
462
|
+
});
|
|
463
|
+
convergence.passHistory.push(passRecord);
|
|
464
|
+
convergence.terminalReason = "hard-verify-failed";
|
|
465
|
+
await input.persistState();
|
|
466
|
+
return { retry: false };
|
|
467
|
+
}
|
|
468
|
+
if (isLegitimateGovernanceVerifyPass(input.state)) {
|
|
469
|
+
convergence.terminalReason = "hard-verify-pass";
|
|
470
|
+
await appendConvergenceKnowledgePattern({
|
|
471
|
+
cwd: input.cwd,
|
|
472
|
+
state: input.state,
|
|
473
|
+
});
|
|
474
|
+
return { retry: false };
|
|
475
|
+
}
|
|
476
|
+
const verifyFailureCategory = verifyNode?.failureCategory;
|
|
477
|
+
const legitimateRevision = isLegitimateGovernanceVerifyRevision(input.state);
|
|
478
|
+
if (!legitimateRevision ||
|
|
479
|
+
(verifyFailureCategory &&
|
|
480
|
+
REVIEW_SOURCE_NON_RETRY_FAILURES.has(verifyFailureCategory))) {
|
|
481
|
+
const passRecord = await buildConvergencePassRecord({
|
|
482
|
+
pass: currentPass,
|
|
483
|
+
status: "terminal",
|
|
484
|
+
reason: "non-retry-failure",
|
|
485
|
+
state: input.state,
|
|
486
|
+
runDir: input.runDir,
|
|
487
|
+
spec: input.spec,
|
|
488
|
+
});
|
|
489
|
+
convergence.passHistory.push(passRecord);
|
|
490
|
+
convergence.terminalReason = "non-retry-failure";
|
|
491
|
+
await input.persistState();
|
|
492
|
+
return { retry: false };
|
|
493
|
+
}
|
|
494
|
+
const passRecord = await buildConvergencePassRecord({
|
|
495
|
+
pass: currentPass,
|
|
496
|
+
status: "retrying",
|
|
497
|
+
reason: "verify-request-revision",
|
|
498
|
+
state: input.state,
|
|
499
|
+
runDir: input.runDir,
|
|
500
|
+
spec: input.spec,
|
|
501
|
+
});
|
|
502
|
+
if (currentPass >= convergence.maxPasses) {
|
|
503
|
+
passRecord.status = "terminal";
|
|
504
|
+
passRecord.reason = "max-passes";
|
|
505
|
+
convergence.passHistory.push(passRecord);
|
|
506
|
+
convergence.terminalReason = "max-passes";
|
|
507
|
+
await input.persistState();
|
|
508
|
+
return { retry: false };
|
|
509
|
+
}
|
|
510
|
+
convergence.passHistory.push(passRecord);
|
|
511
|
+
convergence.currentPass = currentPass + 1;
|
|
512
|
+
// Reset the writer plus the governance verify chain: the next implement-pi
|
|
513
|
+
// pass must consume the preserved verify-pi findings (injected as
|
|
514
|
+
// convergence feedback) instead of returning already-satisfied.
|
|
515
|
+
for (const id of collectGovernanceVerifyResetIds(input.spec)) {
|
|
516
|
+
const task = input.tasksById.get(id);
|
|
517
|
+
if (!task)
|
|
518
|
+
continue;
|
|
519
|
+
input.state.nodes[id] = freshNodeRecord(task);
|
|
520
|
+
}
|
|
521
|
+
await input.persistState();
|
|
522
|
+
return { retry: true };
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Reset set for the governance verify loop: the exclusive writer and the
|
|
526
|
+
* verify chain closure (verify-shell, verify-pi, governance gate, and every
|
|
527
|
+
* transitive descendant so closeout re-adjudicates from fresh evidence).
|
|
528
|
+
*/
|
|
529
|
+
function collectGovernanceVerifyResetIds(spec) {
|
|
530
|
+
const resetIds = new Set([
|
|
531
|
+
GOVERNANCE_VERIFY_WRITER_NODE_ID,
|
|
532
|
+
"verify-shell",
|
|
533
|
+
GOVERNANCE_VERIFY_NODE_ID,
|
|
534
|
+
GOVERNANCE_VERIFY_GATE_NODE_ID,
|
|
535
|
+
]);
|
|
536
|
+
for (const id of collectTransitiveDescendantTaskIds(spec, GOVERNANCE_VERIFY_GATE_NODE_ID)) {
|
|
537
|
+
resetIds.add(id);
|
|
538
|
+
}
|
|
539
|
+
return resetIds;
|
|
540
|
+
}
|
|
400
541
|
function hasConvergenceChain(tasksById, spec) {
|
|
401
542
|
const chain = getConvergenceChain(spec);
|
|
402
543
|
return chain.every((id) => tasksById.has(id));
|
|
@@ -367,19 +367,26 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
const budgetBlockedIds = new Set(blockedChildren.map((entry) => entry.nodeId));
|
|
370
|
+
const failOnTokenBudgetExhaustion = input.expansion.failOnTokenBudgetExhaustion === true;
|
|
370
371
|
const failedChildren = tolerateChildFailures
|
|
371
372
|
? []
|
|
372
373
|
: childNodeIds.filter((nodeId) => {
|
|
373
374
|
const record = input.state.nodes[nodeId];
|
|
374
375
|
if (!record || record.status === "FINISHED")
|
|
375
376
|
return false;
|
|
376
|
-
if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record))
|
|
377
|
-
return
|
|
377
|
+
if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record)) {
|
|
378
|
+
return failOnTokenBudgetExhaustion;
|
|
379
|
+
}
|
|
378
380
|
return true;
|
|
379
381
|
});
|
|
382
|
+
const finishedItemCount = childNodeIds.filter((nodeId) => input.state.nodes[nodeId]?.status === "FINISHED").length;
|
|
383
|
+
const blockedItemCount = budgetBlockedIds.size;
|
|
380
384
|
const aggregate = {
|
|
381
385
|
workflowNodeId: input.expansion.workflowNodeId,
|
|
382
386
|
itemCount: items.length,
|
|
387
|
+
expectedItemCount: items.length,
|
|
388
|
+
finishedItemCount,
|
|
389
|
+
blockedItemCount,
|
|
383
390
|
children: childNodeIds.map((nodeId, index) => ({
|
|
384
391
|
nodeId,
|
|
385
392
|
item: items[index],
|
|
@@ -399,21 +406,30 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
399
406
|
?.reason,
|
|
400
407
|
})),
|
|
401
408
|
tokensUsed: totalTokensUsed,
|
|
409
|
+
maxTotalTokens: tokenBudget?.maxTotalTokens,
|
|
402
410
|
caseOutcomeNotes: caseOutcomeNotes.length > 0 ? caseOutcomeNotes : undefined,
|
|
403
411
|
};
|
|
412
|
+
const tokenBudgetFailure = failOnTokenBudgetExhaustion && blockedItemCount > 0;
|
|
404
413
|
return {
|
|
405
|
-
// With tolerateChildFailures: barrier
|
|
406
|
-
// case evidence
|
|
407
|
-
|
|
414
|
+
// With tolerateChildFailures: barrier succeeds and product outcomes live in
|
|
415
|
+
// case evidence. Backend-test opts into token-budget fail-closed because a
|
|
416
|
+
// partial generated module set is never a valid downstream input.
|
|
417
|
+
ok: failedChildren.length === 0 && !tokenBudgetFailure,
|
|
408
418
|
stdout: JSON.stringify(aggregate),
|
|
409
419
|
stderr: caseOutcomeNotes.length > 0
|
|
410
420
|
? `map children recorded as case outcomes: ${caseOutcomeNotes
|
|
411
421
|
.map((entry) => `${entry.nodeId}=${entry.status}:${entry.reason}`)
|
|
412
422
|
.join(", ")}`
|
|
423
|
+
: tokenBudgetFailure
|
|
424
|
+
? `dynamic map token budget exhausted: expected=${items.length} finished=${finishedItemCount} blocked=${blockedItemCount} tokensUsed=${totalTokensUsed} maxTotalTokens=${tokenBudget?.maxTotalTokens ?? "unbounded"}`
|
|
425
|
+
: failedChildren.length > 0
|
|
426
|
+
? `dynamic map children failed: ${failedChildren.join(", ")}`
|
|
427
|
+
: "",
|
|
428
|
+
failureCategory: tokenBudgetFailure
|
|
429
|
+
? "dynamic-expansion-token-budget-exhausted"
|
|
413
430
|
: failedChildren.length > 0
|
|
414
|
-
?
|
|
415
|
-
: "",
|
|
416
|
-
failureCategory: failedChildren.length > 0 ? "dynamic-expansion-child-failed" : "success",
|
|
431
|
+
? "dynamic-expansion-child-failed"
|
|
432
|
+
: "success",
|
|
417
433
|
durationMs: Date.now() - started,
|
|
418
434
|
};
|
|
419
435
|
}
|
|
@@ -30,7 +30,7 @@ export function getNodeOutputAsJson(state, nodeId) {
|
|
|
30
30
|
/** Normalize a line the same way verdict gates do (strip whole-line Markdown emphasis). */
|
|
31
31
|
export function normalizeVerdictCandidateLine(value) {
|
|
32
32
|
const trimmed = String(value).trim();
|
|
33
|
-
const emphasized = trimmed.match(
|
|
33
|
+
const emphasized = trimmed.match(/^(?:`{1,3}|\*{1,3})\s*(VERDICT:[^`*]+?)\s*(?:`{1,3}|\*{1,3})$/);
|
|
34
34
|
const line = (emphasized ? emphasized[1] : trimmed).trim();
|
|
35
35
|
// Canonicalize the colon separator and trailing sentence punctuation so
|
|
36
36
|
// "VERDICT:pass", "VERDICT: pass", or "VERDICT: pass." match the declared
|
|
@@ -13,6 +13,7 @@ export const frontendNormalizationActionSchema = z.enum([
|
|
|
13
13
|
"strip-comments",
|
|
14
14
|
"canonicalize-requirement-ids",
|
|
15
15
|
"canonicalize-verification-alias",
|
|
16
|
+
"drop-unresolved-verification-symbols",
|
|
16
17
|
"inject-source-binding",
|
|
17
18
|
]);
|
|
18
19
|
export function sha256Hex(input) {
|
|
@@ -40,6 +41,9 @@ export function serializeDeterministicJson(value) {
|
|
|
40
41
|
export function deterministicSha256(value) {
|
|
41
42
|
return sha256Hex(serializeDeterministicJson(value));
|
|
42
43
|
}
|
|
44
|
+
/** Provider-safe limits for the two planner outputs. Larger work must split. */
|
|
45
|
+
export const FRONTEND_PLAN_RESPONSE_MAX_BYTES = 14 * 1024;
|
|
46
|
+
export const FRONTEND_CANONICAL_CONTRACT_MAX_BYTES = 16 * 1024;
|
|
43
47
|
/** A contract materialization failure carries its classification plus the
|
|
44
48
|
* audit context collected up to the failure point, so the prewrite gate can
|
|
45
49
|
* persist a complete frontend-prewrite-result-v1 even when the canonical
|
|
@@ -102,6 +106,21 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
102
106
|
return [...labels];
|
|
103
107
|
}
|
|
104
108
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
109
|
+
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
110
|
+
/**
|
|
111
|
+
* Build the deterministic, run-owned portion of the frontend contract. The
|
|
112
|
+
* planner is allowed to fill semantic fields with an RFC 7386 merge patch but
|
|
113
|
+
* cannot redefine task identity, risk, or the implementation write boundary.
|
|
114
|
+
*/
|
|
115
|
+
export function buildFrontendImplementationContractSkeleton(input) {
|
|
116
|
+
return {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
sourceBinding: canonicalFrontendContractSourceBinding(input.sourceBinding),
|
|
119
|
+
riskLevel: input.riskLevel,
|
|
120
|
+
targets: { files: [...input.targetFiles] },
|
|
121
|
+
mockApi: { productionDefaultOff: true },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
105
124
|
/**
|
|
106
125
|
* Load the canonical frontend-implementation-contract-v1 JSON Schema from the
|
|
107
126
|
* installed loop-agent package docs/templates/ path. Package-root discovery
|
|
@@ -499,9 +518,9 @@ function isPlainObject(value) {
|
|
|
499
518
|
* ever materialized, so a patch that removes a required field still fails
|
|
500
519
|
* closed at the gate.
|
|
501
520
|
*/
|
|
502
|
-
export function
|
|
521
|
+
export function applyFrontendContractMergePatch(target, patch) {
|
|
503
522
|
if (!isPlainObject(patch)) {
|
|
504
|
-
throw new Error("frontend
|
|
523
|
+
throw new Error("frontend contract patch must be a JSON object");
|
|
505
524
|
}
|
|
506
525
|
const merge = (base, delta) => {
|
|
507
526
|
if (delta === null)
|
|
@@ -526,6 +545,9 @@ export function applyFrontendImplementationContractPatch(target, patch) {
|
|
|
526
545
|
};
|
|
527
546
|
return merge(target, patch);
|
|
528
547
|
}
|
|
548
|
+
export function applyFrontendImplementationContractPatch(target, patch) {
|
|
549
|
+
return applyFrontendContractMergePatch(target, patch);
|
|
550
|
+
}
|
|
529
551
|
export async function assertFrontendSourceBindingFresh(input) {
|
|
530
552
|
for (const source of input.binding.sources) {
|
|
531
553
|
const absolute = resolveDagTaskSourcePath({
|
|
@@ -1608,6 +1630,256 @@ export async function writeFrontendImplementationContractArtifact(input) {
|
|
|
1608
1630
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1609
1631
|
};
|
|
1610
1632
|
}
|
|
1633
|
+
const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
1634
|
+
"/schemaVersion",
|
|
1635
|
+
"/sourceBinding",
|
|
1636
|
+
"/riskLevel",
|
|
1637
|
+
"/targets/files",
|
|
1638
|
+
"/mockApi/productionDefaultOff",
|
|
1639
|
+
];
|
|
1640
|
+
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
1641
|
+
const record = asRecord(patch);
|
|
1642
|
+
if (!record)
|
|
1643
|
+
return ["/"];
|
|
1644
|
+
const violations = [];
|
|
1645
|
+
for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
|
|
1646
|
+
if (Object.hasOwn(record, key))
|
|
1647
|
+
violations.push(`/${key}`);
|
|
1648
|
+
}
|
|
1649
|
+
const targets = asRecord(record.targets);
|
|
1650
|
+
if (targets && Object.hasOwn(targets, "files"))
|
|
1651
|
+
violations.push("/targets/files");
|
|
1652
|
+
const mockApi = asRecord(record.mockApi);
|
|
1653
|
+
if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
|
|
1654
|
+
violations.push("/mockApi/productionDefaultOff");
|
|
1655
|
+
return violations;
|
|
1656
|
+
}
|
|
1657
|
+
function workspaceRootFromDagRunDir(runDir) {
|
|
1658
|
+
const absolute = path.resolve(runDir);
|
|
1659
|
+
const segments = absolute.split(path.sep);
|
|
1660
|
+
const harnessIndex = segments.lastIndexOf(".harness");
|
|
1661
|
+
if (harnessIndex < 0 ||
|
|
1662
|
+
segments[harnessIndex + 1] !== "dag-runs" ||
|
|
1663
|
+
harnessIndex + 3 >= segments.length) {
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
1667
|
+
return joined || path.parse(absolute).root;
|
|
1668
|
+
}
|
|
1669
|
+
function verificationSymbolMatchesFile(symbol, content) {
|
|
1670
|
+
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1671
|
+
return content.includes("describe(");
|
|
1672
|
+
const symbols = symbol.includes(" / ")
|
|
1673
|
+
? symbol
|
|
1674
|
+
.split(/[((]/, 1)[0]
|
|
1675
|
+
.split("/")
|
|
1676
|
+
.map((item) => item.trim())
|
|
1677
|
+
.filter(Boolean)
|
|
1678
|
+
: [symbol];
|
|
1679
|
+
return symbols.every((item) => {
|
|
1680
|
+
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1681
|
+
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1682
|
+
.filter((candidate) => Boolean(candidate))
|
|
1683
|
+
.some((candidate) => content.includes(candidate));
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
async function dropUnresolvedVerificationSymbols(input) {
|
|
1687
|
+
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
1688
|
+
const targets = asRecord(input.contract)?.verificationTargets;
|
|
1689
|
+
if (!workspaceRoot || !Array.isArray(targets))
|
|
1690
|
+
return false;
|
|
1691
|
+
let changed = false;
|
|
1692
|
+
for (const targetValue of targets) {
|
|
1693
|
+
const target = asRecord(targetValue);
|
|
1694
|
+
const file = asString(target?.file);
|
|
1695
|
+
const symbol = asString(target?.symbol);
|
|
1696
|
+
if (!target || !file || !symbol)
|
|
1697
|
+
continue;
|
|
1698
|
+
const absolute = path.resolve(workspaceRoot, file);
|
|
1699
|
+
const relative = path.relative(workspaceRoot, absolute);
|
|
1700
|
+
if (relative.startsWith("..") ||
|
|
1701
|
+
path.isAbsolute(relative) ||
|
|
1702
|
+
relative.includes("..")) {
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
let content;
|
|
1706
|
+
try {
|
|
1707
|
+
content = await readFile(absolute, "utf8");
|
|
1708
|
+
}
|
|
1709
|
+
catch {
|
|
1710
|
+
// Missing files remain a trace-time hard failure; do not hide them by
|
|
1711
|
+
// changing the optional symbol during candidate normalization.
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
if (verificationSymbolMatchesFile(symbol, content))
|
|
1715
|
+
continue;
|
|
1716
|
+
delete target.symbol;
|
|
1717
|
+
changed = true;
|
|
1718
|
+
}
|
|
1719
|
+
return changed;
|
|
1720
|
+
}
|
|
1721
|
+
function extractSingleOpenspecCitationsBlock(text) {
|
|
1722
|
+
const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
|
|
1723
|
+
if (blocks.length !== 1) {
|
|
1724
|
+
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1725
|
+
}
|
|
1726
|
+
return blocks[0];
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Frontend plan/revision output has two JSON-bearing regions: the primary
|
|
1730
|
+
* payload and line-oriented OpenSpec citations. Keep their parsing domains
|
|
1731
|
+
* separate so a citation row can never become a competing contract candidate.
|
|
1732
|
+
* A bare JSON payload is retained only for historical node-output compatibility.
|
|
1733
|
+
*/
|
|
1734
|
+
export function extractFrontendPlanPayload(text) {
|
|
1735
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(text);
|
|
1736
|
+
return {
|
|
1737
|
+
payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
|
|
1738
|
+
citationsBlock,
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function extractFrontendPlanPrimaryPayload(text) {
|
|
1742
|
+
const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
|
|
1743
|
+
if (fences.length > 1) {
|
|
1744
|
+
throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
|
|
1745
|
+
}
|
|
1746
|
+
if (fences.length === 1) {
|
|
1747
|
+
try {
|
|
1748
|
+
return JSON.parse(fences[0][1]);
|
|
1749
|
+
}
|
|
1750
|
+
catch (error) {
|
|
1751
|
+
throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return extractFrontendImplementationJson(text);
|
|
1755
|
+
}
|
|
1756
|
+
async function writeFrontendPlanCandidateRaw(input) {
|
|
1757
|
+
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1758
|
+
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text);
|
|
1759
|
+
return relativePath;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1763
|
+
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1764
|
+
* format/schema failures can be inspected and replayed without trusting model
|
|
1765
|
+
* prose. A successful result returns canonical full-contract text for all
|
|
1766
|
+
* downstream review and prewrite nodes.
|
|
1767
|
+
*/
|
|
1768
|
+
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
1769
|
+
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
1770
|
+
const attempt = input.attempt ?? 1;
|
|
1771
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1772
|
+
const rawPath = await writeFrontendPlanCandidateRaw({
|
|
1773
|
+
runDir: input.runDir,
|
|
1774
|
+
nodeId,
|
|
1775
|
+
attempt,
|
|
1776
|
+
text: input.text,
|
|
1777
|
+
});
|
|
1778
|
+
const reportBase = {
|
|
1779
|
+
schemaVersion: 1,
|
|
1780
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
|
|
1781
|
+
nodeId,
|
|
1782
|
+
attempt,
|
|
1783
|
+
rawPath,
|
|
1784
|
+
protectedPaths: [...FRONTEND_PLAN_PATCH_PROTECTED_PATHS],
|
|
1785
|
+
candidateRawSha256: sha256Hex(input.text),
|
|
1786
|
+
};
|
|
1787
|
+
let extractedPatchPath;
|
|
1788
|
+
let extractedPatchSha256;
|
|
1789
|
+
const writeReport = async (value) => {
|
|
1790
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
|
|
1791
|
+
...reportBase,
|
|
1792
|
+
...(extractedPatchPath
|
|
1793
|
+
? { extractedPatchPath, extractedPatchSha256 }
|
|
1794
|
+
: {}),
|
|
1795
|
+
...value,
|
|
1796
|
+
});
|
|
1797
|
+
};
|
|
1798
|
+
try {
|
|
1799
|
+
if (Buffer.byteLength(input.text, "utf8") > FRONTEND_PLAN_RESPONSE_MAX_BYTES) {
|
|
1800
|
+
throw new Error(`frontend plan response exceeds the ${FRONTEND_PLAN_RESPONSE_MAX_BYTES}-byte limit`);
|
|
1801
|
+
}
|
|
1802
|
+
if (!input.sourceBinding)
|
|
1803
|
+
throw new Error("frontend plan patch validator requires DAG sourceBinding");
|
|
1804
|
+
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1805
|
+
if (!skeleton)
|
|
1806
|
+
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1807
|
+
const patch = extractFrontendPlanPrimaryPayload(input.text);
|
|
1808
|
+
if (!isPlainObject(patch))
|
|
1809
|
+
throw new Error("frontend plan patch must extract to one JSON object");
|
|
1810
|
+
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1811
|
+
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1812
|
+
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1813
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1814
|
+
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1815
|
+
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1816
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1817
|
+
if (protectedViolations.length > 0)
|
|
1818
|
+
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1819
|
+
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
1820
|
+
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
1821
|
+
runDir: input.runDir,
|
|
1822
|
+
contract: merged,
|
|
1823
|
+
});
|
|
1824
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
1825
|
+
runDir: input.runDir,
|
|
1826
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
1827
|
+
sourceBinding: input.sourceBinding,
|
|
1828
|
+
});
|
|
1829
|
+
if (Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8") >
|
|
1830
|
+
FRONTEND_CANONICAL_CONTRACT_MAX_BYTES) {
|
|
1831
|
+
throw new Error(`frontend canonical contract exceeds the ${FRONTEND_CANONICAL_CONTRACT_MAX_BYTES}-byte limit; split the task`);
|
|
1832
|
+
}
|
|
1833
|
+
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1834
|
+
await writeReport({
|
|
1835
|
+
classification: "accepted-normalized",
|
|
1836
|
+
normalizedContractPath: path.posix.relative(input.runDir.replaceAll(path.sep, "/"), normalizedArtifact.path.replaceAll(path.sep, "/")),
|
|
1837
|
+
normalizedContractSha256: normalizedArtifact.sha256,
|
|
1838
|
+
normalizationActions: [
|
|
1839
|
+
"apply-runtime-skeleton",
|
|
1840
|
+
...(droppedUnresolvedSymbols
|
|
1841
|
+
? ["drop-unresolved-verification-symbols"]
|
|
1842
|
+
: []),
|
|
1843
|
+
...analysis.normalizationActions,
|
|
1844
|
+
],
|
|
1845
|
+
errors: [],
|
|
1846
|
+
});
|
|
1847
|
+
return {
|
|
1848
|
+
ok: true,
|
|
1849
|
+
contract: analysis.canonical,
|
|
1850
|
+
structuredArtifact: {
|
|
1851
|
+
path: normalizedArtifact.path,
|
|
1852
|
+
sha256: normalizedArtifact.sha256,
|
|
1853
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1854
|
+
},
|
|
1855
|
+
frontendPlanOutput: {
|
|
1856
|
+
schemaVersion: 1,
|
|
1857
|
+
payloadKind: "canonical-contract",
|
|
1858
|
+
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1859
|
+
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1860
|
+
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1861
|
+
rawCandidateSha256: sha256Hex(input.text),
|
|
1862
|
+
},
|
|
1863
|
+
normalizedText: [
|
|
1864
|
+
"Frontend implementation contract compiled from the runtime skeleton.",
|
|
1865
|
+
"",
|
|
1866
|
+
"```json",
|
|
1867
|
+
serializeDeterministicJson(analysis.canonical),
|
|
1868
|
+
"```",
|
|
1869
|
+
citationsBlock,
|
|
1870
|
+
].join("\n"),
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
catch (error) {
|
|
1874
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1875
|
+
await writeReport({
|
|
1876
|
+
classification: "retryable-invalid",
|
|
1877
|
+
normalizationActions: [],
|
|
1878
|
+
errors: [{ path: "$", code: "invalid-output", message: reason }],
|
|
1879
|
+
});
|
|
1880
|
+
return { ok: false, reason };
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1611
1883
|
/**
|
|
1612
1884
|
* Node-output self-check for plan nodes that produce the implementation
|
|
1613
1885
|
* contract. Mirrors the prewrite gate validation so schema/typo/null violations
|
|
@@ -1635,7 +1907,7 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1635
1907
|
/**
|
|
1636
1908
|
* Node-output self-check for the plan revision node, which emits an RFC 7386
|
|
1637
1909
|
* merge-patch delta against the original contract instead of a full contract.
|
|
1638
|
-
* Deliberately loose and format-level: the output must contain
|
|
1910
|
+
* Deliberately loose and format-level: the output must contain exactly one
|
|
1639
1911
|
* ```openspec-citations fenced block and must extract (after the citations
|
|
1640
1912
|
* blocks are stripped) to exactly one JSON object. The delta carries no
|
|
1641
1913
|
* schemaVersion/targets, so no full-contract schema/semantic checks run here;
|
|
@@ -1643,25 +1915,32 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1643
1915
|
* the prewrite gate, which remains the only authority.
|
|
1644
1916
|
*/
|
|
1645
1917
|
export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1918
|
+
const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
|
|
1919
|
+
const attempt = input.attempt ?? 1;
|
|
1920
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1921
|
+
const rawPath = await writeFrontendPlanCandidateRaw({
|
|
1922
|
+
runDir: input.runDir,
|
|
1923
|
+
nodeId,
|
|
1924
|
+
attempt,
|
|
1925
|
+
text: input.text,
|
|
1926
|
+
});
|
|
1927
|
+
if (Buffer.byteLength(input.text, "utf8") > FRONTEND_PLAN_RESPONSE_MAX_BYTES) {
|
|
1649
1928
|
return {
|
|
1650
1929
|
ok: false,
|
|
1651
|
-
reason:
|
|
1930
|
+
reason: `frontend revision response exceeds the ${FRONTEND_PLAN_RESPONSE_MAX_BYTES}-byte limit`,
|
|
1652
1931
|
};
|
|
1653
1932
|
}
|
|
1654
|
-
|
|
1655
|
-
// would otherwise count as competing balanced objects in the extractor.
|
|
1656
|
-
const textWithoutCitations = input.text.replace(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g, "");
|
|
1933
|
+
let citationsBlock;
|
|
1657
1934
|
let patch;
|
|
1658
1935
|
try {
|
|
1659
|
-
|
|
1936
|
+
const parsed = extractFrontendPlanPayload(input.text);
|
|
1937
|
+
citationsBlock = parsed.citationsBlock;
|
|
1938
|
+
patch = parsed.payload;
|
|
1660
1939
|
}
|
|
1661
1940
|
catch (error) {
|
|
1662
1941
|
return {
|
|
1663
1942
|
ok: false,
|
|
1664
|
-
reason:
|
|
1943
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1665
1944
|
};
|
|
1666
1945
|
}
|
|
1667
1946
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
@@ -1670,7 +1949,34 @@ export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
|
1670
1949
|
reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
|
|
1671
1950
|
};
|
|
1672
1951
|
}
|
|
1673
|
-
|
|
1952
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1953
|
+
if (protectedViolations.length > 0) {
|
|
1954
|
+
return {
|
|
1955
|
+
ok: false,
|
|
1956
|
+
reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
|
|
1960
|
+
const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
|
|
1961
|
+
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1962
|
+
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1963
|
+
return {
|
|
1964
|
+
ok: true,
|
|
1965
|
+
contract: patch,
|
|
1966
|
+
structuredArtifact: {
|
|
1967
|
+
path: payloadArtifact.path,
|
|
1968
|
+
sha256: payloadArtifact.sha256,
|
|
1969
|
+
schemaId: "frontend-implementation-contract-revision-patch-v1",
|
|
1970
|
+
},
|
|
1971
|
+
frontendPlanOutput: {
|
|
1972
|
+
schemaVersion: 1,
|
|
1973
|
+
payloadKind: "merge-patch",
|
|
1974
|
+
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1975
|
+
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1976
|
+
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1977
|
+
rawCandidateSha256: sha256Hex(input.text),
|
|
1978
|
+
},
|
|
1979
|
+
};
|
|
1674
1980
|
}
|
|
1675
1981
|
export async function materializeFrontendImplementationContract(input) {
|
|
1676
1982
|
const analysis = await analyzeFrontendImplementationContract(input);
|