@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
|
@@ -4,14 +4,15 @@ import { z } from "zod";
|
|
|
4
4
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
5
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
7
|
-
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
7
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_CANONICAL_CONTRACT_MAX_BYTES, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
8
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
9
9
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
10
10
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
11
|
+
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
11
12
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
12
13
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
13
14
|
/** Canonical contract serialization must stay at or below this byte size. */
|
|
14
|
-
export const FRONTEND_CONTRACT_MAX_BYTES =
|
|
15
|
+
export const FRONTEND_CONTRACT_MAX_BYTES = FRONTEND_CANONICAL_CONTRACT_MAX_BYTES;
|
|
15
16
|
/**
|
|
16
17
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
17
18
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -39,6 +40,7 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
39
40
|
"component-spec-not-cited",
|
|
40
41
|
"component-choices-missing",
|
|
41
42
|
"contract-too-large",
|
|
43
|
+
"revision-patch-unmergeable",
|
|
42
44
|
]);
|
|
43
45
|
export const frontendPrewriteResultV1Schema = z
|
|
44
46
|
.object({
|
|
@@ -160,9 +162,12 @@ async function readNodeText(runDir, nodeId) {
|
|
|
160
162
|
if (record.status !== "FINISHED") {
|
|
161
163
|
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
162
164
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
// Pi execution mirrors a successful structured-contract normalization into
|
|
166
|
+
// both assistantText and stdout. Treat node output the same way as the DAG
|
|
167
|
+
// runtime's canonicalNodeOutput(): assistantText is authoritative and stdout
|
|
168
|
+
// is only a fallback. Concatenating the two duplicate fields makes a single
|
|
169
|
+
// canonical plan appear as two complete JSON contracts to the prewrite gate.
|
|
170
|
+
const text = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
166
171
|
if (!text)
|
|
167
172
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
168
173
|
return text;
|
|
@@ -171,16 +176,129 @@ async function readNodeRawText(runDir, nodeId) {
|
|
|
171
176
|
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
172
177
|
return record.assistantText ?? record.stdout ?? "";
|
|
173
178
|
}
|
|
179
|
+
function resolveRunOwnedArtifact(runDir, candidate, label) {
|
|
180
|
+
const root = path.resolve(runDir);
|
|
181
|
+
const resolved = path.resolve(candidate);
|
|
182
|
+
const relative = path.relative(root, resolved);
|
|
183
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
184
|
+
throw new Error(`frontend prewrite gate ${label} must be contained by the current run directory`);
|
|
185
|
+
}
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
async function readHashBoundArtifact(input) {
|
|
189
|
+
const resolved = resolveRunOwnedArtifact(input.runDir, input.artifactPath, input.label);
|
|
190
|
+
const text = await readFile(resolved, "utf8");
|
|
191
|
+
const actual = sha256Hex(text);
|
|
192
|
+
if (actual !== input.expectedSha256) {
|
|
193
|
+
throw new Error(`frontend prewrite gate ${input.label} SHA-256 mismatch (expected ${input.expectedSha256}, got ${actual})`);
|
|
194
|
+
}
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
async function readFrontendPlanNodeInput(runDir, nodeId) {
|
|
198
|
+
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
199
|
+
if (record.status !== "FINISHED") {
|
|
200
|
+
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
201
|
+
}
|
|
202
|
+
const frontend = record.frontendPlanOutput;
|
|
203
|
+
if (!frontend) {
|
|
204
|
+
const payloadText = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
205
|
+
if (!payloadText)
|
|
206
|
+
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
207
|
+
return {
|
|
208
|
+
artifactFirst: false,
|
|
209
|
+
payloadText,
|
|
210
|
+
citationsText: payloadText,
|
|
211
|
+
rawCandidateSha256: sha256Hex(record.assistantText ?? record.stdout ?? ""),
|
|
212
|
+
payloadKind: "canonical-contract",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (frontend.schemaVersion !== 1 ||
|
|
216
|
+
!record.structuredArtifactPath ||
|
|
217
|
+
!record.structuredArtifactSha256 ||
|
|
218
|
+
!record.structuredArtifactSchemaId) {
|
|
219
|
+
throw new Error(`frontend prewrite gate ${nodeId} has incomplete artifact-first metadata`);
|
|
220
|
+
}
|
|
221
|
+
const expectedSchema = frontend.payloadKind === "canonical-contract"
|
|
222
|
+
? "frontend-implementation-contract-v1"
|
|
223
|
+
: "frontend-implementation-contract-revision-patch-v1";
|
|
224
|
+
if (record.structuredArtifactSchemaId !== expectedSchema) {
|
|
225
|
+
throw new Error(`frontend prewrite gate ${nodeId} artifact schema ${record.structuredArtifactSchemaId} does not match ${frontend.payloadKind}`);
|
|
226
|
+
}
|
|
227
|
+
const [payloadText, citationsText] = await Promise.all([
|
|
228
|
+
readHashBoundArtifact({
|
|
229
|
+
runDir,
|
|
230
|
+
artifactPath: record.structuredArtifactPath,
|
|
231
|
+
expectedSha256: record.structuredArtifactSha256,
|
|
232
|
+
label: `${nodeId} primary payload`,
|
|
233
|
+
}),
|
|
234
|
+
readHashBoundArtifact({
|
|
235
|
+
runDir,
|
|
236
|
+
artifactPath: frontend.citationsArtifactPath,
|
|
237
|
+
expectedSha256: frontend.citationsArtifactSha256,
|
|
238
|
+
label: `${nodeId} citations`,
|
|
239
|
+
}),
|
|
240
|
+
readHashBoundArtifact({
|
|
241
|
+
runDir,
|
|
242
|
+
artifactPath: frontend.rawCandidatePath,
|
|
243
|
+
expectedSha256: frontend.rawCandidateSha256,
|
|
244
|
+
label: `${nodeId} raw candidate`,
|
|
245
|
+
}).then(() => undefined),
|
|
246
|
+
]);
|
|
247
|
+
return {
|
|
248
|
+
artifactFirst: true,
|
|
249
|
+
payloadText,
|
|
250
|
+
citationsText,
|
|
251
|
+
rawCandidateSha256: frontend.rawCandidateSha256,
|
|
252
|
+
payloadKind: frontend.payloadKind,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Same shape predicate as the internal `isContract` inside
|
|
257
|
+
* `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
|
|
258
|
+
* schemaVersion 1 + a plain `targets` object + `requirements` and
|
|
259
|
+
* `verificationTargets` arrays. A merge-patch delta does not carry these
|
|
260
|
+
* fields, so this is the deterministic boundary between a full contract and a
|
|
261
|
+
* patch. Keep the two predicates in sync.
|
|
262
|
+
*/
|
|
263
|
+
function isFullContractShape(value) {
|
|
264
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
const record = value;
|
|
268
|
+
const targets = record.targets;
|
|
269
|
+
return (record.schemaVersion === 1 &&
|
|
270
|
+
targets !== null &&
|
|
271
|
+
typeof targets === "object" &&
|
|
272
|
+
!Array.isArray(targets) &&
|
|
273
|
+
Array.isArray(record.requirements) &&
|
|
274
|
+
Array.isArray(record.verificationTargets));
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Shared merge pipeline for revisionPatch mode and content-driven
|
|
278
|
+
* compatibility: read the original contract node, extract the original
|
|
279
|
+
* contract and the effective node's patch, apply the RFC 7386 merge-patch, and
|
|
280
|
+
* serialize deterministically. Throws on any failure; callers map the error to
|
|
281
|
+
* their own classification (retryable-invalid for explicit revisionPatch mode,
|
|
282
|
+
* blocked for content-driven fallback).
|
|
283
|
+
*/
|
|
284
|
+
async function mergeRevisionPatch(runDir, planText, originalNodeId) {
|
|
285
|
+
const originalText = await readNodeText(runDir, originalNodeId);
|
|
286
|
+
const originalValue = extractFrontendImplementationJson(originalText);
|
|
287
|
+
const patchValue = extractFrontendImplementationJson(planText);
|
|
288
|
+
const merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
|
|
289
|
+
return serializeDeterministicJson(merged);
|
|
290
|
+
}
|
|
174
291
|
function firstNonEmptyVerdictLine(text) {
|
|
175
292
|
const lines = text
|
|
176
293
|
.split(/\r?\n/)
|
|
177
294
|
.map((line) => line.trim())
|
|
178
295
|
.filter(Boolean);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
296
|
+
// Emphasis stripping (backtick + asterisk) is shared with the runIf
|
|
297
|
+
// selector and node-level output-protocol validation via
|
|
298
|
+
// normalizeVerdictCandidateLine. The gate keeps its own loose
|
|
299
|
+
// pass/request-revision normalization below (prefix match + trailing
|
|
300
|
+
// prose tolerance) for deterministic fail-closed behavior.
|
|
301
|
+
const normalizedLines = lines.map(normalizeVerdictCandidateLine);
|
|
184
302
|
const first = normalizedLines[0] ?? "";
|
|
185
303
|
// The prompt requires VERDICT to be the first non-empty line, but model
|
|
186
304
|
// output can still prepend a summary. Keep the protocol strict in the
|
|
@@ -416,9 +534,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
416
534
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
417
535
|
openspecPolicy,
|
|
418
536
|
};
|
|
419
|
-
let
|
|
537
|
+
let planInput;
|
|
420
538
|
try {
|
|
421
|
-
|
|
539
|
+
planInput = await readFrontendPlanNodeInput(input.runDir, planNodeId);
|
|
422
540
|
}
|
|
423
541
|
catch (error) {
|
|
424
542
|
return finalizePrewrite(input, {
|
|
@@ -428,6 +546,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
428
546
|
failureCode: "plan-text-unreadable",
|
|
429
547
|
});
|
|
430
548
|
}
|
|
549
|
+
const planText = planInput.payloadText;
|
|
550
|
+
const citationsText = planInput.citationsText;
|
|
551
|
+
basePending.candidateRawSha256 = planInput.rawCandidateSha256;
|
|
431
552
|
let reviewText;
|
|
432
553
|
try {
|
|
433
554
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -459,9 +580,21 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
459
580
|
let effectiveContractText = null;
|
|
460
581
|
if (revisionPatchMode) {
|
|
461
582
|
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
462
|
-
let originalText;
|
|
463
583
|
try {
|
|
464
|
-
|
|
584
|
+
const originalInput = await readFrontendPlanNodeInput(input.runDir, originalNodeId);
|
|
585
|
+
if (planInput.artifactFirst && !originalInput.artifactFirst) {
|
|
586
|
+
throw new Error(`artifact-first revision ${planNodeId} requires artifact-first original contract ${originalNodeId}`);
|
|
587
|
+
}
|
|
588
|
+
if (planInput.artifactFirst) {
|
|
589
|
+
if (planInput.payloadKind !== "merge-patch" ||
|
|
590
|
+
originalInput.payloadKind !== "canonical-contract") {
|
|
591
|
+
throw new Error("artifact-first revision/original payload kinds are invalid");
|
|
592
|
+
}
|
|
593
|
+
effectiveContractText = serializeDeterministicJson(applyFrontendImplementationContractPatch(JSON.parse(originalInput.payloadText), JSON.parse(planInput.payloadText)));
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
597
|
+
}
|
|
465
598
|
}
|
|
466
599
|
catch (error) {
|
|
467
600
|
return finalizePrewrite(input, {
|
|
@@ -472,35 +605,54 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
472
605
|
failureCode: "contract-invalid",
|
|
473
606
|
});
|
|
474
607
|
}
|
|
475
|
-
|
|
476
|
-
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
// Content-driven compatibility (Bug 1): legacy DAGs without
|
|
611
|
+
// revisionPatch can still receive a merge-patch delta from the revision
|
|
612
|
+
// node. Detect the output shape instead of trusting the DAG flag alone.
|
|
613
|
+
// - Full contract shape → keep legacy full-contract semantics unchanged.
|
|
614
|
+
// - Patch shape with a usable original contract node → auto-merge and
|
|
615
|
+
// run the existing analysis/materialization pipeline.
|
|
616
|
+
// - Patch shape but unmergeable → blocked + revision-patch-unmergeable
|
|
617
|
+
// (no retryable loop).
|
|
618
|
+
let planJsonValue = null;
|
|
477
619
|
try {
|
|
478
|
-
|
|
479
|
-
patchValue = extractFrontendImplementationJson(planText);
|
|
480
|
-
}
|
|
481
|
-
catch (error) {
|
|
482
|
-
return finalizePrewrite(input, {
|
|
483
|
-
...basePending,
|
|
484
|
-
verdict,
|
|
485
|
-
classification: "retryable-invalid",
|
|
486
|
-
failureReason: `frontend prewrite gate revisionPatch mode failed to parse contract/patch: ${error instanceof Error ? error.message : String(error)}`,
|
|
487
|
-
failureCode: "contract-invalid",
|
|
488
|
-
});
|
|
620
|
+
planJsonValue = extractFrontendImplementationJson(planText);
|
|
489
621
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
622
|
+
catch {
|
|
623
|
+
// Not machine-readable JSON: fall back to the legacy full-contract
|
|
624
|
+
// path where analyzeFrontendImplementationContract surfaces the
|
|
625
|
+
// original diagnostic (fail-closed without a new failure mode).
|
|
493
626
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
627
|
+
if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
|
|
628
|
+
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
629
|
+
if (!originalNodeId || originalNodeId === planNodeId) {
|
|
630
|
+
return finalizePrewrite(input, {
|
|
631
|
+
...basePending,
|
|
632
|
+
verdict,
|
|
633
|
+
classification: "blocked",
|
|
634
|
+
failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
|
|
635
|
+
failureCode: "revision-patch-unmergeable",
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
const originalText = await readNodeText(input.runDir, originalNodeId);
|
|
640
|
+
const originalValue = extractFrontendImplementationJson(originalText);
|
|
641
|
+
if (!isFullContractShape(originalValue)) {
|
|
642
|
+
throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
|
|
643
|
+
}
|
|
644
|
+
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
return finalizePrewrite(input, {
|
|
648
|
+
...basePending,
|
|
649
|
+
verdict,
|
|
650
|
+
classification: "blocked",
|
|
651
|
+
failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
|
|
652
|
+
failureCode: "revision-patch-unmergeable",
|
|
653
|
+
});
|
|
654
|
+
}
|
|
502
655
|
}
|
|
503
|
-
effectiveContractText = serializeDeterministicJson(merged);
|
|
504
656
|
}
|
|
505
657
|
const requirementIdSearchText = effectiveContractText ?? planText;
|
|
506
658
|
const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
|
|
@@ -550,14 +702,17 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
550
702
|
workspaceRoot,
|
|
551
703
|
});
|
|
552
704
|
}
|
|
553
|
-
// Candidate -> canonical conversion.
|
|
554
|
-
//
|
|
705
|
+
// Candidate -> canonical conversion. Artifact-first inputs must keep using
|
|
706
|
+
// the already hash-verified payload: re-reading the selected node record
|
|
707
|
+
// here would silently fall back to its prose and defeat the integrity gate.
|
|
708
|
+
// Historical records intentionally retain their node-text compatibility path.
|
|
555
709
|
let analysis;
|
|
556
710
|
try {
|
|
711
|
+
const rawContractText = effectiveContractText ?? (planInput.artifactFirst ? planText : undefined);
|
|
557
712
|
analysis = await analyzeFrontendImplementationContract({
|
|
558
713
|
runDir: input.runDir,
|
|
559
|
-
fromNodeId:
|
|
560
|
-
rawContractText
|
|
714
|
+
fromNodeId: rawContractText ? undefined : planNodeId,
|
|
715
|
+
rawContractText,
|
|
561
716
|
sourceBinding: input.sourceBinding,
|
|
562
717
|
});
|
|
563
718
|
}
|
|
@@ -702,7 +857,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
702
857
|
repoRoot: candidateRoot,
|
|
703
858
|
});
|
|
704
859
|
if (candidatePaths.length > 0) {
|
|
705
|
-
const parsed = parseOpenspecCitations(
|
|
860
|
+
const parsed = parseOpenspecCitations(citationsText, candidateRoot);
|
|
706
861
|
if (!parsed.blockPresent || parsed.unparseable) {
|
|
707
862
|
return finalizePrewrite(input, {
|
|
708
863
|
...basePending,
|
|
@@ -843,7 +998,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
843
998
|
});
|
|
844
999
|
}
|
|
845
1000
|
if (openspecPolicy === "cited") {
|
|
846
|
-
const parsedCitations = parseOpenspecCitations(
|
|
1001
|
+
const parsedCitations = parseOpenspecCitations(citationsText, candidateRoot);
|
|
847
1002
|
const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
|
|
848
1003
|
const readSet = new Set(openspecReadPaths);
|
|
849
1004
|
const uncorroborated = [
|