@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.
@@ -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?.trim() || record.stdout?.trim() || "";
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 require Mock-backed strategy
603
- // assessment when a native project Mock capability is actually present.
604
- // Projects without confirmed Mock support continue through the normal
605
- // frontend writer and must preserve the real integration gap instead of
606
- // inventing a temporary Mock framework or blocking solely for missing Mock.
607
- return capability.status === "present" ? "required" : "not-required";
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: "Fail closed because required Mock verification entrypoints were not materialized. Resolve the task contract and regenerate the DAG.",
2046
+ subtask_prompt: `Fail closed: ${reason} Resolve the task contract and regenerate the DAG.`,
2043
2047
  shell: {
2044
2048
  commands: [
2045
- "node -e \"console.error('frontend Mock contract blocked: required verification entrypoints are unavailable'); process.exit(1)\"",
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 capabilityStatus = sources.frontendMockCapability?.status;
2106
- return ((sources.taskConfig.frontendMock?.policy ?? "auto") === "auto" &&
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" || capabilityStatus === "ambiguous"));
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
- ? `Include a Requirement Coverage section that lists every exact source identifier: ${requirementIds.join(", ")}. Preserve each identifier verbatim and map it to concrete implementation and verification steps.`
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
- return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints);
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, followed by exactly one fenced json object conforming to frontend-implementation-contract-v1 when this node is the effective plan source. No file writes.",
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 so small topology can materialize the contract without plan-revision.",
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 one fenced json object conforming to frontend-implementation-contract-v1. The JSON is the authoritative materialization input. No file writes.",
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-contract-json-pi",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.34.5",
3
+ "version": "0.35.0-beta.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -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, and exactly one `frontend-implementation-contract-v1` JSON object. 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. `auto` with absent/ambiguous project Mock capability must select `not-needed`, keep real requests default, and record the gap; `required` cannot select `not-needed`. 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 effective plan/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`. 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.
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)