@tea-agent/loop-agent 0.27.1-beta.2 → 0.27.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/CHANGELOG.md +16 -1
- package/dist/executors/dag-pi-executor.js +22 -18
- package/dist/workflows/dag/frontend-implementation-contract.js +6 -124
- package/dist/workflows/dag/frontend-prewrite-gate.js +5 -35
- package/dist/workflows/dag/init-hybrid.js +38 -65
- package/dist/workflows/dag/output-protocol.js +0 -23
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/harness.json +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.27.1] - 2026-08-04
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 前端测试 L-5 报告看板全新改版,与后端报告模板统一
|
|
10
|
+
- 修复后端测试因 pytest 脚本漏生成而直接阻断的问题,升级为精确的缺失脚本恢复机制
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- 前端测试 L-5 看板新增确定性质量信号、门禁摘要、证据详情及用例执行详情展示
|
|
15
|
+
|
|
16
|
+
### 改进
|
|
17
|
+
|
|
18
|
+
- 前端 L-5 报告看板与后端报告模板视觉与结构对齐,提升跨端报告一致性
|
|
19
|
+
|
|
5
20
|
### 修复
|
|
6
21
|
|
|
7
|
-
-
|
|
22
|
+
- 修复后端测试在 Markdown 已声明安全映射脚本、但 pytest writer 漏生成文件时直接以 invalid-output 阻断 repair 的问题:initial facts 升级为 backend-test-pytest-collection-v2,可记录 expected/existing/missing scripts、collectionAttempted=false 与 missing-mapped-pytest-script,复用既有单次有界 repair 精确创建缺失脚本,并由最终收集、scope、安全和 hash freshness 门禁重新授权业务执行
|
|
8
23
|
|
|
9
24
|
## [0.27.0] - 2026-08-03
|
|
10
25
|
|
|
@@ -576,19 +576,32 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
576
576
|
export function validateWriterImplementationOutcome(text, changedFiles) {
|
|
577
577
|
const parsed = parseWriterImplementationOutcome(text);
|
|
578
578
|
const diagnostics = writerOutcomeDiagnostics(text, parsed, changedFiles.length);
|
|
579
|
-
|
|
580
|
-
// may be missing, malformed, or inconsistent without blocking a completed
|
|
581
|
-
// writer. An explicit blocked signal remains a hard failure.
|
|
582
|
-
if (parsed.candidates.some((candidate) => candidate.outcome === "blocked")) {
|
|
579
|
+
if (parsed.kind !== "valid") {
|
|
583
580
|
return {
|
|
584
581
|
ok: false,
|
|
585
|
-
reason: `writer outcome validation failed:
|
|
582
|
+
reason: `writer outcome validation failed: ${parsed.kind} outcome; ${diagnostics}; expected IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked`,
|
|
586
583
|
};
|
|
587
584
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
585
|
+
const outcome = parsed.outcome;
|
|
586
|
+
if (outcome === "blocked") {
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
reason: `writer outcome validation failed: IMPLEMENTATION_OUTCOME: blocked cannot complete successfully; ${diagnostics}`,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (outcome === "changed" && changedFiles.length === 0) {
|
|
593
|
+
return {
|
|
594
|
+
ok: false,
|
|
595
|
+
reason: `writer outcome validation failed: changed outcome has an empty diff; ${diagnostics}`,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
if (outcome === "already-satisfied" && changedFiles.length > 0) {
|
|
599
|
+
return {
|
|
600
|
+
ok: false,
|
|
601
|
+
reason: `writer outcome validation failed: already-satisfied outcome has a non-empty diff; ${diagnostics}`,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return { ok: true, outcome };
|
|
592
605
|
}
|
|
593
606
|
function parseWriterImplementationOutcome(text) {
|
|
594
607
|
const lines = text.split(/\r?\n/);
|
|
@@ -719,15 +732,6 @@ function normalizeProtocolLine(line, firstProtocolLine, nextLine) {
|
|
|
719
732
|
const value = normalizedProtocolValue(direct[1] ?? "");
|
|
720
733
|
return value ? `${firstProtocolLine} ${value}` : undefined;
|
|
721
734
|
}
|
|
722
|
-
// Writers sometimes place a short delivery sentence before the protocol
|
|
723
|
-
// line. Accept the protocol token when it appears inline, while preserving
|
|
724
|
-
// the candidate value so template repetitions still fail as unknown or
|
|
725
|
-
// conflicting outcomes.
|
|
726
|
-
const inline = normalizedLine.match(new RegExp(`${labelPattern}\\s*[::]\\s*(.+)$`, "i"));
|
|
727
|
-
if (inline) {
|
|
728
|
-
const value = normalizedProtocolValue(inline[1] ?? "");
|
|
729
|
-
return value ? `${firstProtocolLine} ${value}` : undefined;
|
|
730
|
-
}
|
|
731
735
|
const splitValue = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]?\\s*$`, "i"));
|
|
732
736
|
if (splitValue && nextLine !== undefined) {
|
|
733
737
|
const value = normalizedProtocolValue(nextLine);
|
|
@@ -155,7 +155,7 @@ export const frontendImplementationContractSchema = z
|
|
|
155
155
|
"not-needed",
|
|
156
156
|
]),
|
|
157
157
|
productionDefaultOff: z.literal(true),
|
|
158
|
-
activation: z.
|
|
158
|
+
activation: z.string().min(1),
|
|
159
159
|
endpoints: z.array(z
|
|
160
160
|
.object({
|
|
161
161
|
method: z.enum([
|
|
@@ -168,10 +168,7 @@ export const frontendImplementationContractSchema = z
|
|
|
168
168
|
"OPTIONS",
|
|
169
169
|
]),
|
|
170
170
|
path: z.string().startsWith("/"),
|
|
171
|
-
|
|
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()),
|
|
171
|
+
fixture: safePath.optional(),
|
|
175
172
|
consumer: safePath.optional(),
|
|
176
173
|
})
|
|
177
174
|
.strict()),
|
|
@@ -190,7 +187,7 @@ export const frontendImplementationContractSchema = z
|
|
|
190
187
|
type: z.enum(["static", "unit", "component", "integration", "mock"]),
|
|
191
188
|
commandLabel: z.string().min(1),
|
|
192
189
|
file: safePath,
|
|
193
|
-
symbol: z.
|
|
190
|
+
symbol: z.string().min(1).optional(),
|
|
194
191
|
requirementIds: z.array(id),
|
|
195
192
|
uiStates: z.array(z.string().min(1)),
|
|
196
193
|
})
|
|
@@ -346,127 +343,12 @@ function secretIssues(value, at = "$", issues = []) {
|
|
|
346
343
|
}
|
|
347
344
|
export function extractFrontendImplementationJson(text) {
|
|
348
345
|
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
|
-
};
|
|
405
346
|
if (trimmed.startsWith("{") && trimmed.endsWith("}"))
|
|
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];
|
|
347
|
+
return JSON.parse(trimmed);
|
|
447
348
|
const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
|
|
448
|
-
if (blocks.length
|
|
349
|
+
if (blocks.length !== 1)
|
|
449
350
|
throw new Error("output must contain exactly one fenced json object");
|
|
450
|
-
|
|
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})`);
|
|
351
|
+
return JSON.parse(blocks[0][1]);
|
|
470
352
|
}
|
|
471
353
|
/**
|
|
472
354
|
* Build the authoritative frontend-implementation-contract sourceBinding from
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
-
import { frontendImplementationContractSchema,
|
|
4
|
+
import { frontendImplementationContractSchema, materializeFrontendImplementationContract, assertFrontendSourceBindingFresh, } from "./frontend-implementation-contract.js";
|
|
6
5
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
7
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
8
7
|
async function selectNode(runDir, primary, fallbacks) {
|
|
@@ -47,12 +46,7 @@ function firstNonEmptyVerdictLine(text) {
|
|
|
47
46
|
// The prompt requires VERDICT to be the first non-empty line, but model
|
|
48
47
|
// output can still prepend a summary. Keep the protocol strict in the
|
|
49
48
|
// prompt while making the deterministic gate resilient to that drift.
|
|
50
|
-
|
|
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;
|
|
49
|
+
return normalizedLines.find((line) => /^VERDICT:/.test(line)) ?? first;
|
|
56
50
|
}
|
|
57
51
|
function eventArgs(event) {
|
|
58
52
|
return event.args ?? event.toolInput ?? event.input ?? {};
|
|
@@ -128,23 +122,6 @@ async function checkOpenspecReadEvidence(input) {
|
|
|
128
122
|
}
|
|
129
123
|
return [...matched];
|
|
130
124
|
}
|
|
131
|
-
async function existingOpenspecCandidates(candidates, repoRoot) {
|
|
132
|
-
const existing = [];
|
|
133
|
-
for (const candidate of candidates) {
|
|
134
|
-
if (!isOpenspecSpecFilePath(candidate))
|
|
135
|
-
continue;
|
|
136
|
-
try {
|
|
137
|
-
const info = await stat(path.resolve(repoRoot, candidate));
|
|
138
|
-
if (info.isFile())
|
|
139
|
-
existing.push(candidate);
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// Model-generated typos and stale capability paths are not readable
|
|
143
|
-
// evidence candidates and must not create a false prewrite block.
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return existing;
|
|
147
|
-
}
|
|
148
125
|
export async function runFrontendPrewriteGate(input) {
|
|
149
126
|
const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
|
|
150
127
|
if (workspaceRoot) {
|
|
@@ -178,20 +155,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
178
155
|
if (missingIds.length > 0) {
|
|
179
156
|
throw new Error(`frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`);
|
|
180
157
|
}
|
|
181
|
-
const
|
|
182
|
-
const artifact = await stat(artifactPath)
|
|
183
|
-
.then(async () => ({
|
|
184
|
-
path: artifactPath,
|
|
185
|
-
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
186
|
-
sha256: createHash("sha256").update(await readFile(artifactPath)).digest("hex"),
|
|
187
|
-
}))
|
|
188
|
-
.catch(() => materializeFrontendImplementationContract({
|
|
158
|
+
const artifact = await materializeFrontendImplementationContract({
|
|
189
159
|
runDir: input.runDir,
|
|
190
160
|
fromNodeId: planNodeId,
|
|
191
161
|
artifactName: input.config.artifactName,
|
|
192
162
|
outputDir: input.config.outputDir,
|
|
193
163
|
sourceBinding: input.sourceBinding,
|
|
194
|
-
})
|
|
164
|
+
});
|
|
195
165
|
const raw = JSON.parse(await readFile(artifact.path, "utf8"));
|
|
196
166
|
const contract = frontendImplementationContractSchema.parse(raw);
|
|
197
167
|
if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
|
|
@@ -205,7 +175,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
205
175
|
throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
|
|
206
176
|
}
|
|
207
177
|
}
|
|
208
|
-
const candidatePaths =
|
|
178
|
+
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
209
179
|
const openspecReadPaths = await checkOpenspecReadEvidence({
|
|
210
180
|
runDir: input.runDir,
|
|
211
181
|
candidatePaths,
|
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -1078,14 +1079,40 @@ function isFrontendLintVerifyCommand(command) {
|
|
|
1078
1079
|
const text = `${command.label}\n${command.args.join(" ")}`;
|
|
1079
1080
|
return /\b(?:lint|eslint)\b/i.test(text);
|
|
1080
1081
|
}
|
|
1082
|
+
function isManagedCiWrapperVerifyCommand(command) {
|
|
1083
|
+
const text = `${command.label}\n${command.args.join(" ")}`.replaceAll("\\", "/");
|
|
1084
|
+
return /\bscripts\/ci(?:-tests)?\.sh\b/.test(text);
|
|
1085
|
+
}
|
|
1086
|
+
function repoHasNpmScript(repoRoot, scriptName) {
|
|
1087
|
+
if (!repoRoot)
|
|
1088
|
+
return false;
|
|
1089
|
+
const packagePath = path.join(repoRoot, "package.json");
|
|
1090
|
+
if (!existsSync(packagePath))
|
|
1091
|
+
return false;
|
|
1092
|
+
try {
|
|
1093
|
+
const decoded = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
1094
|
+
return typeof decoded.scripts?.[scriptName] === "string";
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1081
1100
|
function partitionFrontendStaticVerifyCommands(input) {
|
|
1082
1101
|
const commands = input.commands ?? [];
|
|
1083
|
-
|
|
1084
|
-
// project lint debt is allowed to remain outside this workflow, and an
|
|
1085
|
-
// explicitly mentioned lint command must not reintroduce the lint gate.
|
|
1102
|
+
const lintCommands = commands.filter(isFrontendLintVerifyCommand);
|
|
1086
1103
|
const staticCommands = commands.filter((command) => !isFrontendLintVerifyCommand(command));
|
|
1104
|
+
if (lintCommands.length === 0 &&
|
|
1105
|
+
commands.some(isManagedCiWrapperVerifyCommand) &&
|
|
1106
|
+
repoHasNpmScript(input.repoRoot, "lint")) {
|
|
1107
|
+
lintCommands.push({
|
|
1108
|
+
args: ["npm", "run", "lint"],
|
|
1109
|
+
cwd: input.repoRoot,
|
|
1110
|
+
label: "npm run lint",
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1087
1113
|
return {
|
|
1088
1114
|
lint: {
|
|
1115
|
+
...(lintCommands.length > 0 ? { commands: lintCommands } : {}),
|
|
1089
1116
|
commandSource: input.commandSource,
|
|
1090
1117
|
},
|
|
1091
1118
|
static: {
|
|
@@ -1184,6 +1211,7 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1184
1211
|
const staticCommands = firstExisting([
|
|
1185
1212
|
"typecheck",
|
|
1186
1213
|
"check-types",
|
|
1214
|
+
"lint",
|
|
1187
1215
|
"check",
|
|
1188
1216
|
"build",
|
|
1189
1217
|
]);
|
|
@@ -2427,62 +2455,9 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2427
2455
|
sourceContext,
|
|
2428
2456
|
].join("\n\n"),
|
|
2429
2457
|
},
|
|
2430
|
-
{
|
|
2431
|
-
id: "frontend-contract-json-pi",
|
|
2432
|
-
depends_on: [
|
|
2433
|
-
"frontend-plan-revision-pi",
|
|
2434
|
-
"frontend-plan-pi",
|
|
2435
|
-
"frontend-final-design-review-pi",
|
|
2436
|
-
"frontend-design-review-pi",
|
|
2437
|
-
],
|
|
2438
|
-
dependsPolicy: "all-or-condition-skip",
|
|
2439
|
-
role: "planner",
|
|
2440
|
-
executor: "pi",
|
|
2441
|
-
complexity: "MED",
|
|
2442
|
-
writePolicy: "read-only",
|
|
2443
|
-
outputMode: "structured-required",
|
|
2444
|
-
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2445
|
-
allowedPaths: readOnlyPaths,
|
|
2446
|
-
forbiddenPaths,
|
|
2447
|
-
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2448
|
-
outputContract: "Return exactly one JSON object conforming to frontend-implementation-contract-v1. No Markdown, prose, comments, or code fences.",
|
|
2449
|
-
subtask_prompt: [
|
|
2450
|
-
"Convert the effective reviewed frontend plan into the canonical frontend-implementation-contract-v1 JSON.",
|
|
2451
|
-
"Use frontend-plan-revision-pi when it is FINISHED; otherwise use frontend-plan-pi. Confirm the effective design review passed before producing the contract.",
|
|
2452
|
-
"Return only the JSON object. Do not wrap it in Markdown or a code fence. Do not add explanatory text.",
|
|
2453
|
-
"Preserve all requirement expectedOutcome, interaction trigger/expectedBehavior, target files, verification targets, Mock/API decisions, and Real Integration Gap from the effective plan.",
|
|
2454
|
-
frontendContractSchemaBlock,
|
|
2455
|
-
sourceContext,
|
|
2456
|
-
].join("\n\n"),
|
|
2457
|
-
},
|
|
2458
|
-
{
|
|
2459
|
-
id: "frontend-contract-json-validate-shell",
|
|
2460
|
-
depends_on: ["frontend-contract-json-pi"],
|
|
2461
|
-
role: "verifier",
|
|
2462
|
-
executor: "shell",
|
|
2463
|
-
complexity: "LOW",
|
|
2464
|
-
writePolicy: "read-only",
|
|
2465
|
-
allowedPaths: readOnlyPaths,
|
|
2466
|
-
forbiddenPaths,
|
|
2467
|
-
outputContract: "Validated frontend implementation contract artifact with schema ID and SHA-256.",
|
|
2468
|
-
subtask_prompt: "Materialize and validate the structured frontend contract before prewrite authorization.",
|
|
2469
|
-
shell: {
|
|
2470
|
-
commands: [],
|
|
2471
|
-
jsonArtifactGate: {
|
|
2472
|
-
fromNodeId: "frontend-contract-json-pi",
|
|
2473
|
-
schemaId: "frontend-implementation-contract-v1",
|
|
2474
|
-
artifactName: "frontend-implementation-contract.json",
|
|
2475
|
-
outputDir: "contracts",
|
|
2476
|
-
},
|
|
2477
|
-
cwd: ".",
|
|
2478
|
-
timeoutMs: 60000,
|
|
2479
|
-
},
|
|
2480
|
-
},
|
|
2481
2458
|
{
|
|
2482
2459
|
id: "frontend-prewrite-gate-shell",
|
|
2483
2460
|
depends_on: [
|
|
2484
|
-
"frontend-contract-json-pi",
|
|
2485
|
-
"frontend-contract-json-validate-shell",
|
|
2486
2461
|
"frontend-final-design-review-pi",
|
|
2487
2462
|
"frontend-design-review-pi",
|
|
2488
2463
|
"frontend-plan-revision-pi",
|
|
@@ -2501,8 +2476,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2501
2476
|
commands: [],
|
|
2502
2477
|
frontendPrewriteGate: {
|
|
2503
2478
|
schemaVersion: 1,
|
|
2504
|
-
planFromNodeId: "frontend-
|
|
2505
|
-
planFallbackFromNodeIds: ["frontend-plan-
|
|
2479
|
+
planFromNodeId: "frontend-plan-revision-pi",
|
|
2480
|
+
planFallbackFromNodeIds: ["frontend-plan-pi"],
|
|
2506
2481
|
reviewFromNodeId: "frontend-final-design-review-pi",
|
|
2507
2482
|
reviewFallbackFromNodeIds: ["frontend-design-review-pi"],
|
|
2508
2483
|
requiredRequirementIds: requirementIds,
|
|
@@ -5312,7 +5287,7 @@ async function buildHybridDagForTemplate(sources, template) {
|
|
|
5312
5287
|
else if (template === "review-gated-dag")
|
|
5313
5288
|
spec = buildReviewGatedHybridDag(standard, sources);
|
|
5314
5289
|
else
|
|
5315
|
-
spec =
|
|
5290
|
+
spec = buildSupervisedHybridDag(standard, sources);
|
|
5316
5291
|
}
|
|
5317
5292
|
applyProjectGovernanceReview(spec, template, sources);
|
|
5318
5293
|
// New generate path always emits DagSpec v4 + bindings.
|
|
@@ -5746,12 +5721,10 @@ function buildWriteSetGateNode(sources) {
|
|
|
5746
5721
|
},
|
|
5747
5722
|
};
|
|
5748
5723
|
}
|
|
5749
|
-
|
|
5724
|
+
function buildSoftVerifyNode(sources) {
|
|
5750
5725
|
const implementId = implementationNodeId();
|
|
5751
5726
|
const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
|
|
5752
|
-
const fallbackCommands =
|
|
5753
|
-
? (await discoverFrontendFallbackVerifyCommands(sources.repoRoot)).staticCommands
|
|
5754
|
-
: ["npm run typecheck"];
|
|
5727
|
+
const fallbackCommands = ["npm run typecheck"];
|
|
5755
5728
|
const focusedIntermediate = sources.verifyCommands?.intermediate.filter((command) => !isFullSuiteVerifyCommand(command));
|
|
5756
5729
|
const plannedIntermediate = applyMavenVerificationPlanning({
|
|
5757
5730
|
repoRoot: sources.repoRoot,
|
|
@@ -5993,7 +5966,7 @@ function resolveSupervisedConvergence(taskConfig) {
|
|
|
5993
5966
|
chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
|
|
5994
5967
|
};
|
|
5995
5968
|
}
|
|
5996
|
-
|
|
5969
|
+
function buildSupervisedHybridDag(standard, sources) {
|
|
5997
5970
|
const contract = getTaskOrThrow(standard, "contract-pi");
|
|
5998
5971
|
const scoutSrc = getTaskOrThrow(standard, "scout-src");
|
|
5999
5972
|
const scoutTests = getTaskOrThrow(standard, "scout-tests");
|
|
@@ -6045,7 +6018,7 @@ async function buildSupervisedHybridDag(standard, sources) {
|
|
|
6045
6018
|
"final-write-set-audit-format-repair-pi",
|
|
6046
6019
|
],
|
|
6047
6020
|
}),
|
|
6048
|
-
|
|
6021
|
+
buildSoftVerifyNode(sources),
|
|
6049
6022
|
buildProcessSupervisorNode(sources),
|
|
6050
6023
|
buildProcessGateNode(sources),
|
|
6051
6024
|
buildRepairNode(sources),
|
|
@@ -206,29 +206,6 @@ export function validateOutputProtocol(protocol, text) {
|
|
|
206
206
|
reason: `missing first non-empty line; expected one of: ${protocol.validLines.map((l) => JSON.stringify(l)).join(" or ")}`,
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
|
-
const isReviewVerdict = protocol.validLines.length === 2 && protocol.validLines.includes("VERDICT: pass") && protocol.validLines.includes("VERDICT: request-revision");
|
|
210
|
-
// Models frequently prepend a short explanation despite the protocol
|
|
211
|
-
// instruction. Recover only when there is exactly one unambiguous protocol
|
|
212
|
-
// line; conflicting or repeated verdicts remain fail-closed.
|
|
213
|
-
const candidates = isReviewVerdict ? text
|
|
214
|
-
.split("\n")
|
|
215
|
-
.map((line) => normalizeVerdictCandidateLine(line.trim()))
|
|
216
|
-
.filter((line) => protocol.validLines.includes(line)) : [];
|
|
217
|
-
const uniqueCandidates = [...new Set(candidates)];
|
|
218
|
-
if (uniqueCandidates.length > 1) {
|
|
219
|
-
return {
|
|
220
|
-
ok: false,
|
|
221
|
-
failureCategory: "protocol-invalid",
|
|
222
|
-
reason: `conflicting protocol lines found: ${uniqueCandidates.map((line) => JSON.stringify(line)).join(" and ")}`,
|
|
223
|
-
firstNonEmptyLine: first,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
if (protocol.validLines.includes(first)) {
|
|
227
|
-
return { ok: true, matchedLine: first };
|
|
228
|
-
}
|
|
229
|
-
if (uniqueCandidates.length === 1) {
|
|
230
|
-
return { ok: true, matchedLine: uniqueCandidates[0] };
|
|
231
|
-
}
|
|
232
209
|
if (!protocol.validLines.includes(first)) {
|
|
233
210
|
return {
|
|
234
211
|
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":
|
|
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" } } } } } },
|
|
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": { "
|
|
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 } } } } },
|
|
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"] } } } } } } }],
|
package/harness.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"workflowPolicy": {
|
|
7
7
|
"defaultImplementationWorkflow": "agent-dag",
|
|
8
8
|
"dag": {
|
|
9
|
-
|
|
9
|
+
"defaultEntry": "task advance",
|
|
10
10
|
"outputLanguage": "zh-CN",
|
|
11
11
|
"profileRouting": {
|
|
12
12
|
"minimal": "standard-dag",
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"executors": {
|
|
59
59
|
"pi": {
|
|
60
60
|
"description": "Pi planning, review, diagnosis, and bounded writing when DAG toolProfile=write",
|
|
61
|
-
"LOW":
|
|
62
|
-
"MED":
|
|
63
|
-
"HIGH":
|
|
61
|
+
"LOW": "minimax-m3",
|
|
62
|
+
"MED": "grok-4.5",
|
|
63
|
+
"HIGH": "gpt-5.6-sol"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
}
|