@tea-agent/loop-agent 0.26.4 → 0.26.5-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.
@@ -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 === "" ? "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: safePath.optional(),
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 === "" ? 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 === "" ? 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,61 @@ 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
+ };
346
398
  if (trimmed.startsWith("{") && trimmed.endsWith("}"))
347
- return JSON.parse(trimmed);
399
+ return parse(trimmed);
348
400
  const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
349
401
  if (blocks.length !== 1)
350
402
  throw new Error("output must contain exactly one fenced json object");
351
- return JSON.parse(blocks[0][1]);
403
+ return parse(blocks[0][1]);
352
404
  }
353
405
  /**
354
406
  * Build the authoritative frontend-implementation-contract sourceBinding from
@@ -46,7 +46,12 @@ function firstNonEmptyVerdictLine(text) {
46
46
  // The prompt requires VERDICT to be the first non-empty line, but model
47
47
  // output can still prepend a summary. Keep the protocol strict in the
48
48
  // prompt while making the deterministic gate resilient to that drift.
49
- return normalizedLines.find((line) => /^VERDICT:/.test(line)) ?? first;
49
+ const verdictLine = normalizedLines.find((line) => /^VERDICT:/.test(line)) ?? first;
50
+ if (/^VERDICT:\s*pass(?:\s*[.!?。!?]|\s+|$)/i.test(verdictLine))
51
+ return "VERDICT: pass";
52
+ if (/^VERDICT:\s*request-revision(?:\s*[.!?。!?]|\s+|$)/i.test(verdictLine))
53
+ return "VERDICT: request-revision";
54
+ return verdictLine;
50
55
  }
51
56
  function eventArgs(event) {
52
57
  return event.args ?? event.toolInput ?? event.input ?? {};
@@ -2455,9 +2455,38 @@ async function buildFrontendHybridDagFromTask(sources) {
2455
2455
  sourceContext,
2456
2456
  ].join("\n\n"),
2457
2457
  },
2458
+ {
2459
+ id: "frontend-contract-json-pi",
2460
+ depends_on: [
2461
+ "frontend-plan-revision-pi",
2462
+ "frontend-plan-pi",
2463
+ "frontend-final-design-review-pi",
2464
+ "frontend-design-review-pi",
2465
+ ],
2466
+ dependsPolicy: "all-or-condition-skip",
2467
+ role: "planner",
2468
+ executor: "pi",
2469
+ complexity: "MED",
2470
+ writePolicy: "read-only",
2471
+ outputMode: "structured-required",
2472
+ retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
2473
+ allowedPaths: readOnlyPaths,
2474
+ forbiddenPaths,
2475
+ skills: FRONTEND_IMPLEMENTATION_SKILLS,
2476
+ outputContract: "Return exactly one JSON object conforming to frontend-implementation-contract-v1. No Markdown, prose, comments, or code fences.",
2477
+ subtask_prompt: [
2478
+ "Convert the effective reviewed frontend plan into the canonical frontend-implementation-contract-v1 JSON.",
2479
+ "Use frontend-plan-revision-pi when it is FINISHED; otherwise use frontend-plan-pi. Confirm the effective design review passed before producing the contract.",
2480
+ "Return only the JSON object. Do not wrap it in Markdown or a code fence. Do not add explanatory text.",
2481
+ "Preserve all requirement expectedOutcome, interaction trigger/expectedBehavior, target files, verification targets, Mock/API decisions, and Real Integration Gap from the effective plan.",
2482
+ frontendContractSchemaBlock,
2483
+ sourceContext,
2484
+ ].join("\n\n"),
2485
+ },
2458
2486
  {
2459
2487
  id: "frontend-prewrite-gate-shell",
2460
2488
  depends_on: [
2489
+ "frontend-contract-json-pi",
2461
2490
  "frontend-final-design-review-pi",
2462
2491
  "frontend-design-review-pi",
2463
2492
  "frontend-plan-revision-pi",
@@ -2476,8 +2505,8 @@ async function buildFrontendHybridDagFromTask(sources) {
2476
2505
  commands: [],
2477
2506
  frontendPrewriteGate: {
2478
2507
  schemaVersion: 1,
2479
- planFromNodeId: "frontend-plan-revision-pi",
2480
- planFallbackFromNodeIds: ["frontend-plan-pi"],
2508
+ planFromNodeId: "frontend-contract-json-pi",
2509
+ planFallbackFromNodeIds: ["frontend-plan-revision-pi", "frontend-plan-pi"],
2481
2510
  reviewFromNodeId: "frontend-final-design-review-pi",
2482
2511
  reviewFallbackFromNodeIds: ["frontend-design-review-pi"],
2483
2512
  requiredRequirementIds: requirementIds,
@@ -5287,7 +5316,7 @@ async function buildHybridDagForTemplate(sources, template) {
5287
5316
  else if (template === "review-gated-dag")
5288
5317
  spec = buildReviewGatedHybridDag(standard, sources);
5289
5318
  else
5290
- spec = buildSupervisedHybridDag(standard, sources);
5319
+ spec = await buildSupervisedHybridDag(standard, sources);
5291
5320
  }
5292
5321
  applyProjectGovernanceReview(spec, template, sources);
5293
5322
  // New generate path always emits DagSpec v4 + bindings.
@@ -5721,10 +5750,12 @@ function buildWriteSetGateNode(sources) {
5721
5750
  },
5722
5751
  };
5723
5752
  }
5724
- function buildSoftVerifyNode(sources) {
5753
+ async function buildSoftVerifyNode(sources) {
5725
5754
  const implementId = implementationNodeId();
5726
5755
  const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
5727
- const fallbackCommands = ["npm run typecheck"];
5756
+ const fallbackCommands = sources.repoRoot
5757
+ ? (await discoverFrontendFallbackVerifyCommands(sources.repoRoot)).staticCommands
5758
+ : ["npm run typecheck"];
5728
5759
  const focusedIntermediate = sources.verifyCommands?.intermediate.filter((command) => !isFullSuiteVerifyCommand(command));
5729
5760
  const plannedIntermediate = applyMavenVerificationPlanning({
5730
5761
  repoRoot: sources.repoRoot,
@@ -5966,7 +5997,7 @@ function resolveSupervisedConvergence(taskConfig) {
5966
5997
  chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
5967
5998
  };
5968
5999
  }
5969
- function buildSupervisedHybridDag(standard, sources) {
6000
+ async function buildSupervisedHybridDag(standard, sources) {
5970
6001
  const contract = getTaskOrThrow(standard, "contract-pi");
5971
6002
  const scoutSrc = getTaskOrThrow(standard, "scout-src");
5972
6003
  const scoutTests = getTaskOrThrow(standard, "scout-tests");
@@ -6018,7 +6049,7 @@ function buildSupervisedHybridDag(standard, sources) {
6018
6049
  "final-write-set-audit-format-repair-pi",
6019
6050
  ],
6020
6051
  }),
6021
- buildSoftVerifyNode(sources),
6052
+ await buildSoftVerifyNode(sources),
6022
6053
  buildProcessSupervisorNode(sources),
6023
6054
  buildProcessGateNode(sources),
6024
6055
  buildRepairNode(sources),
package/harness.json CHANGED
@@ -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": "minimax-m3",
62
- "MED": "grok-4.5",
63
- "HIGH": "gpt-5.6-sol"
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"}
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.26.4",
3
+ "version": "0.26.5-beta.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",