@tea-agent/loop-agent 0.39.0-beta.0 → 0.39.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-stamp.json +3 -3
- package/dist/workflows/dag/frontend-implementation-contract.js +19 -25
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -19
- package/dist/workflows/dag/init-hybrid.js +63 -27
- package/dist/workflows/dag/node-execution.js +53 -19
- package/dist/workflows/dag/types.js +4 -0
- package/package.json +1 -1
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.39.0-beta.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-08-
|
|
3
|
+
"version": "0.39.0-beta.2",
|
|
4
|
+
"gitSha": "cf766ed6e83a3498e13744ffd944ac10d7b1aeca",
|
|
5
|
+
"builtAt": "2026-08-21T07:43:32.268Z"
|
|
6
6
|
}
|
|
@@ -41,9 +41,6 @@ export function serializeDeterministicJson(value) {
|
|
|
41
41
|
export function deterministicSha256(value) {
|
|
42
42
|
return sha256Hex(serializeDeterministicJson(value));
|
|
43
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;
|
|
47
44
|
/** A contract materialization failure carries its classification plus the
|
|
48
45
|
* audit context collected up to the failure point, so the prewrite gate can
|
|
49
46
|
* persist a complete frontend-prewrite-result-v1 even when the canonical
|
|
@@ -1718,8 +1715,13 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1718
1715
|
}
|
|
1719
1716
|
return changed;
|
|
1720
1717
|
}
|
|
1721
|
-
function extractSingleOpenspecCitationsBlock(text) {
|
|
1722
|
-
const blocks = text.
|
|
1718
|
+
function extractSingleOpenspecCitationsBlock(text, citationsRequired = true) {
|
|
1719
|
+
const blocks = [...text.matchAll(/^```openspec-citations[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim)].map((match) => match[0]);
|
|
1720
|
+
if (!citationsRequired) {
|
|
1721
|
+
if (blocks.length === 0)
|
|
1722
|
+
return "";
|
|
1723
|
+
throw new Error("frontend plan output must not include an openspec-citations block when the generation-frozen OpenSpec candidate set is empty");
|
|
1724
|
+
}
|
|
1723
1725
|
if (blocks.length !== 1) {
|
|
1724
1726
|
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1725
1727
|
}
|
|
@@ -1731,8 +1733,8 @@ function extractSingleOpenspecCitationsBlock(text) {
|
|
|
1731
1733
|
* separate so a citation row can never become a competing contract candidate.
|
|
1732
1734
|
* A bare JSON payload is retained only for historical node-output compatibility.
|
|
1733
1735
|
*/
|
|
1734
|
-
export function extractFrontendPlanPayload(text) {
|
|
1735
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(text);
|
|
1736
|
+
export function extractFrontendPlanPayload(text, options) {
|
|
1737
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(text, options?.citationsRequired !== false);
|
|
1736
1738
|
return {
|
|
1737
1739
|
payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
|
|
1738
1740
|
citationsBlock,
|
|
@@ -1796,21 +1798,20 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1796
1798
|
});
|
|
1797
1799
|
};
|
|
1798
1800
|
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
1801
|
if (!input.sourceBinding)
|
|
1803
1802
|
throw new Error("frontend plan patch validator requires DAG sourceBinding");
|
|
1804
1803
|
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1805
1804
|
if (!skeleton)
|
|
1806
1805
|
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1806
|
+
// Preserve the raw primary candidate even if the separately parsed
|
|
1807
|
+
// citation fence is malformed; the retry report is an audit artifact.
|
|
1807
1808
|
const patch = extractFrontendPlanPrimaryPayload(input.text);
|
|
1808
1809
|
if (!isPlainObject(patch))
|
|
1809
1810
|
throw new Error("frontend plan patch must extract to one JSON object");
|
|
1810
1811
|
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1811
1812
|
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1812
1813
|
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1813
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1814
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text, input.structuredContractOutput?.citationsRequired !== false);
|
|
1814
1815
|
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1815
1816
|
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1816
1817
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
@@ -1826,10 +1827,6 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1826
1827
|
rawContractText: serializeDeterministicJson(merged),
|
|
1827
1828
|
sourceBinding: input.sourceBinding,
|
|
1828
1829
|
});
|
|
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
1830
|
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1834
1831
|
await writeReport({
|
|
1835
1832
|
classification: "accepted-normalized",
|
|
@@ -1907,9 +1904,10 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1907
1904
|
/**
|
|
1908
1905
|
* Node-output self-check for the plan revision node, which emits an RFC 7386
|
|
1909
1906
|
* merge-patch delta against the original contract instead of a full contract.
|
|
1910
|
-
* Deliberately loose and format-level:
|
|
1911
|
-
* ```openspec-citations fenced block
|
|
1912
|
-
*
|
|
1907
|
+
* Deliberately loose and format-level: when OpenSpec candidates exist, the
|
|
1908
|
+
* output contains exactly one ```openspec-citations fenced block; otherwise it
|
|
1909
|
+
* contains no citation fence. It must extract (after the citations block is
|
|
1910
|
+
* stripped) to exactly one JSON object. The delta carries no
|
|
1913
1911
|
* schemaVersion/targets, so no full-contract schema/semantic checks run here;
|
|
1914
1912
|
* content parsing, patch application, and merged-contract validation stay with
|
|
1915
1913
|
* the prewrite gate, which remains the only authority.
|
|
@@ -1924,16 +1922,12 @@ export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
|
1924
1922
|
attempt,
|
|
1925
1923
|
text: input.text,
|
|
1926
1924
|
});
|
|
1927
|
-
if (Buffer.byteLength(input.text, "utf8") > FRONTEND_PLAN_RESPONSE_MAX_BYTES) {
|
|
1928
|
-
return {
|
|
1929
|
-
ok: false,
|
|
1930
|
-
reason: `frontend revision response exceeds the ${FRONTEND_PLAN_RESPONSE_MAX_BYTES}-byte limit`,
|
|
1931
|
-
};
|
|
1932
|
-
}
|
|
1933
1925
|
let citationsBlock;
|
|
1934
1926
|
let patch;
|
|
1935
1927
|
try {
|
|
1936
|
-
const parsed = extractFrontendPlanPayload(input.text
|
|
1928
|
+
const parsed = extractFrontendPlanPayload(input.text, {
|
|
1929
|
+
citationsRequired: input.structuredContractOutput?.citationsRequired,
|
|
1930
|
+
});
|
|
1937
1931
|
citationsBlock = parsed.citationsBlock;
|
|
1938
1932
|
patch = parsed.payload;
|
|
1939
1933
|
}
|
|
@@ -4,15 +4,13 @@ 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,
|
|
7
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, 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
11
|
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
12
12
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
13
13
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
14
|
-
/** Canonical contract serialization must stay at or below this byte size. */
|
|
15
|
-
export const FRONTEND_CONTRACT_MAX_BYTES = FRONTEND_CANONICAL_CONTRACT_MAX_BYTES;
|
|
16
14
|
/**
|
|
17
15
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
18
16
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -39,7 +37,6 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
39
37
|
"component-spec-reference-invalid",
|
|
40
38
|
"component-spec-not-cited",
|
|
41
39
|
"component-choices-missing",
|
|
42
|
-
"contract-too-large",
|
|
43
40
|
"revision-patch-unmergeable",
|
|
44
41
|
]);
|
|
45
42
|
export const frontendPrewriteResultV1Schema = z
|
|
@@ -732,21 +729,6 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
732
729
|
}
|
|
733
730
|
throw error;
|
|
734
731
|
}
|
|
735
|
-
// Size guard: a canonical contract above 64KB is a retryable-invalid defect
|
|
736
|
-
// (compress the contract) rather than a writer authorization failure.
|
|
737
|
-
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
|
|
738
|
-
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
739
|
-
return finalizePrewrite(input, {
|
|
740
|
-
...basePending,
|
|
741
|
-
verdict,
|
|
742
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
743
|
-
normalizationActions: [],
|
|
744
|
-
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
745
|
-
classification: "retryable-invalid",
|
|
746
|
-
failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
|
|
747
|
-
failureCode: "contract-too-large",
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
732
|
const mockStrategy = analysis.canonical.mockApi.strategy;
|
|
751
733
|
if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
|
|
752
734
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
@@ -1530,6 +1530,21 @@ function buildSourceContextBlock(sources) {
|
|
|
1530
1530
|
}
|
|
1531
1531
|
return parts.join("\n\n");
|
|
1532
1532
|
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Planner/reviewer nodes already receive the contract/scout artifacts. They
|
|
1535
|
+
* only need stable task-source pointers for an unresolved or conflicting
|
|
1536
|
+
* field, not a second inline copy of every fact-source document.
|
|
1537
|
+
*/
|
|
1538
|
+
function buildArtifactFirstSourceAccessBlock(sources) {
|
|
1539
|
+
const binding = buildDagSourceBinding(sources);
|
|
1540
|
+
return [
|
|
1541
|
+
"## Bound task-source access (read only when upstream artifacts are missing or conflict)",
|
|
1542
|
+
...binding.sources.map((source) => `- ${source.kind}: ${source.path}`),
|
|
1543
|
+
"Use these exact repository-readable paths. Do not search for substitutes or read runtime implementation code to diagnose a format retry.",
|
|
1544
|
+
`- taskId: ${binding.taskId}`,
|
|
1545
|
+
`- allowedPaths: ${sources.taskConfig.allowedPaths.join(", ") || "(none)"}`,
|
|
1546
|
+
].join("\n");
|
|
1547
|
+
}
|
|
1533
1548
|
async function loadMaterializedSourceReferences(sourceDir) {
|
|
1534
1549
|
const referenceDir = path.join(sourceDir, REFERENCE_DIRECTORY);
|
|
1535
1550
|
const referencePaths = [];
|
|
@@ -2523,7 +2538,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2523
2538
|
"Emit only editable fields: requirements[], implementationSteps[], targets.routes/publicApiChanges, uiStates[], interactions[], mockApi.strategy/activation/endpoints, verificationTargets[], designEvidence, evidenceGaps[], stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap.",
|
|
2524
2539
|
"Protected and forbidden: schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, schemaId, targetFiles, requirementCoverage.",
|
|
2525
2540
|
"Invariants: every requirement has expectedOutcome; every interaction has trigger + expectedBehavior; applicable uiStates have expectedBehavior + implementationTargets + verificationTargetIds; non-applicable uiStates have notApplicableReason; commandLabel is frozen; mockApi production default remains off.",
|
|
2526
|
-
"
|
|
2541
|
+
"Each behavior belongs to one primary row: requirements cover acceptance, uiStates cover only distinct visible states, and interactions cover only distinct user triggers. Link with IDs; do not restate the same prose in all three. Steps are action plus files, not a prose mirror. Omit optional fields when upstream contract/scout facts already settle them, and omit unchanged revision fields.",
|
|
2542
|
+
"Enums: mock strategy native|browser-intercept|request-adapter|not-needed; uiComponentChoices decision specified|reuse-existing|new. Omit optional empty strings.",
|
|
2527
2543
|
].join("\n");
|
|
2528
2544
|
const sourceContext = [
|
|
2529
2545
|
buildSourceContextBlock(sources),
|
|
@@ -2531,24 +2547,20 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2531
2547
|
]
|
|
2532
2548
|
.filter(Boolean)
|
|
2533
2549
|
.join("\n\n");
|
|
2550
|
+
const artifactFirstSourceContext = [
|
|
2551
|
+
buildArtifactFirstSourceAccessBlock(sources),
|
|
2552
|
+
capabilityContextBlock,
|
|
2553
|
+
]
|
|
2554
|
+
.filter(Boolean)
|
|
2555
|
+
.join("\n\n");
|
|
2534
2556
|
const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
|
|
2535
2557
|
mockCapability.verifyCommands.length > 0;
|
|
2536
2558
|
const requirementIds = frontendSourceBinding.requirementIds;
|
|
2537
2559
|
const requirementCoverageInstruction = requirementIds.length > 0
|
|
2538
2560
|
? [
|
|
2539
|
-
|
|
2540
|
-
`
|
|
2541
|
-
|
|
2542
|
-
``,
|
|
2543
|
-
`Bad example (empty expectedOutcome, empty targets -- REJECTED at contract materialization):`,
|
|
2544
|
-
`- AC-001: expectedOutcome="" implementationTargets=[] verificationTargets=[]`,
|
|
2545
|
-
``,
|
|
2546
|
-
`Good example (concrete expectedOutcome, real files, real verification targets):`,
|
|
2547
|
-
`- AC-001: expectedOutcome="TypeScript compilation exits with code 0 and produces no errors in dist/" implementationTargets=["src/app.tsx"] verificationTargets=["vt-typecheck":"npm run typecheck","tsconfig.json"]`,
|
|
2548
|
-
``,
|
|
2549
|
-
...requirementIds.map((id) => `- ${id}: [expectedOutcome] [implementation files] [verification targets]`),
|
|
2550
|
-
``,
|
|
2551
|
-
`Every requirement MUST have a non-empty expectedOutcome. Every interaction MUST have non-empty trigger and expectedBehavior. UI states with applicable=true MUST have non-empty expectedBehavior. Empty strings or omitted fields for these will cause contract rejection.`,
|
|
2561
|
+
"## Requirement Coverage",
|
|
2562
|
+
`Cover each frozen ID exactly once in requirements[]: ${requirementIds.join(", ")}. Each item needs a concrete expectedOutcome, implementationTargets, and verificationTargetIds.`,
|
|
2563
|
+
"Interactions need trigger + expectedBehavior; applicable uiStates need expectedBehavior + implementationTargets + verificationTargetIds. Do not use empty strings.",
|
|
2552
2564
|
].join("\n")
|
|
2553
2565
|
: "";
|
|
2554
2566
|
const verificationTargetFileInstruction = [
|
|
@@ -2562,6 +2574,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2562
2574
|
"Before producing this contract/plan, use the Pi read tool to read the FULL bound source files (需求.md, 执行约束.md, and every `references/*` Bound readPath listed above) — the inline copies above may be truncated excerpts, and requirement/acceptance references are authoritative only in their full form.",
|
|
2563
2575
|
"Do not drop scope fields, acceptance criteria, non-goals, UI states, or column/field definitions that exist in the full sources but are absent from the inline excerpts; if a field appears in the full source, it belongs in the contract.",
|
|
2564
2576
|
].join("\n");
|
|
2577
|
+
const plannerSourceReadInstruction = [
|
|
2578
|
+
"## Source-read policy",
|
|
2579
|
+
"Use frontend-contract-pi and frontend-scout-pi artifacts as primary evidence. Do not repeat reads that those facts already settle.",
|
|
2580
|
+
"Read a bound source only to resolve a missing or conflicting field; read every applicable OpenSpec candidate directly before citing it. Never infer omitted task fields from a preview.",
|
|
2581
|
+
].join("\n");
|
|
2565
2582
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2566
2583
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
2567
2584
|
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
@@ -2710,6 +2727,17 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2710
2727
|
...(classified?.theme ?? []),
|
|
2711
2728
|
...(classified?.rule.components ?? []),
|
|
2712
2729
|
].filter((value, index, array) => array.indexOf(value) === index).sort();
|
|
2730
|
+
const frontendPlanCitationsRequired = openspecGate.openspecCandidatePaths.length > 0 ||
|
|
2731
|
+
componentSpecCandidatePaths.length > 0;
|
|
2732
|
+
const frontendPlanCitationInstruction = frontendPlanCitationsRequired
|
|
2733
|
+
? openspecCitationInstruction
|
|
2734
|
+
: "The generation-frozen OpenSpec/component candidate set is empty. Do not read, cite, or emit an openspec-citations fence; output only the primary JSON patch. The runtime records an empty citation artifact deterministically.";
|
|
2735
|
+
const frontendPlanOutputContract = frontendPlanCitationsRequired
|
|
2736
|
+
? "JSON-only patch output: exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton, immediately followed by exactly one ```openspec-citations``` fenced citation block. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime applies the patch, validates it, and promotes canonical full-contract JSON for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes."
|
|
2737
|
+
: "JSON-only patch output: exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton. The generation-frozen OpenSpec candidate set is empty, so do NOT emit an openspec-citations fence. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime applies the patch, validates it, and promotes canonical full-contract JSON for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes.";
|
|
2738
|
+
const frontendRevisionOutputContract = frontendPlanCitationsRequired
|
|
2739
|
+
? "When the initial design review requests revision, return exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively), immediately followed by exactly one ```openspec-citations``` fenced citation block. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only; the gate applies the patch on the original contract and renders plan.md deterministically. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes."
|
|
2740
|
+
: "JSON-only output: when the initial design review requests revision, return exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively). The generation-frozen OpenSpec candidate set is empty, so do NOT emit an openspec-citations fence. Do NOT emit a full contract, Markdown explanation, prose, or another fenced block. No file writes.";
|
|
2713
2741
|
if (openspecGate.openspecPolicy === "cited") {
|
|
2714
2742
|
if (openspecGate.openspecCandidatePaths.length === 0) {
|
|
2715
2743
|
advisories.push("openspec 策略 cited:契约声明的 requiredReadPaths 与任务源引用均为空,prewrite gate 不强制读取 openspec;如需增强规范门禁,请在 task.json.frontendOpenspec.requiredReadPaths 声明必读路径或在任务源中显式引用 openspec 文件。");
|
|
@@ -2795,11 +2823,12 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2795
2823
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
|
|
2796
2824
|
retryOnInvalid: true,
|
|
2797
2825
|
skeleton: frontendContractSkeleton,
|
|
2826
|
+
citationsRequired: frontendPlanCitationsRequired,
|
|
2798
2827
|
},
|
|
2799
2828
|
allowedPaths: readOnlyPaths,
|
|
2800
2829
|
forbiddenPaths,
|
|
2801
2830
|
skills: [],
|
|
2802
|
-
outputContract:
|
|
2831
|
+
outputContract: frontendPlanOutputContract,
|
|
2803
2832
|
subtask_prompt: [
|
|
2804
2833
|
"Use frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Return JSON-only output containing only an editable RFC 7386 plan patch. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
|
|
2805
2834
|
"The patch fields become the complete implementation plan after deterministic merge. Do not produce a separate plan document, prose mirror, or full contract.",
|
|
@@ -2807,19 +2836,22 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2807
2836
|
"Encode ordered steps (implementationSteps), target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the contract JSON fields. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2808
2837
|
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
2809
2838
|
"Consume the Scout TARGET_SURFACE evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
|
|
2810
|
-
|
|
2839
|
+
frontendPlanCitationsRequired
|
|
2840
|
+
? "Output exactly two adjacent blocks: (1) one fenced json object containing the editable plan patch; (2) one openspec-citations citation block."
|
|
2841
|
+
: "Output exactly one fenced json object containing the editable plan patch; do not emit an openspec-citations block.",
|
|
2842
|
+
"Do NOT emit protected skeleton fields, a full contract, Markdown plan explanation, raw JSON, or any other fenced block.",
|
|
2811
2843
|
"Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
|
|
2812
2844
|
requirementCoverageInstruction,
|
|
2813
2845
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2814
2846
|
verificationTargetFileInstruction,
|
|
2815
2847
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2816
|
-
|
|
2848
|
+
plannerSourceReadInstruction,
|
|
2817
2849
|
fixedVerificationContext,
|
|
2818
|
-
|
|
2850
|
+
artifactFirstSourceContext,
|
|
2819
2851
|
mockContextBlock,
|
|
2820
2852
|
frontendPlanFieldGuide,
|
|
2821
2853
|
frontendComponentConformanceInstruction,
|
|
2822
|
-
|
|
2854
|
+
frontendPlanCitationInstruction,
|
|
2823
2855
|
].join("\n\n"),
|
|
2824
2856
|
},
|
|
2825
2857
|
{
|
|
@@ -2843,7 +2875,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2843
2875
|
"Component selection conformance is a hard blocking condition: VERDICT: request-revision when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; or when any uiComponentChoices specReference.path is not cited in the openspec-citations block or has no successful read event.",
|
|
2844
2876
|
"Read-only: do not modify repository files.",
|
|
2845
2877
|
fixedVerificationContext,
|
|
2846
|
-
|
|
2878
|
+
artifactFirstSourceContext,
|
|
2847
2879
|
frontendContractFieldSummary,
|
|
2848
2880
|
mockContextBlock,
|
|
2849
2881
|
].join("\n\n"),
|
|
@@ -2862,28 +2894,32 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2862
2894
|
structuredContractOutput: {
|
|
2863
2895
|
schemaId: "frontend-implementation-contract-revision-patch-v1",
|
|
2864
2896
|
retryOnInvalid: true,
|
|
2897
|
+
citationsRequired: frontendPlanCitationsRequired,
|
|
2865
2898
|
},
|
|
2866
2899
|
allowedPaths: readOnlyPaths,
|
|
2867
2900
|
forbiddenPaths,
|
|
2868
2901
|
skills: [],
|
|
2869
|
-
outputContract:
|
|
2902
|
+
outputContract: frontendRevisionOutputContract,
|
|
2870
2903
|
subtask_prompt: [
|
|
2871
|
-
"Consume frontend-plan-pi
|
|
2904
|
+
"Consume the hash-bound frontend-plan-pi contract artifact and frontend-design-review-pi findings. Read the artifact only for fields affected by a Required Plan Correction.",
|
|
2872
2905
|
"This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce an RFC 7386 merge-patch delta against the original contract JSON that addresses every Required Plan Correction from the design findings.",
|
|
2873
2906
|
"The patch delta may update editable contract fields such as requirements, implementationSteps, targets.routes/publicApiChanges, uiStates, interactions, mockApi.strategy/activation/endpoints, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, and realIntegrationGap. It must not modify protected schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff. Only include fields you change; omit unchanged fields (the gate applies the patch on the original contract). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
|
|
2874
|
-
|
|
2907
|
+
"Only include changed fields. For a changed requirement, preserve its expectedOutcome, implementationTargets, and verificationTargetIds; do not resend unchanged requirements.",
|
|
2875
2908
|
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
2876
2909
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
2877
|
-
|
|
2910
|
+
frontendPlanCitationsRequired
|
|
2911
|
+
? "Output exactly two adjacent blocks: (1) one fenced json object containing the merge-patch delta; (2) one openspec-citations citation block."
|
|
2912
|
+
: "JSON-only: output exactly one fenced json object containing the merge-patch delta; do not emit an openspec-citations block.",
|
|
2913
|
+
"Do NOT emit a full contract, Markdown explanation, prose, raw JSON or JSON objects outside the fenced payload, secrets, or unsafe paths.",
|
|
2878
2914
|
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
|
|
2879
2915
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2880
2916
|
verificationTargetFileInstruction,
|
|
2881
2917
|
fixedVerificationContext,
|
|
2882
|
-
|
|
2918
|
+
artifactFirstSourceContext,
|
|
2883
2919
|
frontendPlanFieldGuide,
|
|
2884
2920
|
mockContextBlock,
|
|
2885
2921
|
frontendComponentConformanceInstruction,
|
|
2886
|
-
|
|
2922
|
+
frontendPlanCitationInstruction,
|
|
2887
2923
|
].join("\n\n"),
|
|
2888
2924
|
},
|
|
2889
2925
|
{
|
|
@@ -2918,7 +2954,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2918
2954
|
"Also request revision for component selection non-conformance: spec-defined components silently replaced or self-invented without a declared deviation, uiComponentChoices missing for UI-visible work, or a uiComponentChoices specReference.path not cited in the openspec-citations block / not actually read.",
|
|
2919
2955
|
"Read-only: do not modify repository files.",
|
|
2920
2956
|
fixedVerificationContext,
|
|
2921
|
-
|
|
2957
|
+
artifactFirstSourceContext,
|
|
2922
2958
|
frontendContractFieldSummary,
|
|
2923
2959
|
mockContextBlock,
|
|
2924
2960
|
].join("\n\n"),
|
|
@@ -129,17 +129,24 @@ export function buildNodePrompt(spec, task, upstream, options) {
|
|
|
129
129
|
convergenceFeedback: options?.convergenceFeedback,
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
|
-
function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
132
|
+
function frontendStructuredArtifactRetryGuidance(schemaId, citationsRequired = true) {
|
|
133
|
+
const format = citationsRequired
|
|
134
|
+
? "one fenced json block immediately followed by one fenced openspec-citations block"
|
|
135
|
+
: "one fenced json block and no openspec-citations block";
|
|
133
136
|
if (schemaId === "frontend-implementation-contract-plan-patch-v1") {
|
|
134
137
|
return [
|
|
135
|
-
|
|
138
|
+
citationsRequired
|
|
139
|
+
? "Return exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by one fenced openspec-citations block, and nothing else."
|
|
140
|
+
: `Return exactly ${format} and nothing else; the json block contains only the editable RFC 7386 plan patch.`,
|
|
136
141
|
"The runtime applies your patch to its protected contract skeleton. Do not emit schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff; do not emit the full contract.",
|
|
137
142
|
"Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
138
143
|
];
|
|
139
144
|
}
|
|
140
145
|
if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
|
|
141
146
|
return [
|
|
142
|
-
|
|
147
|
+
citationsRequired
|
|
148
|
+
? "Return exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by one fenced openspec-citations block, and nothing else."
|
|
149
|
+
: `Return exactly ${format} and nothing else; the json block contains the RFC 7386 merge-patch delta.`,
|
|
143
150
|
"The delta contains only the fields you change; null deletes a key, arrays and scalars replace, and plain objects merge recursively. Do not emit a full contract or a schemaVersion/targets section.",
|
|
144
151
|
"Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
145
152
|
];
|
|
@@ -149,6 +156,27 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
|
149
156
|
"The contract JSON fields are the complete implementation plan. Do not emit a separate plan document, Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
150
157
|
];
|
|
151
158
|
}
|
|
159
|
+
function isFrontendPlanArtifactSchema(schemaId) {
|
|
160
|
+
return (schemaId === "frontend-implementation-contract-plan-patch-v1" ||
|
|
161
|
+
schemaId === "frontend-implementation-contract-revision-patch-v1");
|
|
162
|
+
}
|
|
163
|
+
function buildFrontendPlanArtifactRecoveryPrompt(options) {
|
|
164
|
+
const cause = options.failure === "truncated"
|
|
165
|
+
? "Previous response was truncated by the provider (stopReason=length)."
|
|
166
|
+
: options.failure === "too-large"
|
|
167
|
+
? "Previous response could not be retained by the provider."
|
|
168
|
+
: "Previous response failed structured validation.";
|
|
169
|
+
return [
|
|
170
|
+
"<retry_instruction>",
|
|
171
|
+
cause,
|
|
172
|
+
options.reason ? `Validation error: ${options.reason}` : "",
|
|
173
|
+
...frontendStructuredArtifactRetryGuidance(options.schemaId, options.citationsRequired),
|
|
174
|
+
"Use concise values and reference paths/IDs instead of copying source prose. Do not omit required semantics.",
|
|
175
|
+
"</retry_instruction>",
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join("\n");
|
|
179
|
+
}
|
|
152
180
|
function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
|
|
153
181
|
if (attemptNumber <= 1)
|
|
154
182
|
return basePrompt;
|
|
@@ -164,6 +192,14 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
164
192
|
if (previousFailureCategory === "invalid-output" &&
|
|
165
193
|
task.structuredContractOutput &&
|
|
166
194
|
previousProtocolReason) {
|
|
195
|
+
if (isFrontendPlanArtifactSchema(task.structuredContractOutput.schemaId)) {
|
|
196
|
+
return buildFrontendPlanArtifactRecoveryPrompt({
|
|
197
|
+
failure: "invalid",
|
|
198
|
+
reason: previousProtocolReason,
|
|
199
|
+
schemaId: task.structuredContractOutput.schemaId,
|
|
200
|
+
citationsRequired: task.structuredContractOutput.citationsRequired,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
167
203
|
return [
|
|
168
204
|
basePrompt,
|
|
169
205
|
"",
|
|
@@ -178,22 +214,13 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
178
214
|
if (previousFailureCategory === "structured-output-truncated" &&
|
|
179
215
|
task.structuredContractOutput) {
|
|
180
216
|
const schemaId = task.structuredContractOutput.schemaId;
|
|
181
|
-
if (schemaId
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"Previous response was truncated (stopReason=length).",
|
|
189
|
-
previousProtocolReason ? `Validation error: ${previousProtocolReason}` : "",
|
|
190
|
-
`Return one short lead-in, one complete fenced json ${payloadKind}, then one fenced openspec-citations block.`,
|
|
191
|
-
"Protected: schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff. Required semantics: requirement expectedOutcome; interaction trigger + expectedBehavior; frozen verification command labels.",
|
|
192
|
-
"No prose, examples, schema copy, Markdown headings, raw JSON, or other fences.",
|
|
193
|
-
"</retry_instruction>",
|
|
194
|
-
]
|
|
195
|
-
.filter(Boolean)
|
|
196
|
-
.join("\n");
|
|
217
|
+
if (isFrontendPlanArtifactSchema(schemaId)) {
|
|
218
|
+
return buildFrontendPlanArtifactRecoveryPrompt({
|
|
219
|
+
failure: "truncated",
|
|
220
|
+
reason: previousProtocolReason,
|
|
221
|
+
schemaId,
|
|
222
|
+
citationsRequired: task.structuredContractOutput.citationsRequired,
|
|
223
|
+
});
|
|
197
224
|
}
|
|
198
225
|
return [
|
|
199
226
|
basePrompt,
|
|
@@ -271,6 +298,13 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
271
298
|
}
|
|
272
299
|
if (task.outputMode === "structured-required") {
|
|
273
300
|
if (task.structuredContractOutput) {
|
|
301
|
+
if (isFrontendPlanArtifactSchema(task.structuredContractOutput.schemaId)) {
|
|
302
|
+
return buildFrontendPlanArtifactRecoveryPrompt({
|
|
303
|
+
failure: "too-large",
|
|
304
|
+
schemaId: task.structuredContractOutput.schemaId,
|
|
305
|
+
citationsRequired: task.structuredContractOutput.citationsRequired,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
274
308
|
return [
|
|
275
309
|
basePrompt,
|
|
276
310
|
"",
|
|
@@ -782,6 +782,10 @@ export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/
|
|
|
782
782
|
* this run-owned skeleton before strict contract validation.
|
|
783
783
|
*/
|
|
784
784
|
skeleton: z.record(z.string(), z.unknown()).optional(),
|
|
785
|
+
/** Frontend plan/revision citation fence is only needed for a
|
|
786
|
+
* generation-frozen OpenSpec candidate set. Undefined preserves the
|
|
787
|
+
* historical required-fence protocol. */
|
|
788
|
+
citationsRequired: z.boolean().optional(),
|
|
785
789
|
})
|
|
786
790
|
.strict()
|
|
787
791
|
.superRefine((value, ctx) => {
|