@tea-agent/loop-agent 0.26.5-beta.0 → 0.26.5-beta.2

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.
@@ -346,12 +346,75 @@ function secretIssues(value, at = "$", issues = []) {
346
346
  }
347
347
  export function extractFrontendImplementationJson(text) {
348
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
+ };
349
398
  if (trimmed.startsWith("{") && trimmed.endsWith("}"))
350
- return JSON.parse(trimmed);
399
+ return parse(trimmed);
351
400
  const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
352
- if (blocks.length !== 1)
401
+ if (blocks.length === 0)
353
402
  throw new Error("output must contain exactly one fenced json object");
354
- return JSON.parse(blocks[0][1]);
403
+ const candidates = [];
404
+ for (const block of blocks) {
405
+ try {
406
+ const candidate = parse(block[1]);
407
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate))
408
+ candidates.push(candidate);
409
+ }
410
+ catch {
411
+ // Ignore incomplete model scratch blocks. A later complete contract
412
+ // block may still be deterministically recoverable.
413
+ }
414
+ }
415
+ if (candidates.length !== 1)
416
+ throw new Error(`output must contain exactly one valid fenced json object (found ${candidates.length})`);
417
+ return candidates[0];
355
418
  }
356
419
  /**
357
420
  * 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) {
@@ -160,13 +161,20 @@ export async function runFrontendPrewriteGate(input) {
160
161
  if (missingIds.length > 0) {
161
162
  throw new Error(`frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`);
162
163
  }
163
- const artifact = await materializeFrontendImplementationContract({
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({
164
172
  runDir: input.runDir,
165
173
  fromNodeId: planNodeId,
166
174
  artifactName: input.config.artifactName,
167
175
  outputDir: input.config.outputDir,
168
176
  sourceBinding: input.sourceBinding,
169
- });
177
+ }));
170
178
  const raw = JSON.parse(await readFile(artifact.path, "utf8"));
171
179
  const contract = frontendImplementationContractSchema.parse(raw);
172
180
  if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
@@ -2455,9 +2455,62 @@ 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
+ },
2486
+ {
2487
+ id: "frontend-contract-json-validate-shell",
2488
+ depends_on: ["frontend-contract-json-pi"],
2489
+ role: "verifier",
2490
+ executor: "shell",
2491
+ complexity: "LOW",
2492
+ writePolicy: "read-only",
2493
+ allowedPaths: readOnlyPaths,
2494
+ forbiddenPaths,
2495
+ outputContract: "Validated frontend implementation contract artifact with schema ID and SHA-256.",
2496
+ subtask_prompt: "Materialize and validate the structured frontend contract before prewrite authorization.",
2497
+ shell: {
2498
+ commands: [],
2499
+ jsonArtifactGate: {
2500
+ fromNodeId: "frontend-contract-json-pi",
2501
+ schemaId: "frontend-implementation-contract-v1",
2502
+ artifactName: "frontend-implementation-contract.json",
2503
+ outputDir: "contracts",
2504
+ },
2505
+ cwd: ".",
2506
+ timeoutMs: 60000,
2507
+ },
2508
+ },
2458
2509
  {
2459
2510
  id: "frontend-prewrite-gate-shell",
2460
2511
  depends_on: [
2512
+ "frontend-contract-json-pi",
2513
+ "frontend-contract-json-validate-shell",
2461
2514
  "frontend-final-design-review-pi",
2462
2515
  "frontend-design-review-pi",
2463
2516
  "frontend-plan-revision-pi",
@@ -2476,8 +2529,8 @@ async function buildFrontendHybridDagFromTask(sources) {
2476
2529
  commands: [],
2477
2530
  frontendPrewriteGate: {
2478
2531
  schemaVersion: 1,
2479
- planFromNodeId: "frontend-plan-revision-pi",
2480
- planFallbackFromNodeIds: ["frontend-plan-pi"],
2532
+ planFromNodeId: "frontend-contract-json-pi",
2533
+ planFallbackFromNodeIds: ["frontend-plan-revision-pi", "frontend-plan-pi"],
2481
2534
  reviewFromNodeId: "frontend-final-design-review-pi",
2482
2535
  reviewFallbackFromNodeIds: ["frontend-design-review-pi"],
2483
2536
  requiredRequirementIds: requirementIds,
@@ -5287,7 +5340,7 @@ async function buildHybridDagForTemplate(sources, template) {
5287
5340
  else if (template === "review-gated-dag")
5288
5341
  spec = buildReviewGatedHybridDag(standard, sources);
5289
5342
  else
5290
- spec = buildSupervisedHybridDag(standard, sources);
5343
+ spec = await buildSupervisedHybridDag(standard, sources);
5291
5344
  }
5292
5345
  applyProjectGovernanceReview(spec, template, sources);
5293
5346
  // New generate path always emits DagSpec v4 + bindings.
@@ -5721,10 +5774,12 @@ function buildWriteSetGateNode(sources) {
5721
5774
  },
5722
5775
  };
5723
5776
  }
5724
- function buildSoftVerifyNode(sources) {
5777
+ async function buildSoftVerifyNode(sources) {
5725
5778
  const implementId = implementationNodeId();
5726
5779
  const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
5727
- const fallbackCommands = ["npm run typecheck"];
5780
+ const fallbackCommands = sources.repoRoot
5781
+ ? (await discoverFrontendFallbackVerifyCommands(sources.repoRoot)).staticCommands
5782
+ : ["npm run typecheck"];
5728
5783
  const focusedIntermediate = sources.verifyCommands?.intermediate.filter((command) => !isFullSuiteVerifyCommand(command));
5729
5784
  const plannedIntermediate = applyMavenVerificationPlanning({
5730
5785
  repoRoot: sources.repoRoot,
@@ -5966,7 +6021,7 @@ function resolveSupervisedConvergence(taskConfig) {
5966
6021
  chainNodeIds: [...SUPERVISED_CONVERGENCE_CHAIN_NODE_IDS],
5967
6022
  };
5968
6023
  }
5969
- function buildSupervisedHybridDag(standard, sources) {
6024
+ async function buildSupervisedHybridDag(standard, sources) {
5970
6025
  const contract = getTaskOrThrow(standard, "contract-pi");
5971
6026
  const scoutSrc = getTaskOrThrow(standard, "scout-src");
5972
6027
  const scoutTests = getTaskOrThrow(standard, "scout-tests");
@@ -6018,7 +6073,7 @@ function buildSupervisedHybridDag(standard, sources) {
6018
6073
  "final-write-set-audit-format-repair-pi",
6019
6074
  ],
6020
6075
  }),
6021
- buildSoftVerifyNode(sources),
6076
+ await buildSoftVerifyNode(sources),
6022
6077
  buildProcessSupervisorNode(sources),
6023
6078
  buildProcessGateNode(sources),
6024
6079
  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.5-beta.0",
3
+ "version": "0.26.5-beta.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",