@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 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
- - 修复 backend-test collection assess 在 Markdown 已声明安全 mapped `test_*.py`、但 pytest writer 漏生成文件时直接以 `invalid-output` 阻断 repair 的问题:initial facts 升级为 `backend-test-pytest-collection-v2`,可记录 expected/existing/missing scripts、`collectionAttempted=false``missing-mapped-pytest-script`,复用既有单次 bounded repair 创建精确缺失脚本,并由 final collection、scope、安全和 hash freshness 门禁重新授权业务执行
22
+ - 修复后端测试在 Markdown 已声明安全映射脚本、但 pytest writer 漏生成文件时直接以 invalid-output 阻断 repair 的问题:initial facts 升级为 backend-test-pytest-collection-v2,可记录 expected/existing/missing scriptscollectionAttempted=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
- // The workspace diff is authoritative. The model outcome is advisory and
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: explicit blocked outcome; ${diagnostics}`,
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
- return {
589
- ok: true,
590
- outcome: changedFiles.length > 0 ? "changed" : "already-satisfied",
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.preprocess((value) => (value === "" || value === null ? "explicit activation boundary" : value), z.string().min(1)),
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
- // 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()),
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.preprocess((value) => (value === "" || value === null ? undefined : value), z.string().min(1).optional()),
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 === 0)
349
+ if (blocks.length !== 1)
449
350
  throw new Error("output must contain exactly one fenced json object");
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})`);
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, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, materializeFrontendImplementationContract, assertFrontendSourceBindingFresh, } from "./frontend-implementation-contract.js";
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
- 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;
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 artifactPath = path.join(input.runDir, input.config.outputDir, input.config.artifactName);
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 = await existingOpenspecCandidates(input.config.openspecCandidatePaths ?? [], workspaceRoot ?? process.cwd());
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
- // Frontend DAGs deliberately never generate lint verification. Existing
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-contract-json-pi",
2505
- planFallbackFromNodeIds: ["frontend-plan-revision-pi", "frontend-plan-pi"],
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 = await buildSupervisedHybridDag(standard, sources);
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
- async function buildSoftVerifyNode(sources) {
5724
+ function buildSoftVerifyNode(sources) {
5750
5725
  const implementId = implementationNodeId();
5751
5726
  const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
5752
- const fallbackCommands = sources.repoRoot
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
- async function buildSupervisedHybridDag(standard, sources) {
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
- await buildSoftVerifyNode(sources),
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": ["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" }] } } } } } },
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": { "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }, "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": { "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
- "defaultEntry": "task advance",
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": {"model": "deepseek/deepseek-v4-flash", "thinking": "high"},
62
- "MED": {"model": "deepseek/deepseek-v4-flash", "thinking": "max"},
63
- "HIGH": {"model": "deepseek/deepseek-v4-flash", "thinking": "max"}
61
+ "LOW": "minimax-m3",
62
+ "MED": "grok-4.5",
63
+ "HIGH": "gpt-5.6-sol"
64
64
  }
65
65
  }
66
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.27.1-beta.2",
3
+ "version": "0.27.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",