@tea-agent/loop-agent 0.25.4 → 0.25.5

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.
Files changed (35) hide show
  1. package/AGENTS.md +6 -0
  2. package/CHANGELOG.md +40 -0
  3. package/dist/commands/client-recovery.js +209 -62
  4. package/dist/executors/dag-pi-executor.js +80 -15
  5. package/dist/executors/model-routing.js +1 -1
  6. package/dist/executors/shell-executor.js +127 -0
  7. package/dist/executors/shell-write-guard.js +21 -7
  8. package/dist/worker/console/repo-fingerprint.js +7 -1
  9. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +964 -0
  10. package/dist/workflows/dag/backend-test-case-manifest.js +39 -1
  11. package/dist/workflows/dag/backend-test-markdown-workflow.js +219 -16
  12. package/dist/workflows/dag/convergence/controller.js +134 -9
  13. package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
  14. package/dist/workflows/dag/init-hybrid.js +262 -75
  15. package/dist/workflows/dag/node-execution.js +64 -11
  16. package/dist/workflows/dag/prompt.js +118 -4
  17. package/dist/workflows/dag/retry-policy.js +5 -4
  18. package/dist/workflows/dag/scheduler.js +32 -5
  19. package/dist/workflows/dag/types.js +7 -4
  20. package/dist/workflows/dag/validate.js +3 -2
  21. package/docs/architecture/dag-execution.md +7 -4
  22. package/docs/architecture/runtime-boundaries.md +1 -1
  23. package/docs/templates/agent-dag.base.json +1 -1
  24. package/docs/templates/agent-dag.final-verification.json +1 -1
  25. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  26. package/docs/templates/backend-test-dag.json +40 -13
  27. package/docs/templates/frontend-test-dag.json +32 -2
  28. package/docs/templates/hybrid-dag.json +1 -1
  29. package/examples/decision-gate-agent-dag.json +1 -1
  30. package/examples/example-dag.json +1 -1
  31. package/examples/hybrid-loop-agent-dag.json +1 -1
  32. package/harness.json +1 -1
  33. package/package.json +1 -1
  34. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  35. package/skills/loop-agent/references/model-routing.md +1 -1
@@ -10,6 +10,7 @@ import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend
10
10
  import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
11
11
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
12
12
  import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
13
+ import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
13
14
  import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
14
15
  import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
15
16
  import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
@@ -20,6 +21,7 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
20
21
  import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
21
22
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
22
23
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
24
+ import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
23
25
  import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
24
26
  import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
25
27
  import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
@@ -372,6 +374,14 @@ async function readRequiredRunReport(reportsDir, filename) {
372
374
  throw new Error(`missing required upstream report: reports/${filename}`);
373
375
  }
374
376
  }
377
+ async function readAdvisoryRunReport(reportsDir, filename, title) {
378
+ try {
379
+ return await readFile(path.join(reportsDir, filename), "utf8");
380
+ }
381
+ catch {
382
+ return `# ${title}\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Missing advisory upstream report: reports/${filename}\n`;
383
+ }
384
+ }
375
385
  async function executeBackendTestPipeline(input, meta) {
376
386
  const pipeline = input.task.shell?.backendTestPipeline;
377
387
  const started = Date.now();
@@ -423,6 +433,48 @@ async function executeBackendTestPipeline(input, meta) {
423
433
  durationMs: Date.now() - started,
424
434
  };
425
435
  }
436
+ const sourceBinding = meta.spec.sourceBinding;
437
+ if (!sourceBinding) {
438
+ const coverage = "# Backend Test Case Coverage Analysis\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Coverage analysis requires spec.sourceBinding; node 4 and node 7 fail closed on the same task/source binding contract.\n";
439
+ const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage);
440
+ outputs.push(`caseCoverage=${coveragePath}`, coverage);
441
+ return {
442
+ ok: false,
443
+ stdout: outputs.join("\n\n"),
444
+ stderr: "backend-test markdown-cases requires spec.sourceBinding",
445
+ failureCategory: "invalid-output",
446
+ durationMs: Date.now() - started,
447
+ };
448
+ }
449
+ const normalizedBinding = {
450
+ taskId: sourceBinding.taskId,
451
+ requirementPath: sourceBinding.sources.find((s) => s.kind === "requirement")?.path ?? "source/requirement.md",
452
+ requirementSha256: sourceBinding.sources.find((s) => s.kind === "requirement")?.sha256 ?? "0".repeat(64),
453
+ referencePaths: sourceBinding.sources.filter((s) => s.kind === "reference").map((s) => s.path),
454
+ requirementIds: sourceBinding.requirementIds,
455
+ };
456
+ try {
457
+ const coverage = await analyzeBackendTestCaseCoverage({ workspaceRoot: input.cwd, sourceBinding: normalizedBinding });
458
+ const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
459
+ const contractsDir = path.join(meta.runDir, "contracts");
460
+ await mkdir(contractsDir, { recursive: true });
461
+ const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
462
+ await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
463
+ outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
464
+ }
465
+ catch (error) {
466
+ const message = error instanceof Error ? error.message : String(error);
467
+ const coverage = `# Backend Test Case Coverage Analysis\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Coverage analysis crashed and is treated as an infrastructure failure: ${message}\n`;
468
+ const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage);
469
+ outputs.push(`caseCoverage=${coveragePath}`, coverage);
470
+ return {
471
+ ok: false,
472
+ stdout: outputs.join("\n\n"),
473
+ stderr: `backend-test coverage analysis crashed: ${message}`,
474
+ failureCategory: "invalid-output",
475
+ durationMs: Date.now() - started,
476
+ };
477
+ }
426
478
  }
427
479
  else if (pipeline === "markdown-traceability") {
428
480
  let report;
@@ -434,6 +486,60 @@ async function executeBackendTestPipeline(input, meta) {
434
486
  }
435
487
  const reportPath = await writeRunReport(meta.runDir, "backend-test-traceability.md", report);
436
488
  outputs.push(`traceability=${reportPath}`, report);
489
+ const sourceBinding = meta.spec.sourceBinding;
490
+ if (!sourceBinding) {
491
+ const correspondence = "# Backend Test Markdown → pytest Correspondence\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Correspondence analysis requires spec.sourceBinding; nodes 4, 6 and 7 fail closed on the same task/source binding contract.\n";
492
+ const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence);
493
+ outputs.push(`correspondence=${correspondencePath}`, correspondence);
494
+ return {
495
+ ok: false,
496
+ stdout: outputs.join("\n\n"),
497
+ stderr: "backend-test markdown-traceability requires spec.sourceBinding",
498
+ failureCategory: "invalid-output",
499
+ durationMs: Date.now() - started,
500
+ };
501
+ }
502
+ try {
503
+ const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({ workspaceRoot: input.cwd, taskId: sourceBinding.taskId });
504
+ const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
505
+ const contractsDir = path.join(meta.runDir, "contracts");
506
+ await mkdir(contractsDir, { recursive: true });
507
+ const factsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-facts.json");
508
+ await writeFile(factsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
509
+ outputs.push(`correspondence=${correspondencePath}`, `correspondenceFacts=${factsPath}`, correspondence.markdown);
510
+ }
511
+ catch (error) {
512
+ const message = error instanceof Error ? error.message : String(error);
513
+ const correspondence = `# Backend Test Markdown → pytest Correspondence\n\n## Status\n\nUNAVAILABLE\n\n## Findings\n\n- Correspondence analysis crashed and is treated as an infrastructure failure: ${message}\n`;
514
+ const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence);
515
+ outputs.push(`correspondence=${correspondencePath}`, correspondence);
516
+ return {
517
+ ok: false,
518
+ stdout: outputs.join("\n\n"),
519
+ stderr: `backend-test correspondence analysis crashed: ${message}`,
520
+ failureCategory: "invalid-output",
521
+ durationMs: Date.now() - started,
522
+ };
523
+ }
524
+ }
525
+ else if (pipeline === "markdown-manifest") {
526
+ const sourceBinding = meta.spec.sourceBinding;
527
+ if (!sourceBinding)
528
+ throw new Error("backend-test markdown-manifest requires spec.sourceBinding");
529
+ const manifest = await materializeBackendTestCaseManifestFromFacts({
530
+ runDir: meta.runDir,
531
+ workspaceRoot: input.cwd,
532
+ sourceBinding: {
533
+ taskId: sourceBinding.taskId,
534
+ requirementPath: sourceBinding.sources.find((s) => s.kind === "requirement")?.path ?? "source/requirement.md",
535
+ requirementSha256: sourceBinding.sources.find((s) => s.kind === "requirement")?.sha256 ?? "0".repeat(64),
536
+ referencePaths: sourceBinding.sources.filter((s) => s.kind === "reference").map((s) => s.path),
537
+ requirementIds: sourceBinding.requirementIds,
538
+ },
539
+ });
540
+ const manifestPath = path.join(meta.runDir, "contracts", "backend-test-case-manifest.json");
541
+ const summary = manifest.coverageSummary;
542
+ outputs.push(`manifest=${manifestPath}`, `materializationStatus=${manifest.materializationStatus ?? "available"}`, `coverageSummary.explicitAcCount=${summary?.explicitAcCount ?? "unavailable"}`, `coverageSummary.coveredAcCount=${summary?.coveredAcCount ?? "unavailable"}`, `coverageSummary.caseCount=${summary?.caseCount ?? "unavailable"}`, `coverageSummary.generatedCount=${summary?.generatedCount ?? "unavailable"}`, `ruleCoverageSummary.ruleCount=${manifest.ruleCoverageSummary?.ruleCount ?? "unavailable"}`, `correspondenceSummary.exactCorrespondenceCount=${manifest.correspondenceSummary?.exactCorrespondenceCount ?? "unavailable"}`);
437
543
  }
438
544
  else if (pipeline === "markdown-execute-html") {
439
545
  const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
@@ -601,14 +707,18 @@ async function executeBackendTestPipeline(input, meta) {
601
707
  });
602
708
  const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
603
709
  const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
710
+ const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
604
711
  const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
712
+ const correspondenceSummary = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
605
713
  const htmlContent = renderBackendTestHtml({
606
714
  title: meta.spec.title,
607
715
  parsed,
608
716
  cases,
609
717
  environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8"),
610
718
  caseValidationSummary,
719
+ caseCoverageSummary,
611
720
  traceabilitySummary,
721
+ correspondenceSummary,
612
722
  });
613
723
  const htmlPath = await writeRunReport(meta.runDir, "backend-test.html", htmlContent);
614
724
  const facts = renderBackendTestFacts({
@@ -618,7 +728,9 @@ async function executeBackendTestPipeline(input, meta) {
618
728
  htmlRelativePath: "reports/backend-test.html",
619
729
  htmlContent,
620
730
  caseValidationSummary,
731
+ caseCoverageSummary,
621
732
  traceabilitySummary,
733
+ correspondenceSummary,
622
734
  });
623
735
  const markdownPath = await writeRunReport(meta.runDir, "backend-test.md", facts);
624
736
  const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
@@ -681,7 +793,9 @@ async function executeBackendTestPipeline(input, meta) {
681
793
  parsed,
682
794
  metrics: l5Metrics,
683
795
  caseValidationSummary,
796
+ caseCoverageSummary,
684
797
  traceabilitySummary,
798
+ correspondenceSummary,
685
799
  failures: failureSummaries.length > 0 ? failureSummaries : undefined,
686
800
  });
687
801
  const l5Path = await writeRunReport(meta.runDir, "backend-test-l5-dashboard.html", l5Html);
@@ -1159,6 +1273,16 @@ async function executeFrontendTestCaseChecklist(input, meta) {
1159
1273
  return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
1160
1274
  }
1161
1275
  }
1276
+ async function executeFrontendTestL5Report(input, meta) {
1277
+ const started = Date.now();
1278
+ try {
1279
+ const output = await renderFrontendTestL5Report({ workspaceRoot: input.cwd, runDir: meta.runDir });
1280
+ return { ok: true, stdout: `Frontend L-5 report: ${output.htmlPath}\nMarkdown: ${output.markdownPath}\nStatus: ${output.metrics.status}`, stderr: "", failureCategory: "success", durationMs: Date.now() - started };
1281
+ }
1282
+ catch (error) {
1283
+ return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
1284
+ }
1285
+ }
1162
1286
  async function executeFrontendTestHtmlReport(input, meta) {
1163
1287
  const started = Date.now();
1164
1288
  try {
@@ -1307,6 +1431,9 @@ export async function executeDagShellNode(input, meta) {
1307
1431
  if (shell?.frontendTestEvidenceValidation) {
1308
1432
  return executeFrontendTestEvidenceValidation(input);
1309
1433
  }
1434
+ if (shell?.frontendTestL5Report) {
1435
+ return executeFrontendTestL5Report(input, meta);
1436
+ }
1310
1437
  if (shell?.frontendTestHtmlReport) {
1311
1438
  return executeFrontendTestHtmlReport(input, meta);
1312
1439
  }
@@ -165,8 +165,8 @@ export function validateShellWriteGuard(input) {
165
165
  return { ok: violations.length === 0, violations };
166
166
  }
167
167
  export async function readGitStatusPorcelain(cwd, options = {}) {
168
- const attempts = Math.max(1, options.attempts ?? 3);
169
- const retryDelayMs = Math.max(0, options.retryDelayMs ?? 100);
168
+ const attempts = Math.max(1, options.attempts ?? 5);
169
+ const retryDelayMs = Math.max(0, options.retryDelayMs ?? 150);
170
170
  let lastError;
171
171
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
172
172
  try {
@@ -179,14 +179,24 @@ export async function readGitStatusPorcelain(cwd, options = {}) {
179
179
  }
180
180
  }
181
181
  }
182
- throw lastError instanceof Error
183
- ? lastError
184
- : new Error(`git status failed after ${attempts} attempts`);
182
+ const detail = lastError instanceof Error ? lastError.message : String(lastError);
183
+ const exitCode = lastError instanceof Error &&
184
+ "exitCode" in lastError &&
185
+ typeof lastError.exitCode === "number"
186
+ ? String(lastError.exitCode)
187
+ : "unavailable";
188
+ const signal = lastError instanceof Error &&
189
+ "signal" in lastError &&
190
+ typeof lastError.signal === "string"
191
+ ? lastError.signal
192
+ : "unavailable";
193
+ throw new Error(`git status failed after ${attempts} attempts (exit code=${exitCode}, signal=${signal}, cwd=${path.resolve(cwd)}): ${detail}`, { cause: lastError });
185
194
  }
186
195
  function readGitStatusPorcelainOnce(cwd) {
187
196
  return new Promise((resolve, reject) => {
188
197
  const child = spawn("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
189
198
  cwd,
199
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
190
200
  stdio: ["ignore", "pipe", "pipe"],
191
201
  });
192
202
  let stdout = "";
@@ -200,12 +210,16 @@ function readGitStatusPorcelainOnce(cwd) {
200
210
  stderr += chunk;
201
211
  });
202
212
  child.on("error", reject);
203
- child.on("close", (code) => {
213
+ child.on("close", (code, signal) => {
204
214
  if (code === 0) {
205
215
  resolve(stdout);
206
216
  return;
207
217
  }
208
- reject(new Error(`git status failed: ${stderr.trim() || stdout.trim()}`));
218
+ const detail = stderr.trim() || stdout.trim() || "no stderr/stdout";
219
+ const error = new Error(`git status failed: ${detail}`);
220
+ error.exitCode = code;
221
+ error.signal = signal;
222
+ reject(error);
209
223
  });
210
224
  });
211
225
  }
@@ -7,7 +7,13 @@ import path from "node:path";
7
7
  * (except a bare root). No extra case-folding on macOS/Windows.
8
8
  */
9
9
  export function normalizeWorktreeRealpath(repoRoot) {
10
- let resolved = realpathSync(path.resolve(repoRoot));
10
+ // The legacy JS implementation preserves an input 8.3 path such as
11
+ // C:\\Users\\MICROS~1 on Windows. The native implementation expands the
12
+ // short and long aliases to the same physical path before hashing.
13
+ const absolute = path.resolve(repoRoot);
14
+ let resolved = process.platform === "win32"
15
+ ? realpathSync.native(absolute)
16
+ : realpathSync(absolute);
11
17
  if (/^[a-zA-Z]:/.test(resolved)) {
12
18
  resolved = resolved[0].toUpperCase() + resolved.slice(1);
13
19
  }