@tea-agent/loop-agent 0.34.5 → 0.35.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/CHANGELOG.md +9 -0
- package/dist/worker/console/static/assets/index-fsjzREob.js +56 -0
- package/dist/worker/console/static/assets/{index-qpkysQYW.css → index-hJqCPs_g.css} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/static/operator-chrome.css +34 -0
- package/dist/worker/observe/static/operator-chrome.js +57 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +3 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +49 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +17 -1
- package/dist/workflows/dag/init-hybrid.js +90 -77
- package/dist/workflows/dag/types.js +2 -0
- package/package.json +1 -1
- package/skills/frontend-implementation/references/node-contracts.md +4 -2
- package/dist/worker/console/static/assets/index-y980PqtP.js +0 -56
|
@@ -8,6 +8,40 @@ import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-s
|
|
|
8
8
|
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
|
+
/**
|
|
12
|
+
* Extract frozen command labels from the DAG run spec (run.json).
|
|
13
|
+
* The run spec is written before any node executes, so it is always available
|
|
14
|
+
* when the prewrite gate materializes the contract.
|
|
15
|
+
*/
|
|
16
|
+
async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
17
|
+
const specPath = path.join(runDir, "run.json");
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = await readFile(specPath, "utf8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// run.json is guaranteed to exist in production (DAG runner writes it
|
|
24
|
+
// before any node executes). When absent (e.g. test fixtures), there is
|
|
25
|
+
// no frozen set to validate against and the check is skipped downstream.
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
let spec;
|
|
29
|
+
try {
|
|
30
|
+
spec = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(`cannot derive frozen command labels: run.json is not valid JSON at ${specPath}: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
const labels = new Set();
|
|
36
|
+
for (const task of spec.tasks ?? []) {
|
|
37
|
+
const cmdLabels = task.shell?.verifyEvidence?.commandLabels;
|
|
38
|
+
if (cmdLabels) {
|
|
39
|
+
for (const label of cmdLabels)
|
|
40
|
+
labels.add(label);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...labels];
|
|
44
|
+
}
|
|
11
45
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
12
46
|
/**
|
|
13
47
|
* Load the canonical frontend-implementation-contract-v1 JSON Schema from the
|
|
@@ -1228,6 +1262,21 @@ export async function materializeFrontendImplementationContract(input) {
|
|
|
1228
1262
|
parsed = canonicalizeRequirementIdsInPayload(parsed);
|
|
1229
1263
|
parsed = canonicalizeVerificationTargetAliases(parsed);
|
|
1230
1264
|
assertFrontendContractPathsSafe(parsed);
|
|
1265
|
+
// Validate verificationTarget commandLabels against frozen command set.
|
|
1266
|
+
// The frozen set is derived from DAG verification shell task verifyEvidence.
|
|
1267
|
+
const frozenLabels = await deriveFrozenCommandLabelsFromRun(input.runDir);
|
|
1268
|
+
if (frozenLabels.length > 0) {
|
|
1269
|
+
const frozen = new Set(frozenLabels);
|
|
1270
|
+
const parsedVt = asRecord(parsed)?.verificationTargets;
|
|
1271
|
+
if (Array.isArray(parsedVt)) {
|
|
1272
|
+
for (const vt of parsedVt) {
|
|
1273
|
+
const label = asString(asRecord(vt)?.commandLabel);
|
|
1274
|
+
if (label && !frozen.has(label)) {
|
|
1275
|
+
throw new Error(`invalid-output: verificationTarget commandLabel "${label}" is not in the frozen command set [${[...frozen].join(", ")}]`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1231
1280
|
const parsedTargets = asRecord(parsed)?.targets;
|
|
1232
1281
|
const parsedTargetFiles = asStringArray(asRecord(parsedTargets)?.files);
|
|
1233
1282
|
if (parsedTargetFiles.some((file) => file.startsWith("/") || file.includes("\\")))
|
|
@@ -30,7 +30,9 @@ async function readNodeText(runDir, nodeId) {
|
|
|
30
30
|
if (record.status !== "FINISHED") {
|
|
31
31
|
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
32
32
|
}
|
|
33
|
-
const text = record.assistantText
|
|
33
|
+
const text = [record.assistantText, record.stdout]
|
|
34
|
+
.filter((value) => Boolean(value?.trim()))
|
|
35
|
+
.join("\n");
|
|
34
36
|
if (!text)
|
|
35
37
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
36
38
|
return text;
|
|
@@ -184,6 +186,15 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
184
186
|
if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
|
|
185
187
|
throw new Error(`frontend prewrite gate blocked mock strategy ${contract.mockApi.strategy}; allowed=${input.config.allowedMockStrategies.join(",")}`);
|
|
186
188
|
}
|
|
189
|
+
// Fail early instead of at the verification trace: a non-not-needed strategy
|
|
190
|
+
// can only be proven by executing the DAG's frozen Mock verification
|
|
191
|
+
// commands. If none were materialized at generation time the contract would
|
|
192
|
+
// fail "trace: selected Mock strategy requires successful Mock verification
|
|
193
|
+
// commands" after implementation, so block before any write is authorized.
|
|
194
|
+
if (contract.mockApi.strategy !== "not-needed" &&
|
|
195
|
+
(input.config.mockCommandLabels?.length ?? 0) === 0) {
|
|
196
|
+
throw new Error(`frontend prewrite gate blocked: contract selected Mock strategy ${contract.mockApi.strategy} but the DAG has no authorized Mock verification commands; add frontendMock.verifyCommands or a project mock script, or select not-needed`);
|
|
197
|
+
}
|
|
187
198
|
if (input.config.implementationWriteSet) {
|
|
188
199
|
const writeSet = new Set(input.config.implementationWriteSet);
|
|
189
200
|
const contractTargets = new Set(contract.targets.files);
|
|
@@ -191,6 +202,11 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
191
202
|
if (uncoveredTargets.length > 0) {
|
|
192
203
|
throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
|
|
193
204
|
}
|
|
205
|
+
const nonStaticVTs = contract.verificationTargets.filter((vt) => vt.type !== "static");
|
|
206
|
+
const uncoveredVTs = nonStaticVTs.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
|
|
207
|
+
if (uncoveredVTs.length > 0) {
|
|
208
|
+
throw new Error(`frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`);
|
|
209
|
+
}
|
|
194
210
|
}
|
|
195
211
|
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
196
212
|
const openspecReadPaths = await checkOpenspecReadEvidence({
|
|
@@ -599,12 +599,15 @@ export function resolveFrontendMockMode(capability, taskConfig, hasApiDep) {
|
|
|
599
599
|
if (!hasApiDep) {
|
|
600
600
|
return "not-required";
|
|
601
601
|
}
|
|
602
|
-
// In auto mode, frontend Mock is optional. Only
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
|
|
602
|
+
// In auto mode, frontend Mock is optional. Only enter Mock-backed "required"
|
|
603
|
+
// mode when the project has a confirmed native Mock capability AND
|
|
604
|
+
// deterministic Mock verification commands. Without executable verification
|
|
605
|
+
// commands a non-not-needed strategy could never be verified, so auto falls
|
|
606
|
+
// back to not-required; the prewrite gate then forces not-needed and the
|
|
607
|
+
// plan records the Real Integration Gap instead of inventing commands.
|
|
608
|
+
return capability.status === "present" && hasDeterministicMockVerification
|
|
609
|
+
? "required"
|
|
610
|
+
: "not-required";
|
|
608
611
|
}
|
|
609
612
|
function mapTaskComplexity(complexity) {
|
|
610
613
|
if (complexity === "small")
|
|
@@ -2008,8 +2011,9 @@ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbid
|
|
|
2008
2011
|
},
|
|
2009
2012
|
};
|
|
2010
2013
|
}
|
|
2011
|
-
function buildBlockedFrontendMockDag(sources, readOnlyPaths, forbiddenPaths, globalConstraints) {
|
|
2014
|
+
function buildBlockedFrontendMockDag(sources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason) {
|
|
2012
2015
|
const { taskConfig } = sources;
|
|
2016
|
+
const reason = blockedReason ?? "Mock contract is blocked by deterministic generation-time Mock safety constraints.";
|
|
2013
2017
|
const spec = {
|
|
2014
2018
|
version: 3,
|
|
2015
2019
|
title: `Frontend implementation DAG (BLOCKED Mock): ${taskConfig.title}`,
|
|
@@ -2039,10 +2043,10 @@ function buildBlockedFrontendMockDag(sources, readOnlyPaths, forbiddenPaths, glo
|
|
|
2039
2043
|
allowedPaths: readOnlyPaths,
|
|
2040
2044
|
forbiddenPaths,
|
|
2041
2045
|
outputContract: "Deterministic generation-time Mock blocker. Always exits nonzero and never reaches a writer.",
|
|
2042
|
-
subtask_prompt:
|
|
2046
|
+
subtask_prompt: `Fail closed: ${reason} Resolve the task contract and regenerate the DAG.`,
|
|
2043
2047
|
shell: {
|
|
2044
2048
|
commands: [
|
|
2045
|
-
|
|
2049
|
+
`node -e ${JSON.stringify(`console.error(${JSON.stringify(`frontend Mock contract blocked: ${reason}`)}); process.exit(1)`)}`,
|
|
2046
2050
|
],
|
|
2047
2051
|
cwd: ".",
|
|
2048
2052
|
timeoutMs: 60000,
|
|
@@ -2093,7 +2097,7 @@ function resolveFrontendMockContextBlock(sources) {
|
|
|
2093
2097
|
if (mode === "not-required") {
|
|
2094
2098
|
parts.push("Generation-time evidence does not require Mock. The assessment must still use contract/scout evidence: select not-needed when Mock is intentionally skipped, or select a safe Mock strategy if project evidence supports one.");
|
|
2095
2099
|
if (frontendMockStrategyMustBeNotNeeded(sources)) {
|
|
2096
|
-
parts.push('Auto mode has no confirmed project Mock capability. The structured contract must set mockApi.strategy to "not-needed". Do not add Mock files or dependencies; keep the real request path as default and record any unproved backend behavior as Real Integration Gap.');
|
|
2100
|
+
parts.push('Auto mode has no confirmed project Mock capability or no deterministic Mock verification command. The structured contract must set mockApi.strategy to "not-needed". Do not add Mock files or dependencies; keep the real request path as default and record any unproved backend behavior as Real Integration Gap.');
|
|
2097
2101
|
}
|
|
2098
2102
|
}
|
|
2099
2103
|
if (mode === "blocked") {
|
|
@@ -2102,10 +2106,15 @@ function resolveFrontendMockContextBlock(sources) {
|
|
|
2102
2106
|
return parts.join("\n");
|
|
2103
2107
|
}
|
|
2104
2108
|
function frontendMockStrategyMustBeNotNeeded(sources) {
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2109
|
+
const policy = sources.taskConfig.frontendMock?.policy ?? "auto";
|
|
2110
|
+
const capability = sources.frontendMockCapability;
|
|
2111
|
+
const capabilityStatus = capability?.status;
|
|
2112
|
+
const hasDeterministicMockVerification = (capability?.verifyCommands.length ?? 0) > 0;
|
|
2113
|
+
return (policy === "auto" &&
|
|
2107
2114
|
(sources.frontendMockMode ?? "not-required") === "not-required" &&
|
|
2108
|
-
(capabilityStatus === "absent" ||
|
|
2115
|
+
(capabilityStatus === "absent" ||
|
|
2116
|
+
capabilityStatus === "ambiguous" ||
|
|
2117
|
+
!hasDeterministicMockVerification));
|
|
2109
2118
|
}
|
|
2110
2119
|
function resolveFrontendCapabilityContextBlock(sources) {
|
|
2111
2120
|
const risk = sources.frontendRisk;
|
|
@@ -2264,6 +2273,44 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2264
2273
|
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
2265
2274
|
"- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
|
|
2266
2275
|
"- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
|
|
2276
|
+
"",
|
|
2277
|
+
"## Bad / Good contract field examples",
|
|
2278
|
+
"",
|
|
2279
|
+
"### verificationTargets - BAD (invented commandLabel, missing file):",
|
|
2280
|
+
'{"id":"vt-1","type":"static","commandLabel":"lint","file":"","requirementIds":["AC-001"],"uiStates":[]} <-- REJECTED: commandLabel not in frozen command set; empty file path',
|
|
2281
|
+
"",
|
|
2282
|
+
'### verificationTargets - GOOD (real frozen label, real file):',
|
|
2283
|
+
'{"id":"vt-1","type":"static","commandLabel":"npm run typecheck","file":"tsconfig.json","requirementIds":["AC-001"],"uiStates":[]} <-- Matches frozen command set; real file path',
|
|
2284
|
+
"",
|
|
2285
|
+
"### requirements - BAD (missing expectedOutcome):",
|
|
2286
|
+
'{"id":"AC-001","expectedOutcome":"","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]} <-- REJECTED: empty expectedOutcome',
|
|
2287
|
+
"",
|
|
2288
|
+
"### requirements - GOOD (concrete expectedOutcome):",
|
|
2289
|
+
'{"id":"AC-001","expectedOutcome":"TypeScript compilation exits with code 0 and produces no errors in dist/","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]}',
|
|
2290
|
+
"",
|
|
2291
|
+
"### interactions - BAD (empty trigger/expectedBehavior):",
|
|
2292
|
+
'{"name":"save-click","trigger":"","expectedBehavior":"","implementationTargets":["src/button.tsx"],"verificationTargetIds":["vt-3"]} <-- REJECTED: empty trigger and expectedBehavior',
|
|
2293
|
+
"",
|
|
2294
|
+
"### interactions - GOOD:",
|
|
2295
|
+
'{"name":"save-click","trigger":"User clicks the Save button in the editor toolbar","expectedBehavior":"POST /api/save is called with editor content; success toast appears; button enters disabled+spinner state until response","implementationTargets":["src/editor/save-button.tsx"],"verificationTargetIds":["vt-3"]}',
|
|
2296
|
+
"",
|
|
2297
|
+
"### uiStates - BAD (applicable=true but missing expectedBehavior):",
|
|
2298
|
+
'{"name":"loading","applicable":true,"expectedBehavior":"","implementationTargets":[],"verificationTargetIds":[]} <-- REJECTED: applicable UI state requires non-empty expectedBehavior, implementationTargets, and verificationTargetIds',
|
|
2299
|
+
"",
|
|
2300
|
+
"### uiStates - GOOD (applicable=true with complete fields):",
|
|
2301
|
+
'{"name":"loading","applicable":true,"expectedBehavior":"Skeleton placeholder visible while data fetches; aria-busy=true on the list container","implementationTargets":["src/dashboard/list-view.tsx"],"verificationTargetIds":["vt-3"]}',
|
|
2302
|
+
"",
|
|
2303
|
+
"### uiStates - BAD (applicable=false without notApplicableReason):",
|
|
2304
|
+
'{"name":"dark-mode","applicable":false} <-- REJECTED: non-applicable UI state requires notApplicableReason',
|
|
2305
|
+
"",
|
|
2306
|
+
"### uiStates - GOOD (applicable=false with reason):",
|
|
2307
|
+
'{"name":"dark-mode","applicable":false,"notApplicableReason":"Dark mode toggle is out of scope for this task; only light theme is targeted"}',
|
|
2308
|
+
"",
|
|
2309
|
+
"### mockApi.endpoints - BAD (strategy=native but empty endpoints):",
|
|
2310
|
+
'{"strategy":"native","productionDefaultOff":true,"activation":"env flag","endpoints":[]} <-- REJECTED: native strategy requires at least one endpoint with method, path, fixture, and consumer',
|
|
2311
|
+
"",
|
|
2312
|
+
"### mockApi.endpoints - GOOD (strategy=native with complete endpoint):",
|
|
2313
|
+
'{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
|
|
2267
2314
|
].join("\n");
|
|
2268
2315
|
})();
|
|
2269
2316
|
const sourceContext = [
|
|
@@ -2276,7 +2323,21 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2276
2323
|
mockCapability.verifyCommands.length > 0;
|
|
2277
2324
|
const requirementIds = frontendSourceBinding.requirementIds;
|
|
2278
2325
|
const requirementCoverageInstruction = requirementIds.length > 0
|
|
2279
|
-
?
|
|
2326
|
+
? [
|
|
2327
|
+
`## Requirement Coverage (per-AC echo with bad/good examples)`,
|
|
2328
|
+
`For each requirement ID below, echo the ID verbatim and confirm: expectedOutcome (user-observable or logic-observable), implementation targets (files), and verification targets (commandLabel + file).`,
|
|
2329
|
+
`Do not skip any ID. Use the bad/good patterns below as reference for each field.`,
|
|
2330
|
+
``,
|
|
2331
|
+
`Bad example (empty expectedOutcome, empty targets -- REJECTED at contract materialization):`,
|
|
2332
|
+
`- AC-001: expectedOutcome="" implementationTargets=[] verificationTargets=[]`,
|
|
2333
|
+
``,
|
|
2334
|
+
`Good example (concrete expectedOutcome, real files, real verification targets):`,
|
|
2335
|
+
`- 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"]`,
|
|
2336
|
+
``,
|
|
2337
|
+
...requirementIds.map((id) => `- ${id}: [expectedOutcome] [implementation files] [verification targets]`),
|
|
2338
|
+
``,
|
|
2339
|
+
`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.`,
|
|
2340
|
+
].join("\n")
|
|
2280
2341
|
: "";
|
|
2281
2342
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2282
2343
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
@@ -2303,7 +2364,12 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2303
2364
|
];
|
|
2304
2365
|
// Guard: blocked mode — generate assessment-only DAG with no writer reachable
|
|
2305
2366
|
if (mockMode === "blocked") {
|
|
2306
|
-
|
|
2367
|
+
const blockedReason = mockCapability.safetyViolation
|
|
2368
|
+
? `Mock contract blocked: ${mockCapability.safetyViolation}`
|
|
2369
|
+
: (taskConfig.frontendMock?.policy ?? "auto") === "required"
|
|
2370
|
+
? "Mock strategy is required, but no authorized Mock verification command was found."
|
|
2371
|
+
: "Mock contract is blocked by deterministic generation-time Mock safety constraints.";
|
|
2372
|
+
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason);
|
|
2307
2373
|
}
|
|
2308
2374
|
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
2309
2375
|
const staticFallbackCommands = fallbackVerifyCommands.staticCommands;
|
|
@@ -2469,14 +2535,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2469
2535
|
allowedPaths: readOnlyPaths,
|
|
2470
2536
|
forbiddenPaths,
|
|
2471
2537
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2472
|
-
outputContract: "Markdown implementation plan with Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks,
|
|
2538
|
+
outputContract: "Markdown implementation plan with Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks, ending with exactly ONE fenced json object (\`\`\`json ... \`\`\`) conforming to frontend-implementation-contract-v1. This fenced block is the single authoritative implementation contract the prewrite gate materializes; it must appear exactly once and must not contain or be followed by any raw JSON or extra fenced block. No file writes.",
|
|
2473
2539
|
subtask_prompt: [
|
|
2474
2540
|
"Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
|
|
2475
2541
|
"Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
|
|
2476
2542
|
"Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2477
2543
|
"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.",
|
|
2478
2544
|
"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.",
|
|
2479
|
-
"End with exactly one fenced json object conforming to frontend-implementation-contract-v1
|
|
2545
|
+
"End with exactly one fenced json object conforming to frontend-implementation-contract-v1. This node is the single contract JSON producer: the fenced block is the authoritative contract the prewrite gate materializes. Do not emit any raw JSON, JSON in prose, or a second fenced block anywhere in the response; the plan text must not contain other balanced JSON objects.",
|
|
2480
2546
|
"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.",
|
|
2481
2547
|
requirementCoverageInstruction,
|
|
2482
2548
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
@@ -2520,7 +2586,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2520
2586
|
allowedPaths: readOnlyPaths,
|
|
2521
2587
|
forbiddenPaths,
|
|
2522
2588
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2523
|
-
outputContract: "When the initial design review requests revision, return a complete Markdown revision plan followed by exactly
|
|
2589
|
+
outputContract: "When the initial design review requests revision, return a complete Markdown revision plan followed by exactly ONE fenced json object (\`\`\`json ... \`\`\`) conforming to frontend-implementation-contract-v1. This fenced block is the single authoritative implementation contract the prewrite gate materializes; it must appear exactly once and must not contain or be followed by any raw JSON or extra fenced block. Do NOT include multiple fenced JSON blocks; only the single authoritative contract JSON block is accepted. No file writes.",
|
|
2524
2590
|
subtask_prompt: [
|
|
2525
2591
|
"Consume frontend-plan-pi (original plan) and frontend-design-review-pi (first design review findings).",
|
|
2526
2592
|
"This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce a complete revised implementation plan that addresses every Required Plan Correction from the design findings.",
|
|
@@ -2528,8 +2594,10 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2528
2594
|
requirementCoverageInstruction,
|
|
2529
2595
|
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
2530
2596
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
2531
|
-
"End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
|
|
2597
|
+
"End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. This node is the single contract JSON producer when the design review requests revision: the fenced block is the authoritative contract the prewrite gate materializes. Do not emit any raw JSON, JSON in prose, or a second fenced block anywhere in the response. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
|
|
2532
2598
|
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the revised contract; do not reduce behavior semantics to IDs and paths.",
|
|
2599
|
+
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2600
|
+
fixedVerificationContext,
|
|
2533
2601
|
sourceContext,
|
|
2534
2602
|
frontendContractSchemaBlock,
|
|
2535
2603
|
].join("\n\n"),
|
|
@@ -2563,62 +2631,9 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2563
2631
|
sourceContext,
|
|
2564
2632
|
].join("\n\n"),
|
|
2565
2633
|
},
|
|
2566
|
-
{
|
|
2567
|
-
id: "frontend-contract-json-pi",
|
|
2568
|
-
depends_on: [
|
|
2569
|
-
"frontend-plan-revision-pi",
|
|
2570
|
-
"frontend-plan-pi",
|
|
2571
|
-
"frontend-final-design-review-pi",
|
|
2572
|
-
"frontend-design-review-pi",
|
|
2573
|
-
],
|
|
2574
|
-
dependsPolicy: "all-or-condition-skip",
|
|
2575
|
-
role: "planner",
|
|
2576
|
-
executor: "pi",
|
|
2577
|
-
complexity: "MED",
|
|
2578
|
-
writePolicy: "read-only",
|
|
2579
|
-
outputMode: "structured-required",
|
|
2580
|
-
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2581
|
-
allowedPaths: readOnlyPaths,
|
|
2582
|
-
forbiddenPaths,
|
|
2583
|
-
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2584
|
-
outputContract: "Return exactly one JSON object conforming to frontend-implementation-contract-v1. No Markdown, prose, comments, or code fences.",
|
|
2585
|
-
subtask_prompt: [
|
|
2586
|
-
"Convert the effective reviewed frontend plan into the canonical frontend-implementation-contract-v1 JSON.",
|
|
2587
|
-
"Use frontend-plan-revision-pi when it is FINISHED; otherwise use frontend-plan-pi. Confirm the effective design review passed before producing the contract.",
|
|
2588
|
-
"Return only the JSON object. Do not wrap it in Markdown or a code fence. Do not add explanatory text.",
|
|
2589
|
-
"Preserve all requirement expectedOutcome, interaction trigger/expectedBehavior, target files, verification targets, Mock/API decisions, and Real Integration Gap from the effective plan.",
|
|
2590
|
-
frontendContractSchemaBlock,
|
|
2591
|
-
sourceContext,
|
|
2592
|
-
].join("\n\n"),
|
|
2593
|
-
},
|
|
2594
|
-
{
|
|
2595
|
-
id: "frontend-contract-json-validate-shell",
|
|
2596
|
-
depends_on: ["frontend-contract-json-pi"],
|
|
2597
|
-
role: "verifier",
|
|
2598
|
-
executor: "shell",
|
|
2599
|
-
complexity: "LOW",
|
|
2600
|
-
writePolicy: "read-only",
|
|
2601
|
-
allowedPaths: readOnlyPaths,
|
|
2602
|
-
forbiddenPaths,
|
|
2603
|
-
outputContract: "Validated frontend implementation contract artifact with schema ID and SHA-256.",
|
|
2604
|
-
subtask_prompt: "Materialize and validate the structured frontend contract before prewrite authorization.",
|
|
2605
|
-
shell: {
|
|
2606
|
-
commands: [],
|
|
2607
|
-
jsonArtifactGate: {
|
|
2608
|
-
fromNodeId: "frontend-contract-json-pi",
|
|
2609
|
-
schemaId: "frontend-implementation-contract-v1",
|
|
2610
|
-
artifactName: "frontend-implementation-contract.json",
|
|
2611
|
-
outputDir: "contracts",
|
|
2612
|
-
},
|
|
2613
|
-
cwd: ".",
|
|
2614
|
-
timeoutMs: 60000,
|
|
2615
|
-
},
|
|
2616
|
-
},
|
|
2617
2634
|
{
|
|
2618
2635
|
id: "frontend-prewrite-gate-shell",
|
|
2619
2636
|
depends_on: [
|
|
2620
|
-
"frontend-contract-json-pi",
|
|
2621
|
-
"frontend-contract-json-validate-shell",
|
|
2622
2637
|
"frontend-final-design-review-pi",
|
|
2623
2638
|
"frontend-design-review-pi",
|
|
2624
2639
|
"frontend-plan-revision-pi",
|
|
@@ -2637,14 +2652,12 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2637
2652
|
commands: [],
|
|
2638
2653
|
frontendPrewriteGate: {
|
|
2639
2654
|
schemaVersion: 1,
|
|
2640
|
-
planFromNodeId: "frontend-
|
|
2641
|
-
planFallbackFromNodeIds: [
|
|
2642
|
-
"frontend-plan-revision-pi",
|
|
2643
|
-
"frontend-plan-pi",
|
|
2644
|
-
],
|
|
2655
|
+
planFromNodeId: "frontend-plan-revision-pi",
|
|
2656
|
+
planFallbackFromNodeIds: ["frontend-plan-pi"],
|
|
2645
2657
|
reviewFromNodeId: "frontend-final-design-review-pi",
|
|
2646
2658
|
reviewFallbackFromNodeIds: ["frontend-design-review-pi"],
|
|
2647
2659
|
requiredRequirementIds: requirementIds,
|
|
2660
|
+
mockCommandLabels: mockVerifyEvidence?.commandLabels ?? [],
|
|
2648
2661
|
allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
|
|
2649
2662
|
frontendMockStrategyMustBeNotNeeded(frontendSources)
|
|
2650
2663
|
? ["not-needed"]
|
|
@@ -183,6 +183,8 @@ export const dagFrontendPrewriteGateSchema = z.object({
|
|
|
183
183
|
allowedMockStrategies: z
|
|
184
184
|
.array(z.enum(["native", "browser-intercept", "request-adapter", "not-needed"]))
|
|
185
185
|
.min(1),
|
|
186
|
+
/** Frozen Mock verification command labels the DAG will actually execute. */
|
|
187
|
+
mockCommandLabels: z.array(z.string()).default([]),
|
|
186
188
|
artifactName: z.string().regex(/^[a-z0-9][a-z0-9._-]*\.json$/),
|
|
187
189
|
outputDir: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
|
|
188
190
|
requireSourceFreshness: z.literal(true),
|
package/package.json
CHANGED
|
@@ -6,8 +6,10 @@ Pre-write nodes are read-only. Preserve IDs, labels, commands, language, require
|
|
|
6
6
|
|
|
7
7
|
- **`frontend-contract-pi`**: `Scope`, `Non-goals`, `Acceptance Criteria`, `UI States`, `Target Runtime Environment`, `Risks`, `Verification Expectations`. No guessed requirements.
|
|
8
8
|
- **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets. Fact vs inference vs gap. Query the knowledge base when available and always search+read `openspec/schemas/`, `openspec/project-specs/`, and `ai_workspace/` before repo fallback. Output stack, routes, components, styling, conventions, state/data, test entry points, reuse, risks.
|
|
9
|
-
- **`frontend-plan-pi` + conditional design loop**: AC → explicit observable `expectedOutcome`, interactions → explicit `trigger` + `expectedBehavior`, then steps, in-bound files, applicable UI states, reuse, deps, Mock/API strategy, activation/rollback, frozen verify entrypoints, real-integration gap
|
|
10
|
-
- **`frontend-prewrite-gate-shell`**: the sole write authorization. Resolve effective plan
|
|
9
|
+
- **`frontend-plan-pi` + conditional design loop**: AC → explicit observable `expectedOutcome`, interactions → explicit `trigger` + `expectedBehavior`, then steps, in-bound files, applicable UI states, reuse, deps, Mock/API strategy, activation/rollback, frozen verify entrypoints, and real-integration gap. These nodes output Markdown plans only; each plan ends with **exactly one** fenced `json` block carrying the single authoritative `frontend-implementation-contract-v1` object. Do not emit raw JSON, JSON in prose, or a second fenced block. `frontend-contract-json-pi` and `frontend-contract-json-validate-shell` do not exist; the prewrite gate materializes the contract from the effective plan node. IDs plus file paths are not sufficient behavior semantics. Use `uiStates: []` for logic-only changes with no user-visible UI state; do not invent UI states. Applicable states require behavior/implementation/verification, while non-applicable states require a reason and omit empty behavior placeholders. Prefer native Mock; browser intercept only with existing e2e; request-adapter only for a reversible seam. A non-`not-needed` strategy requires frozen Mock verify commands (`frontendMock.verifyCommands` / `package.json` mock script / capability seed); `auto` with absent/ambiguous capability or no command selects `not-needed` (real requests stay default, gap recorded); `required` without a command is generation-time blocked (no writer is generated). Initial design pass uses the original plan; only exact `request-revision` runs read-only revision plus final review. Small-risk runs one design review only.
|
|
10
|
+
- **`frontend-prewrite-gate-shell`**: the sole write authorization. Resolve the effective plan (revised plan when the revision branch ran, otherwise the original plan) and its review, require exact pass, retain every REQ/BR/AC id, enforce Mock policy, validate schema/source binding and writeSet containment, and materialize `contracts/frontend-implementation-contract.json` from the **single** fenced contract block in the effective plan node (multiple candidates fail closed with `invalid-output`). A planned fixture or consumer may be a future writer output and need not exist before authorization. Fallback is allowed only when a conditional primary is absent; an existing malformed primary fails closed. Generation-time blocked Mock produces one deterministic blocking shell node and no writer.
|
|
11
|
+
- Every `verificationTarget.commandLabel` must be one of the **frozen command labels** derived from the DAG run spec's `verifyEvidence.commandLabels`; any other value is rejected fail-closed at materialization (`invalid-output`). An empty frozen set (e.g. no `run.json`) skips the check.
|
|
12
|
+
- `mockApi.strategy !== "not-needed"` fails closed at the gate when `mockCommandLabels` is empty (`no authorized Mock verification commands`).
|
|
11
13
|
- **`frontend-implement-pi`**: sole regular exclusive writer and consumer of `frontend-bounded-implement`, not this discovery skill. Stay in `writeSet`; real requests default-on; Mock reversible, dev/test-only, production-off. Atomic handler/intercept/adapter with consumer+tests. Stop on forbidden paths or guesses. First line must be `IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked`; runtime checks it against the attributed diff. Optional mock-verify when frozen; static+behavior always; behavior must prove page consumption. Skipped-Mock `not-needed` keeps real integration pending unless the real backend path has fresh evidence.
|
|
12
14
|
|
|
13
15
|
## Contract / trace / stages (M1–M2)
|