@tea-agent/loop-agent 0.34.5 → 0.35.0-beta.1
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("\\")))
|
|
@@ -191,6 +191,11 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
191
191
|
if (uncoveredTargets.length > 0) {
|
|
192
192
|
throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
|
|
193
193
|
}
|
|
194
|
+
const nonStaticVTs = contract.verificationTargets.filter((vt) => vt.type !== "static");
|
|
195
|
+
const uncoveredVTs = nonStaticVTs.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
|
|
196
|
+
if (uncoveredVTs.length > 0) {
|
|
197
|
+
throw new Error(`frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`);
|
|
198
|
+
}
|
|
194
199
|
}
|
|
195
200
|
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
196
201
|
const openspecReadPaths = await checkOpenspecReadEvidence({
|
|
@@ -2264,6 +2264,44 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2264
2264
|
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
2265
2265
|
"- 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
2266
|
"- 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.",
|
|
2267
|
+
"",
|
|
2268
|
+
"## Bad / Good contract field examples",
|
|
2269
|
+
"",
|
|
2270
|
+
"### verificationTargets - BAD (invented commandLabel, missing file):",
|
|
2271
|
+
'{"id":"vt-1","type":"static","commandLabel":"lint","file":"","requirementIds":["AC-001"],"uiStates":[]} <-- REJECTED: commandLabel not in frozen command set; empty file path',
|
|
2272
|
+
"",
|
|
2273
|
+
'### verificationTargets - GOOD (real frozen label, real file):',
|
|
2274
|
+
'{"id":"vt-1","type":"static","commandLabel":"npm run typecheck","file":"tsconfig.json","requirementIds":["AC-001"],"uiStates":[]} <-- Matches frozen command set; real file path',
|
|
2275
|
+
"",
|
|
2276
|
+
"### requirements - BAD (missing expectedOutcome):",
|
|
2277
|
+
'{"id":"AC-001","expectedOutcome":"","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]} <-- REJECTED: empty expectedOutcome',
|
|
2278
|
+
"",
|
|
2279
|
+
"### requirements - GOOD (concrete expectedOutcome):",
|
|
2280
|
+
'{"id":"AC-001","expectedOutcome":"TypeScript compilation exits with code 0 and produces no errors in dist/","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]}',
|
|
2281
|
+
"",
|
|
2282
|
+
"### interactions - BAD (empty trigger/expectedBehavior):",
|
|
2283
|
+
'{"name":"save-click","trigger":"","expectedBehavior":"","implementationTargets":["src/button.tsx"],"verificationTargetIds":["vt-3"]} <-- REJECTED: empty trigger and expectedBehavior',
|
|
2284
|
+
"",
|
|
2285
|
+
"### interactions - GOOD:",
|
|
2286
|
+
'{"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"]}',
|
|
2287
|
+
"",
|
|
2288
|
+
"### uiStates - BAD (applicable=true but missing expectedBehavior):",
|
|
2289
|
+
'{"name":"loading","applicable":true,"expectedBehavior":"","implementationTargets":[],"verificationTargetIds":[]} <-- REJECTED: applicable UI state requires non-empty expectedBehavior, implementationTargets, and verificationTargetIds',
|
|
2290
|
+
"",
|
|
2291
|
+
"### uiStates - GOOD (applicable=true with complete fields):",
|
|
2292
|
+
'{"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"]}',
|
|
2293
|
+
"",
|
|
2294
|
+
"### uiStates - BAD (applicable=false without notApplicableReason):",
|
|
2295
|
+
'{"name":"dark-mode","applicable":false} <-- REJECTED: non-applicable UI state requires notApplicableReason',
|
|
2296
|
+
"",
|
|
2297
|
+
"### uiStates - GOOD (applicable=false with reason):",
|
|
2298
|
+
'{"name":"dark-mode","applicable":false,"notApplicableReason":"Dark mode toggle is out of scope for this task; only light theme is targeted"}',
|
|
2299
|
+
"",
|
|
2300
|
+
"### mockApi.endpoints - BAD (strategy=native but empty endpoints):",
|
|
2301
|
+
'{"strategy":"native","productionDefaultOff":true,"activation":"env flag","endpoints":[]} <-- REJECTED: native strategy requires at least one endpoint with method, path, fixture, and consumer',
|
|
2302
|
+
"",
|
|
2303
|
+
"### mockApi.endpoints - GOOD (strategy=native with complete endpoint):",
|
|
2304
|
+
'{"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
2305
|
].join("\n");
|
|
2268
2306
|
})();
|
|
2269
2307
|
const sourceContext = [
|
|
@@ -2276,7 +2314,21 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2276
2314
|
mockCapability.verifyCommands.length > 0;
|
|
2277
2315
|
const requirementIds = frontendSourceBinding.requirementIds;
|
|
2278
2316
|
const requirementCoverageInstruction = requirementIds.length > 0
|
|
2279
|
-
?
|
|
2317
|
+
? [
|
|
2318
|
+
`## Requirement Coverage (per-AC echo with bad/good examples)`,
|
|
2319
|
+
`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).`,
|
|
2320
|
+
`Do not skip any ID. Use the bad/good patterns below as reference for each field.`,
|
|
2321
|
+
``,
|
|
2322
|
+
`Bad example (empty expectedOutcome, empty targets -- REJECTED at contract materialization):`,
|
|
2323
|
+
`- AC-001: expectedOutcome="" implementationTargets=[] verificationTargets=[]`,
|
|
2324
|
+
``,
|
|
2325
|
+
`Good example (concrete expectedOutcome, real files, real verification targets):`,
|
|
2326
|
+
`- 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"]`,
|
|
2327
|
+
``,
|
|
2328
|
+
...requirementIds.map((id) => `- ${id}: [expectedOutcome] [implementation files] [verification targets]`),
|
|
2329
|
+
``,
|
|
2330
|
+
`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.`,
|
|
2331
|
+
].join("\n")
|
|
2280
2332
|
: "";
|
|
2281
2333
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2282
2334
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
@@ -2469,7 +2521,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2469
2521
|
allowedPaths: readOnlyPaths,
|
|
2470
2522
|
forbiddenPaths,
|
|
2471
2523
|
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
|
|
2524
|
+
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 (\`\`\`json ... \`\`\`) conforming to frontend-implementation-contract-v1 when this node is the effective plan source. Do NOT include multiple fenced JSON blocks; only the single authoritative contract JSON block is accepted. No file writes.",
|
|
2473
2525
|
subtask_prompt: [
|
|
2474
2526
|
"Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
|
|
2475
2527
|
"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.",
|
|
@@ -2520,7 +2572,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2520
2572
|
allowedPaths: readOnlyPaths,
|
|
2521
2573
|
forbiddenPaths,
|
|
2522
2574
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2523
|
-
outputContract: "When the initial design review requests revision, return a complete Markdown revision plan followed by exactly
|
|
2575
|
+
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. Do NOT include multiple fenced JSON blocks; only the single authoritative contract JSON block is accepted. The JSON is the authoritative materialization input. No file writes.",
|
|
2524
2576
|
subtask_prompt: [
|
|
2525
2577
|
"Consume frontend-plan-pi (original plan) and frontend-design-review-pi (first design review findings).",
|
|
2526
2578
|
"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.",
|
|
@@ -2530,6 +2582,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2530
2582
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
2531
2583
|
"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.",
|
|
2532
2584
|
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the revised contract; do not reduce behavior semantics to IDs and paths.",
|
|
2585
|
+
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2586
|
+
fixedVerificationContext,
|
|
2533
2587
|
sourceContext,
|
|
2534
2588
|
frontendContractSchemaBlock,
|
|
2535
2589
|
].join("\n\n"),
|
|
@@ -2581,12 +2635,16 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2581
2635
|
allowedPaths: readOnlyPaths,
|
|
2582
2636
|
forbiddenPaths,
|
|
2583
2637
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2584
|
-
outputContract: "Return exactly one JSON object conforming to frontend-implementation-contract-v1.
|
|
2638
|
+
outputContract: "Return exactly one raw JSON object conforming to frontend-implementation-contract-v1. OUTPUT ONLY THE JSON OBJECT. NO MARKDOWN, NO PROSE, NO COMMENTS, NO CODE FENCES, NO BACKTICKS. The first character must be '{' and the last character must be '}'. Any text before or after the JSON will cause the output to be REJECTED.",
|
|
2585
2639
|
subtask_prompt: [
|
|
2586
2640
|
"Convert the effective reviewed frontend plan into the canonical frontend-implementation-contract-v1 JSON.",
|
|
2587
2641
|
"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
2642
|
"Return only the JSON object. Do not wrap it in Markdown or a code fence. Do not add explanatory text.",
|
|
2589
2643
|
"Preserve all requirement expectedOutcome, interaction trigger/expectedBehavior, target files, verification targets, Mock/API decisions, and Real Integration Gap from the effective plan.",
|
|
2644
|
+
"OUTPUT REQUIREMENT: The response must consist solely of a raw JSON object - no Markdown headers, no code fences (no \`\`\`json), no explanatory prose before or after. The very first character you output must be '{' and the very last character must be '}'. If you add ANY text, the extraction gate will reject the output.",
|
|
2645
|
+
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2646
|
+
requirementCoverageInstruction,
|
|
2647
|
+
fixedVerificationContext,
|
|
2590
2648
|
frontendContractSchemaBlock,
|
|
2591
2649
|
sourceContext,
|
|
2592
2650
|
].join("\n\n"),
|