@tea-agent/loop-agent 0.28.0 → 0.28.1-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.
- package/dist/executors/dag-pi-executor.js +18 -22
- package/dist/task/task-demand-routing.js +1 -15
- package/dist/workflows/dag/frontend-implementation-contract.js +124 -6
- package/dist/workflows/dag/frontend-prewrite-gate.js +22 -8
- package/dist/workflows/dag/init-hybrid.js +65 -38
- package/dist/workflows/dag/output-protocol.js +23 -0
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/package.json +1 -1
|
@@ -681,32 +681,19 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
681
681
|
export function validateWriterImplementationOutcome(text, changedFiles) {
|
|
682
682
|
const parsed = parseWriterImplementationOutcome(text);
|
|
683
683
|
const diagnostics = writerOutcomeDiagnostics(text, parsed, changedFiles.length);
|
|
684
|
-
|
|
684
|
+
// The workspace diff is authoritative. The model outcome is advisory and
|
|
685
|
+
// may be missing, malformed, or inconsistent without blocking a completed
|
|
686
|
+
// writer. An explicit blocked signal remains a hard failure.
|
|
687
|
+
if (parsed.candidates.some((candidate) => candidate.outcome === "blocked")) {
|
|
685
688
|
return {
|
|
686
689
|
ok: false,
|
|
687
|
-
reason: `writer outcome validation failed:
|
|
690
|
+
reason: `writer outcome validation failed: explicit blocked outcome; ${diagnostics}`,
|
|
688
691
|
};
|
|
689
692
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
reason: `writer outcome validation failed: IMPLEMENTATION_OUTCOME: blocked cannot complete successfully; ${diagnostics}`,
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
if (outcome === "changed" && changedFiles.length === 0) {
|
|
698
|
-
return {
|
|
699
|
-
ok: false,
|
|
700
|
-
reason: `writer outcome validation failed: changed outcome has an empty diff; ${diagnostics}`,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
if (outcome === "already-satisfied" && changedFiles.length > 0) {
|
|
704
|
-
return {
|
|
705
|
-
ok: false,
|
|
706
|
-
reason: `writer outcome validation failed: already-satisfied outcome has a non-empty diff; ${diagnostics}`,
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
return { ok: true, outcome };
|
|
693
|
+
return {
|
|
694
|
+
ok: true,
|
|
695
|
+
outcome: changedFiles.length > 0 ? "changed" : "already-satisfied",
|
|
696
|
+
};
|
|
710
697
|
}
|
|
711
698
|
function parseWriterImplementationOutcome(text) {
|
|
712
699
|
const lines = text.split(/\r?\n/);
|
|
@@ -837,6 +824,15 @@ function normalizeProtocolLine(line, firstProtocolLine, nextLine) {
|
|
|
837
824
|
const value = normalizedProtocolValue(direct[1] ?? "");
|
|
838
825
|
return value ? `${firstProtocolLine} ${value}` : undefined;
|
|
839
826
|
}
|
|
827
|
+
// Writers sometimes place a short delivery sentence before the protocol
|
|
828
|
+
// line. Accept the protocol token when it appears inline, while preserving
|
|
829
|
+
// the candidate value so template repetitions still fail as unknown or
|
|
830
|
+
// conflicting outcomes.
|
|
831
|
+
const inline = normalizedLine.match(new RegExp(`${labelPattern}\\s*[::]\\s*(.+)$`, "i"));
|
|
832
|
+
if (inline) {
|
|
833
|
+
const value = normalizedProtocolValue(inline[1] ?? "");
|
|
834
|
+
return value ? `${firstProtocolLine} ${value}` : undefined;
|
|
835
|
+
}
|
|
840
836
|
const splitValue = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]?\\s*$`, "i"));
|
|
841
837
|
if (splitValue && nextLine !== undefined) {
|
|
842
838
|
const value = normalizedProtocolValue(nextLine);
|
|
@@ -334,24 +334,10 @@ export function classifyTaskDemand(input) {
|
|
|
334
334
|
});
|
|
335
335
|
}
|
|
336
336
|
const backendDelivery = titleSignals.backendDelivery || requirementSignals.backendDelivery;
|
|
337
|
-
const frontendProjectDefaultImplementation = hasStrongFrontendProjectEvidence &&
|
|
338
|
-
frontendPath &&
|
|
339
|
-
!hasBackendTaskType &&
|
|
340
|
-
!backendDelivery &&
|
|
341
|
-
!frontendNegated &&
|
|
342
|
-
!allowedPathsOnlyCoverNonProductArtifacts;
|
|
343
|
-
if (frontendProjectDefaultImplementation) {
|
|
344
|
-
addSignal(signals, {
|
|
345
|
-
id: "frontend-project-default-implementation",
|
|
346
|
-
source: "project",
|
|
347
|
-
message: "frontend project evidence selects the frontend implementation workflow by default",
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
337
|
const frontendDelivery = titleSignals.frontendDelivery ||
|
|
351
338
|
requirementSignals.frontendDelivery ||
|
|
352
339
|
pathSupportedFrontendDelivery ||
|
|
353
|
-
projectSupportedFrontendDelivery
|
|
354
|
-
frontendProjectDefaultImplementation;
|
|
340
|
+
projectSupportedFrontendDelivery;
|
|
355
341
|
if (allowedPathsOnlyCoverNonProductArtifacts && frontendDelivery) {
|
|
356
342
|
blockers.push("non-product-allowed-paths");
|
|
357
343
|
}
|
|
@@ -155,7 +155,7 @@ export const frontendImplementationContractSchema = z
|
|
|
155
155
|
"not-needed",
|
|
156
156
|
]),
|
|
157
157
|
productionDefaultOff: z.literal(true),
|
|
158
|
-
activation: z.string().min(1),
|
|
158
|
+
activation: z.preprocess((value) => (value === "" || value === null ? "explicit activation boundary" : value), z.string().min(1)),
|
|
159
159
|
endpoints: z.array(z
|
|
160
160
|
.object({
|
|
161
161
|
method: z.enum([
|
|
@@ -168,7 +168,10 @@ export const frontendImplementationContractSchema = z
|
|
|
168
168
|
"OPTIONS",
|
|
169
169
|
]),
|
|
170
170
|
path: z.string().startsWith("/"),
|
|
171
|
-
fixture
|
|
171
|
+
// Models occasionally emit an empty fixture when Mock is
|
|
172
|
+
// intentionally not needed. Treat it like an omitted optional
|
|
173
|
+
// field; active Mock strategies still fail the refinement below.
|
|
174
|
+
fixture: z.preprocess((value) => (value === "" || value === null ? undefined : value), safePath.optional()),
|
|
172
175
|
consumer: safePath.optional(),
|
|
173
176
|
})
|
|
174
177
|
.strict()),
|
|
@@ -187,7 +190,7 @@ export const frontendImplementationContractSchema = z
|
|
|
187
190
|
type: z.enum(["static", "unit", "component", "integration", "mock"]),
|
|
188
191
|
commandLabel: z.string().min(1),
|
|
189
192
|
file: safePath,
|
|
190
|
-
symbol: z.string().min(1).optional(),
|
|
193
|
+
symbol: z.preprocess((value) => (value === "" || value === null ? undefined : value), z.string().min(1).optional()),
|
|
191
194
|
requirementIds: z.array(id),
|
|
192
195
|
uiStates: z.array(z.string().min(1)),
|
|
193
196
|
})
|
|
@@ -343,12 +346,127 @@ function secretIssues(value, at = "$", issues = []) {
|
|
|
343
346
|
}
|
|
344
347
|
export function extractFrontendImplementationJson(text) {
|
|
345
348
|
const trimmed = text.trim();
|
|
349
|
+
const parse = (source) => {
|
|
350
|
+
try {
|
|
351
|
+
return JSON.parse(source);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
// Models sometimes put ordinary ASCII quotes inside a JSON string
|
|
355
|
+
// (for example: `reason: "支持..."`). Repair only quotes that are
|
|
356
|
+
// clearly not structural: a closing quote is followed by JSON
|
|
357
|
+
// punctuation, while an embedded quote is followed by content.
|
|
358
|
+
let repaired = "";
|
|
359
|
+
let inString = false;
|
|
360
|
+
let escaped = false;
|
|
361
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
362
|
+
const character = source[index];
|
|
363
|
+
if (character !== '"') {
|
|
364
|
+
repaired += character;
|
|
365
|
+
if (inString && character === "\\" && !escaped)
|
|
366
|
+
escaped = true;
|
|
367
|
+
else
|
|
368
|
+
escaped = false;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (escaped) {
|
|
372
|
+
repaired += character;
|
|
373
|
+
escaped = false;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (!inString) {
|
|
377
|
+
inString = true;
|
|
378
|
+
repaired += character;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const next = source.slice(index + 1).trimStart()[0];
|
|
382
|
+
if ([",", "}", "]", ":"].includes(next ?? "") || source.slice(index + 1).trim() === "") {
|
|
383
|
+
inString = false;
|
|
384
|
+
repaired += character;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
repaired += "\\\"";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
return JSON.parse(repaired);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const isContract = (candidate) => {
|
|
399
|
+
const record = asRecord(candidate);
|
|
400
|
+
return (record?.schemaVersion === 1 &&
|
|
401
|
+
asRecord(record.targets) !== null &&
|
|
402
|
+
Array.isArray(record.requirements) &&
|
|
403
|
+
Array.isArray(record.verificationTargets));
|
|
404
|
+
};
|
|
346
405
|
if (trimmed.startsWith("{") && trimmed.endsWith("}"))
|
|
347
|
-
return
|
|
406
|
+
return parse(trimmed);
|
|
407
|
+
const balancedObjects = [];
|
|
408
|
+
for (let start = 0; start < trimmed.length; start += 1) {
|
|
409
|
+
if (trimmed[start] !== "{")
|
|
410
|
+
continue;
|
|
411
|
+
let depth = 0;
|
|
412
|
+
let inString = false;
|
|
413
|
+
let escaped = false;
|
|
414
|
+
for (let index = start; index < trimmed.length; index += 1) {
|
|
415
|
+
const character = trimmed[index];
|
|
416
|
+
if (inString) {
|
|
417
|
+
if (escaped)
|
|
418
|
+
escaped = false;
|
|
419
|
+
else if (character === "\\")
|
|
420
|
+
escaped = true;
|
|
421
|
+
else if (character === '"')
|
|
422
|
+
inString = false;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (character === '"')
|
|
426
|
+
inString = true;
|
|
427
|
+
else if (character === "{")
|
|
428
|
+
depth += 1;
|
|
429
|
+
else if (character === "}" && --depth === 0) {
|
|
430
|
+
try {
|
|
431
|
+
const candidate = parse(trimmed.slice(start, index + 1));
|
|
432
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate))
|
|
433
|
+
balancedObjects.push(candidate);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// Continue scanning for a later complete JSON object.
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const balancedContracts = balancedObjects.filter(isContract);
|
|
443
|
+
if (balancedContracts.length === 1)
|
|
444
|
+
return balancedContracts[0];
|
|
445
|
+
if (balancedObjects.length === 1)
|
|
446
|
+
return balancedObjects[0];
|
|
348
447
|
const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
|
|
349
|
-
if (blocks.length
|
|
448
|
+
if (blocks.length === 0)
|
|
350
449
|
throw new Error("output must contain exactly one fenced json object");
|
|
351
|
-
|
|
450
|
+
const candidates = [];
|
|
451
|
+
for (const block of blocks) {
|
|
452
|
+
try {
|
|
453
|
+
const candidate = parse(block[1]);
|
|
454
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate))
|
|
455
|
+
candidates.push(candidate);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Ignore incomplete model scratch blocks. A later complete contract
|
|
459
|
+
// block may still be deterministically recoverable.
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const contractCandidates = candidates.filter(isContract);
|
|
463
|
+
if (contractCandidates.length === 1)
|
|
464
|
+
return contractCandidates[0];
|
|
465
|
+
if (candidates.length === 1)
|
|
466
|
+
return candidates[0];
|
|
467
|
+
if (candidates.length === 0)
|
|
468
|
+
throw new Error("output must contain exactly one valid fenced json object (found 0)");
|
|
469
|
+
throw new Error(`output must contain exactly one valid frontend contract json object (found ${contractCandidates.length || candidates.length})`);
|
|
352
470
|
}
|
|
353
471
|
/**
|
|
354
472
|
* Build the authoritative frontend-implementation-contract sourceBinding from
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
4
|
-
import { frontendImplementationContractSchema, materializeFrontendImplementationContract, assertFrontendSourceBindingFresh, } from "./frontend-implementation-contract.js";
|
|
5
|
+
import { frontendImplementationContractSchema, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, materializeFrontendImplementationContract, assertFrontendSourceBindingFresh, } from "./frontend-implementation-contract.js";
|
|
5
6
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
6
7
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
7
8
|
async function selectNode(runDir, primary, fallbacks) {
|
|
@@ -46,7 +47,12 @@ function firstNonEmptyVerdictLine(text) {
|
|
|
46
47
|
// The prompt requires VERDICT to be the first non-empty line, but model
|
|
47
48
|
// output can still prepend a summary. Keep the protocol strict in the
|
|
48
49
|
// prompt while making the deterministic gate resilient to that drift.
|
|
49
|
-
|
|
50
|
+
const verdictLine = normalizedLines.find((line) => /^VERDICT:/.test(line)) ?? first;
|
|
51
|
+
if (/^VERDICT:\s*pass(?:\s*[.!?。!?]|\s+|$)/i.test(verdictLine))
|
|
52
|
+
return "VERDICT: pass";
|
|
53
|
+
if (/^VERDICT:\s*request-revision(?:\s*[.!?。!?]|\s+|$)/i.test(verdictLine))
|
|
54
|
+
return "VERDICT: request-revision";
|
|
55
|
+
return verdictLine;
|
|
50
56
|
}
|
|
51
57
|
function eventArgs(event) {
|
|
52
58
|
return event.args ?? event.toolInput ?? event.input ?? {};
|
|
@@ -155,13 +161,20 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
155
161
|
if (missingIds.length > 0) {
|
|
156
162
|
throw new Error(`frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`);
|
|
157
163
|
}
|
|
158
|
-
const
|
|
164
|
+
const artifactPath = path.join(input.runDir, input.config.outputDir, input.config.artifactName);
|
|
165
|
+
const artifact = await stat(artifactPath)
|
|
166
|
+
.then(async () => ({
|
|
167
|
+
path: artifactPath,
|
|
168
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
169
|
+
sha256: createHash("sha256").update(await readFile(artifactPath)).digest("hex"),
|
|
170
|
+
}))
|
|
171
|
+
.catch(() => materializeFrontendImplementationContract({
|
|
159
172
|
runDir: input.runDir,
|
|
160
173
|
fromNodeId: planNodeId,
|
|
161
174
|
artifactName: input.config.artifactName,
|
|
162
175
|
outputDir: input.config.outputDir,
|
|
163
176
|
sourceBinding: input.sourceBinding,
|
|
164
|
-
});
|
|
177
|
+
}));
|
|
165
178
|
const raw = JSON.parse(await readFile(artifact.path, "utf8"));
|
|
166
179
|
const contract = frontendImplementationContractSchema.parse(raw);
|
|
167
180
|
if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
|
|
@@ -183,10 +196,11 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
183
196
|
reviewNodeId,
|
|
184
197
|
repoRoot: workspaceRoot ?? process.cwd(),
|
|
185
198
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
// OpenSpec is advisory design evidence, not an authorization boundary.
|
|
200
|
+
// Model read behavior is nondeterministic, and a missed read must not block
|
|
201
|
+
// a contract/schema/write-set-authorized implementation. Preserve any
|
|
202
|
+
// successful evidence for review context, while allowing unavailable
|
|
203
|
+
// evidence to be reported downstream.
|
|
190
204
|
return {
|
|
191
205
|
ok: true,
|
|
192
206
|
planNodeId,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
5
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -1080,40 +1079,14 @@ function isFrontendLintVerifyCommand(command) {
|
|
|
1080
1079
|
const text = `${command.label}\n${command.args.join(" ")}`;
|
|
1081
1080
|
return /\b(?:lint|eslint)\b/i.test(text);
|
|
1082
1081
|
}
|
|
1083
|
-
function isManagedCiWrapperVerifyCommand(command) {
|
|
1084
|
-
const text = `${command.label}\n${command.args.join(" ")}`.replaceAll("\\", "/");
|
|
1085
|
-
return /\bscripts\/ci(?:-tests)?\.sh\b/.test(text);
|
|
1086
|
-
}
|
|
1087
|
-
function repoHasNpmScript(repoRoot, scriptName) {
|
|
1088
|
-
if (!repoRoot)
|
|
1089
|
-
return false;
|
|
1090
|
-
const packagePath = path.join(repoRoot, "package.json");
|
|
1091
|
-
if (!existsSync(packagePath))
|
|
1092
|
-
return false;
|
|
1093
|
-
try {
|
|
1094
|
-
const decoded = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
1095
|
-
return typeof decoded.scripts?.[scriptName] === "string";
|
|
1096
|
-
}
|
|
1097
|
-
catch {
|
|
1098
|
-
return false;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
1082
|
function partitionFrontendStaticVerifyCommands(input) {
|
|
1102
1083
|
const commands = input.commands ?? [];
|
|
1103
|
-
|
|
1084
|
+
// Frontend DAGs deliberately never generate lint verification. Existing
|
|
1085
|
+
// project lint debt is allowed to remain outside this workflow, and an
|
|
1086
|
+
// explicitly mentioned lint command must not reintroduce the lint gate.
|
|
1104
1087
|
const staticCommands = commands.filter((command) => !isFrontendLintVerifyCommand(command));
|
|
1105
|
-
if (lintCommands.length === 0 &&
|
|
1106
|
-
commands.some(isManagedCiWrapperVerifyCommand) &&
|
|
1107
|
-
repoHasNpmScript(input.repoRoot, "lint")) {
|
|
1108
|
-
lintCommands.push({
|
|
1109
|
-
args: ["npm", "run", "lint"],
|
|
1110
|
-
cwd: input.repoRoot,
|
|
1111
|
-
label: "npm run lint",
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
1088
|
return {
|
|
1115
1089
|
lint: {
|
|
1116
|
-
...(lintCommands.length > 0 ? { commands: lintCommands } : {}),
|
|
1117
1090
|
commandSource: input.commandSource,
|
|
1118
1091
|
},
|
|
1119
1092
|
static: {
|
|
@@ -1212,7 +1185,6 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1212
1185
|
const staticCommands = firstExisting([
|
|
1213
1186
|
"typecheck",
|
|
1214
1187
|
"check-types",
|
|
1215
|
-
"lint",
|
|
1216
1188
|
"check",
|
|
1217
1189
|
"build",
|
|
1218
1190
|
]);
|
|
@@ -2457,9 +2429,62 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2457
2429
|
sourceContext,
|
|
2458
2430
|
].join("\n\n"),
|
|
2459
2431
|
},
|
|
2432
|
+
{
|
|
2433
|
+
id: "frontend-contract-json-pi",
|
|
2434
|
+
depends_on: [
|
|
2435
|
+
"frontend-plan-revision-pi",
|
|
2436
|
+
"frontend-plan-pi",
|
|
2437
|
+
"frontend-final-design-review-pi",
|
|
2438
|
+
"frontend-design-review-pi",
|
|
2439
|
+
],
|
|
2440
|
+
dependsPolicy: "all-or-condition-skip",
|
|
2441
|
+
role: "planner",
|
|
2442
|
+
executor: "pi",
|
|
2443
|
+
complexity: "MED",
|
|
2444
|
+
writePolicy: "read-only",
|
|
2445
|
+
outputMode: "structured-required",
|
|
2446
|
+
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2447
|
+
allowedPaths: readOnlyPaths,
|
|
2448
|
+
forbiddenPaths,
|
|
2449
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2450
|
+
outputContract: "Return exactly one JSON object conforming to frontend-implementation-contract-v1. No Markdown, prose, comments, or code fences.",
|
|
2451
|
+
subtask_prompt: [
|
|
2452
|
+
"Convert the effective reviewed frontend plan into the canonical frontend-implementation-contract-v1 JSON.",
|
|
2453
|
+
"Use frontend-plan-revision-pi when it is FINISHED; otherwise use frontend-plan-pi. Confirm the effective design review passed before producing the contract.",
|
|
2454
|
+
"Return only the JSON object. Do not wrap it in Markdown or a code fence. Do not add explanatory text.",
|
|
2455
|
+
"Preserve all requirement expectedOutcome, interaction trigger/expectedBehavior, target files, verification targets, Mock/API decisions, and Real Integration Gap from the effective plan.",
|
|
2456
|
+
frontendContractSchemaBlock,
|
|
2457
|
+
sourceContext,
|
|
2458
|
+
].join("\n\n"),
|
|
2459
|
+
},
|
|
2460
|
+
{
|
|
2461
|
+
id: "frontend-contract-json-validate-shell",
|
|
2462
|
+
depends_on: ["frontend-contract-json-pi"],
|
|
2463
|
+
role: "verifier",
|
|
2464
|
+
executor: "shell",
|
|
2465
|
+
complexity: "LOW",
|
|
2466
|
+
writePolicy: "read-only",
|
|
2467
|
+
allowedPaths: readOnlyPaths,
|
|
2468
|
+
forbiddenPaths,
|
|
2469
|
+
outputContract: "Validated frontend implementation contract artifact with schema ID and SHA-256.",
|
|
2470
|
+
subtask_prompt: "Materialize and validate the structured frontend contract before prewrite authorization.",
|
|
2471
|
+
shell: {
|
|
2472
|
+
commands: [],
|
|
2473
|
+
jsonArtifactGate: {
|
|
2474
|
+
fromNodeId: "frontend-contract-json-pi",
|
|
2475
|
+
schemaId: "frontend-implementation-contract-v1",
|
|
2476
|
+
artifactName: "frontend-implementation-contract.json",
|
|
2477
|
+
outputDir: "contracts",
|
|
2478
|
+
},
|
|
2479
|
+
cwd: ".",
|
|
2480
|
+
timeoutMs: 60000,
|
|
2481
|
+
},
|
|
2482
|
+
},
|
|
2460
2483
|
{
|
|
2461
2484
|
id: "frontend-prewrite-gate-shell",
|
|
2462
2485
|
depends_on: [
|
|
2486
|
+
"frontend-contract-json-pi",
|
|
2487
|
+
"frontend-contract-json-validate-shell",
|
|
2463
2488
|
"frontend-final-design-review-pi",
|
|
2464
2489
|
"frontend-design-review-pi",
|
|
2465
2490
|
"frontend-plan-revision-pi",
|
|
@@ -2478,8 +2503,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2478
2503
|
commands: [],
|
|
2479
2504
|
frontendPrewriteGate: {
|
|
2480
2505
|
schemaVersion: 1,
|
|
2481
|
-
planFromNodeId: "frontend-
|
|
2482
|
-
planFallbackFromNodeIds: ["frontend-plan-pi"],
|
|
2506
|
+
planFromNodeId: "frontend-contract-json-pi",
|
|
2507
|
+
planFallbackFromNodeIds: ["frontend-plan-revision-pi", "frontend-plan-pi"],
|
|
2483
2508
|
reviewFromNodeId: "frontend-final-design-review-pi",
|
|
2484
2509
|
reviewFallbackFromNodeIds: ["frontend-design-review-pi"],
|
|
2485
2510
|
requiredRequirementIds: requirementIds,
|
|
@@ -5363,7 +5388,7 @@ async function buildHybridDagForTemplate(sources, template) {
|
|
|
5363
5388
|
else if (template === "review-gated-dag")
|
|
5364
5389
|
spec = buildReviewGatedHybridDag(standard, sources);
|
|
5365
5390
|
else
|
|
5366
|
-
spec = buildSupervisedHybridDag(standard, sources);
|
|
5391
|
+
spec = await buildSupervisedHybridDag(standard, sources);
|
|
5367
5392
|
}
|
|
5368
5393
|
applyProjectGovernanceReview(spec, template, sources);
|
|
5369
5394
|
// New generate path always emits DagSpec v4 + bindings.
|
|
@@ -5807,10 +5832,12 @@ function buildWriteSetGateNode(sources) {
|
|
|
5807
5832
|
},
|
|
5808
5833
|
};
|
|
5809
5834
|
}
|
|
5810
|
-
function buildSoftVerifyNode(sources) {
|
|
5835
|
+
async function buildSoftVerifyNode(sources) {
|
|
5811
5836
|
const implementId = implementationNodeId();
|
|
5812
5837
|
const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
|
|
5813
|
-
const fallbackCommands =
|
|
5838
|
+
const fallbackCommands = sources.repoRoot
|
|
5839
|
+
? (await discoverFrontendFallbackVerifyCommands(sources.repoRoot)).staticCommands
|
|
5840
|
+
: ["npm run typecheck"];
|
|
5814
5841
|
const focusedIntermediate = sources.verifyCommands?.intermediate.filter((command) => !isFullSuiteVerifyCommand(command));
|
|
5815
5842
|
const plannedIntermediate = applyMavenVerificationPlanning({
|
|
5816
5843
|
repoRoot: sources.repoRoot,
|
|
@@ -6052,7 +6079,7 @@ function resolveSupervisedConvergence(taskConfig) {
|
|
|
6052
6079
|
chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
|
|
6053
6080
|
};
|
|
6054
6081
|
}
|
|
6055
|
-
function buildSupervisedHybridDag(standard, sources) {
|
|
6082
|
+
async function buildSupervisedHybridDag(standard, sources) {
|
|
6056
6083
|
const contract = getTaskOrThrow(standard, "contract-pi");
|
|
6057
6084
|
const scoutSrc = getTaskOrThrow(standard, "scout-src");
|
|
6058
6085
|
const scoutTests = getTaskOrThrow(standard, "scout-tests");
|
|
@@ -6104,7 +6131,7 @@ function buildSupervisedHybridDag(standard, sources) {
|
|
|
6104
6131
|
"final-write-set-audit-format-repair-pi",
|
|
6105
6132
|
],
|
|
6106
6133
|
}),
|
|
6107
|
-
buildSoftVerifyNode(sources),
|
|
6134
|
+
await buildSoftVerifyNode(sources),
|
|
6108
6135
|
buildProcessSupervisorNode(sources),
|
|
6109
6136
|
buildProcessGateNode(sources),
|
|
6110
6137
|
buildRepairNode(sources),
|
|
@@ -148,6 +148,29 @@ export function validateOutputProtocol(protocol, text) {
|
|
|
148
148
|
reason: `missing first non-empty line; expected one of: ${protocol.validLines.map((l) => JSON.stringify(l)).join(" or ")}`,
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
|
+
const isReviewVerdict = protocol.validLines.length === 2 && protocol.validLines.includes("VERDICT: pass") && protocol.validLines.includes("VERDICT: request-revision");
|
|
152
|
+
// Models frequently prepend a short explanation despite the protocol
|
|
153
|
+
// instruction. Recover only when there is exactly one unambiguous protocol
|
|
154
|
+
// line; conflicting or repeated verdicts remain fail-closed.
|
|
155
|
+
const candidates = isReviewVerdict ? text
|
|
156
|
+
.split("\n")
|
|
157
|
+
.map((line) => normalizeVerdictCandidateLine(line.trim()))
|
|
158
|
+
.filter((line) => protocol.validLines.includes(line)) : [];
|
|
159
|
+
const uniqueCandidates = [...new Set(candidates)];
|
|
160
|
+
if (uniqueCandidates.length > 1) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
failureCategory: "protocol-invalid",
|
|
164
|
+
reason: `conflicting protocol lines found: ${uniqueCandidates.map((line) => JSON.stringify(line)).join(" and ")}`,
|
|
165
|
+
firstNonEmptyLine: first,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (protocol.validLines.includes(first)) {
|
|
169
|
+
return { ok: true, matchedLine: first };
|
|
170
|
+
}
|
|
171
|
+
if (uniqueCandidates.length === 1) {
|
|
172
|
+
return { ok: true, matchedLine: uniqueCandidates[0] };
|
|
173
|
+
}
|
|
151
174
|
if (!protocol.validLines.includes(first)) {
|
|
152
175
|
return {
|
|
153
176
|
ok: false,
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"requirements": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["id", "expectedOutcome", "implementationTargets", "verificationTargetIds"], "properties": { "id": { "$ref": "#/$defs/requirementId" }, "expectedOutcome": { "type": "string", "minLength": 1 }, "implementationTargets": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "verificationTargetIds": { "type": "array", "items": { "type": "string", "minLength": 1 } }, "evidenceGap": { "$ref": "#/$defs/gap" } } } },
|
|
14
14
|
"uiStates": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["name", "applicable"], "properties": { "name": { "type": "string", "minLength": 1 }, "applicable": { "type": "boolean" }, "expectedBehavior": { "type": "string", "minLength": 1 }, "implementationTargets": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "verificationTargetIds": { "type": "array", "items": { "type": "string", "minLength": 1 } }, "notApplicableReason": { "type": "string", "minLength": 1 } }, "allOf": [{ "if": { "properties": { "applicable": { "const": true } }, "required": ["applicable"] }, "then": { "required": ["expectedBehavior", "implementationTargets", "verificationTargetIds"], "properties": { "implementationTargets": { "minItems": 1 }, "verificationTargetIds": { "minItems": 1 } } } }, { "if": { "properties": { "applicable": { "const": false } }, "required": ["applicable"] }, "then": { "required": ["notApplicableReason"] } }] } },
|
|
15
15
|
"interactions": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["name", "trigger", "expectedBehavior", "implementationTargets", "verificationTargetIds"], "properties": { "name": { "type": "string", "minLength": 1 }, "trigger": { "type": "string", "minLength": 1 }, "expectedBehavior": { "type": "string", "minLength": 1 }, "implementationTargets": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "verificationTargetIds": { "type": "array", "items": { "type": "string" } } } } },
|
|
16
|
-
"mockApi": { "type": "object", "additionalProperties": false, "required": ["strategy", "productionDefaultOff", "activation", "endpoints"], "properties": { "strategy": { "enum": ["native", "browser-intercept", "request-adapter", "not-needed"] }, "productionDefaultOff": { "const": true }, "activation": { "type": "string", "minLength": 1 }, "endpoints": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["method", "path"], "properties": { "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] }, "path": { "type": "string", "pattern": "^/" }, "fixture": { "$ref": "#/$defs/path" }, "consumer": { "$ref": "#/$defs/path" } } } } } },
|
|
16
|
+
"mockApi": { "type": "object", "additionalProperties": false, "required": ["strategy", "productionDefaultOff", "activation", "endpoints"], "properties": { "strategy": { "enum": ["native", "browser-intercept", "request-adapter", "not-needed"] }, "productionDefaultOff": { "const": true }, "activation": { "type": ["string", "null"], "minLength": 1 }, "endpoints": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["method", "path"], "properties": { "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] }, "path": { "type": "string", "pattern": "^/" }, "fixture": { "anyOf": [{ "$ref": "#/$defs/path" }, { "type": "null" }] }, "consumer": { "anyOf": [{ "$ref": "#/$defs/path" }, { "type": "null" }] } } } } } },
|
|
17
17
|
"designEvidence": { "type": "object", "additionalProperties": false, "required": ["source", "paths", "conflicts"], "properties": { "source": { "type": "string", "minLength": 1 }, "paths": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "conflicts": { "type": "array", "items": { "type": "string", "minLength": 1 } } } },
|
|
18
|
-
"verificationTargets": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["id", "type", "commandLabel", "file", "requirementIds", "uiStates"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "enum": ["static", "unit", "component", "integration", "mock"] }, "commandLabel": { "type": "string", "minLength": 1 }, "file": { "$ref": "#/$defs/path" }, "symbol": { "type": "string", "minLength": 1 }, "requirementIds": { "type": "array", "items": { "$ref": "#/$defs/requirementId" } }, "uiStates": { "type": "array", "items": { "type": "string", "minLength": 1 } } } } },
|
|
18
|
+
"verificationTargets": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["id", "type", "commandLabel", "file", "requirementIds", "uiStates"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "enum": ["static", "unit", "component", "integration", "mock"] }, "commandLabel": { "type": "string", "minLength": 1 }, "file": { "$ref": "#/$defs/path" }, "symbol": { "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }, "requirementIds": { "type": "array", "items": { "$ref": "#/$defs/requirementId" } }, "uiStates": { "type": "array", "items": { "type": "string", "minLength": 1 } } } } },
|
|
19
19
|
"evidenceGaps": { "type": "array", "items": { "$ref": "#/$defs/gap" } }
|
|
20
20
|
},
|
|
21
21
|
"allOf": [{ "if": { "properties": { "mockApi": { "properties": { "strategy": { "enum": ["native", "browser-intercept", "request-adapter"] } } } } }, "then": { "properties": { "mockApi": { "properties": { "endpoints": { "minItems": 1, "items": { "required": ["method", "path", "fixture", "consumer"] } } } } } } }],
|