@tea-agent/loop-agent 0.39.0-next.27 → 0.39.0-next.29

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 (117) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +150 -96
  3. package/README.md +8 -4
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +16 -5
  6. package/dist/cli/program.js +16 -2
  7. package/dist/cli/update/notifier.js +2 -2
  8. package/dist/cli.js +2 -1
  9. package/dist/commands/init-model-catalog.js +464 -0
  10. package/dist/commands/init-upgrade.js +265 -97
  11. package/dist/commands/init.js +428 -27
  12. package/dist/commands/inspect-next.js +8 -0
  13. package/dist/executors/dag-pi-executor.js +18 -9
  14. package/dist/executors/pi-playwright-cli-tool.js +8 -1
  15. package/dist/executors/shell-executor.js +209 -13
  16. package/dist/shared/operator/capabilities.js +7 -0
  17. package/dist/shared/update/console-notifier.js +100 -0
  18. package/dist/{cli → shared}/update/npm-client.js +41 -8
  19. package/dist/{cli → shared}/update/state.js +41 -13
  20. package/dist/worker/cli.js +31 -1
  21. package/dist/worker/console/chat/routes.js +1 -0
  22. package/dist/worker/console/operator-user-error.js +1 -1
  23. package/dist/worker/console/routes.js +3 -0
  24. package/dist/worker/console/security.js +53 -5
  25. package/dist/worker/console/server.js +10 -4
  26. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-DGn9iNrd.js} +1 -1
  27. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-Dc0C2Nyv.js} +1 -1
  28. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-COnz_Bhe.js} +1 -1
  29. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-VhIP6vgf.js} +1 -1
  30. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-D74lpmlb.js} +1 -1
  31. package/dist/worker/console/static/assets/channel-DWcZjWB3.js +1 -0
  32. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BxmiogQE.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-C00Oi-2I.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DTrT1Vze.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DdKW7-fL.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-D0q1WN6w.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DBa14MSI.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-DkP-pEox.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-lgPogpcX.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-D-apZer0.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-B1T2yAIe.js} +1 -1
  42. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-IjoI3dqU.js +1 -0
  43. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-IjoI3dqU.js +1 -0
  44. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-0P6hi3jF.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-CntzMSR8.js} +1 -1
  46. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-C9zmXJtG.js} +1 -1
  47. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-DfBCZqZw.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-DLp-bhGL.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BksDcwZm.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-Bdag7DYP.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CoY5uNbk.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-KcgrDXHt.js} +1 -1
  53. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CikErwJb.js} +1 -1
  54. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DqmwwzvZ.js} +1 -1
  55. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-35krh9WG.js} +1 -1
  56. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-CvTF1gJ4.js} +1 -1
  57. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BiOWWClJ.js} +1 -1
  58. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-CqWlWE4l.js} +3 -3
  59. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Crha5dcA.js} +1 -1
  60. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DEtbscOZ.js} +1 -1
  61. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-D2YYzx3z.js} +1 -1
  62. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-C7x18ttQ.js} +1 -1
  63. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-CCLZkz45.js} +1 -1
  64. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-Dmy4lSGv.js} +5 -5
  65. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CmvIol59.js} +1 -1
  66. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-DVpEdirN.js} +1 -1
  67. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-cKQy456v.js} +1 -1
  68. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-bx06I38m.js} +1 -1
  69. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-BjfIVIJr.js} +1 -1
  70. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-DcuPGbKM.js} +1 -1
  71. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-DvgVeDoV.js} +1 -1
  72. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-DLBG9nm-.js} +1 -1
  73. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-D33L7FBF.js} +1 -1
  74. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-hvqD9TpS.js} +1 -1
  75. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C6MII0D1.js +1 -0
  76. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-BtjH81Mz.js} +2 -2
  77. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D_tk0V8D.js +8 -0
  78. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-CJ47Kraf.js} +1 -1
  79. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-COVPl-wh.js} +1 -1
  80. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-CCoQJLUY.js} +1 -1
  81. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-DTWJvfAd.js} +1 -1
  82. package/dist/worker/console/static/index.html +1 -1
  83. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  84. package/dist/worker/observe/node-input.js +2 -0
  85. package/dist/worker/observe/node-process.js +377 -0
  86. package/dist/worker/observe/routes.js +24 -0
  87. package/dist/worker/observe/shell-handler-keys.js +34 -0
  88. package/dist/worker/observe/static/api.js +8 -0
  89. package/dist/worker/observe/static/constants.js +8 -1
  90. package/dist/worker/observe/static/format.js +23 -0
  91. package/dist/worker/observe/static/markdown-render.js +20 -1
  92. package/dist/worker/observe/static/operator-chrome.js +4 -0
  93. package/dist/worker/observe/static/state.js +11 -0
  94. package/dist/worker/observe/static/styles.css +3 -0
  95. package/dist/worker/observe/static/views/dag-inspector.js +247 -7
  96. package/dist/worker/observe/static/views/dag.js +8 -1
  97. package/dist/worker/observe/static/views/session-timeline.js +60 -4
  98. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  99. package/dist/workflows/dag/failure-category.js +8 -0
  100. package/dist/workflows/dag/frontend-implementation-contract.js +5 -1
  101. package/dist/workflows/dag/init-hybrid.js +35 -10
  102. package/dist/workflows/dag/node-execution.js +20 -2
  103. package/dist/workflows/dag/retry-policy.js +91 -0
  104. package/dist/workflows/dag/types.js +9 -1
  105. package/docs/init-surface.manifest.json +5 -0
  106. package/docs/operations/README.md +1 -1
  107. package/docs/templates/init-managed-agents.md +13 -3
  108. package/package.json +5 -1
  109. package/scripts/next-info.mjs +356 -0
  110. package/scripts/next-publish-gate.mjs +238 -0
  111. package/scripts/release-source-binding.mjs +251 -0
  112. package/skills/loop-agent/references/command-reference.md +11 -4
  113. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  114. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  115. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  116. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -0,0 +1,8 @@
1
+ import { collectNextInfo, formatNextInfo, } from "../../scripts/next-info.mjs";
2
+ export async function runInspectNext(repoRoot, args = []) {
3
+ const json = args.includes("--json");
4
+ const result = collectNextInfo({ cwd: repoRoot });
5
+ process.stdout.write(formatNextInfo(result, { json }));
6
+ if (!result.ok)
7
+ process.exitCode = result.exitCode || 1;
8
+ }
@@ -9,7 +9,7 @@ import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND
9
9
  import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
10
10
  import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
11
11
  import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
12
- import { isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
12
+ import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
13
13
  import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
14
14
  import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
15
15
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
@@ -942,22 +942,31 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
942
942
  if (meta.writeGuardAttribution === "best-effort") {
943
943
  stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
944
944
  }
945
- // Pure provider stall/timeout with zero workspace side effects is eligible
946
- // for a single writer transport retry (implement-pi flake resilience).
947
- // Partial writes, tool calls, or write-guard failures keep raw `timeout`.
948
- const writerCleanTimeout = !mapped.ok &&
949
- mapped.failureCategory === "timeout" &&
950
- writeGuardOk &&
945
+ const rawFailureCategory = mapped.failureCategory;
946
+ const writeToolCallCount = readWriterThinkingExhaustionEvidence(result).writeToolCallCount;
947
+ const zeroSideEffects = writeGuardOk &&
951
948
  !completenessFailure &&
952
949
  !writerThinkingExhausted &&
953
- isWriterTransportRetryCandidate(input.task) &&
950
+ !writerOutcomeViolation &&
954
951
  changeManifestChangedFiles !== undefined &&
955
952
  changeManifestChangedFiles.length === 0 &&
956
- readWriterThinkingExhaustionEvidence(result).writeToolCallCount === 0;
953
+ writeToolCallCount === 0;
954
+ const targetCleanTransport = !mapped.ok &&
955
+ isTargetTemplateTransientRetryNode(input.task) &&
956
+ rawFailureCategory !== undefined &&
957
+ ["timeout", "network", "rate-limit", "unavailable"].includes(rawFailureCategory) &&
958
+ zeroSideEffects;
959
+ const frontendCleanTimeout = !mapped.ok &&
960
+ rawFailureCategory === "timeout" &&
961
+ isWriterTransportRetryCandidate(input.task) &&
962
+ !isTargetTemplateTransientRetryNode(input.task) &&
963
+ zeroSideEffects;
964
+ const writerCleanTimeout = targetCleanTransport || frontendCleanTimeout;
957
965
  return {
958
966
  ...mapped,
959
967
  ok: false,
960
968
  stderr: stderrParts.filter(Boolean).join("\n\n"),
969
+ ...(rawFailureCategory ? { rawFailureCategory } : {}),
961
970
  failureCategory: mapped.ok
962
971
  ? writeGuardOk
963
972
  ? completenessFailure
@@ -111,6 +111,13 @@ export async function readPlaywrightCliReceipts(runDir, nodeId) {
111
111
  }
112
112
  }
113
113
  export function summarizePlaywrightCliReceipts(receipts) {
114
+ // Sequence numbers are assigned at dispatch time, which is the controller's
115
+ // causal order. Receipt lines are appended when each command COMPLETES, so
116
+ // concurrent commands (e.g. parallel find calls issued in one turn) persist
117
+ // out of sequence order. Judge chain contiguity on the dispatch order by
118
+ // sorting on `sequence` first; a genuinely missing receipt (gap) or a
119
+ // duplicated sequence still fails the contiguity check below.
120
+ const orderedReceipts = [...receipts].sort((a, b) => a.sequence - b.sequence);
114
121
  let hasSuccessfulOpen = false;
115
122
  let hasInteractionOrAssertion = false;
116
123
  let hasMeaningfulAssertion = false;
@@ -122,7 +129,7 @@ export function summarizePlaywrightCliReceipts(receipts) {
122
129
  let hasContiguousControllerSequence = true;
123
130
  let previousSequence = 0;
124
131
  const caseIds = new Set();
125
- for (const receipt of receipts) {
132
+ for (const receipt of orderedReceipts) {
126
133
  caseIds.add(receipt.caseId);
127
134
  const succeeded = receipt.exitCode === 0 && !receipt.timedOut;
128
135
  const ordered = receipt.sequence === previousSequence + 1;
@@ -123,6 +123,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
123
123
  const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
124
124
  const SUMMARY_STDOUT_MAX = 4_000;
125
125
  const SUMMARY_STDERR_MAX = 2_000;
126
+ const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
127
+ const TEST_TIMEOUT_MARKERS = [
128
+ /Test timed out in \d+ms/i,
129
+ /Timeout of \d+ms exceeded/i,
130
+ ];
131
+ const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
132
+ /AssertionError/i,
133
+ /expected:?\s/i,
134
+ /received:?\s/i,
135
+ /Snapshot /i,
136
+ /toMatchSnapshot/i,
137
+ /SyntaxError/i,
138
+ /TS\d{4}/,
139
+ /error TS/i,
140
+ /eslint/i,
141
+ /compile error/i,
142
+ /Failed to compile/i,
143
+ ];
144
+ function isIdentifiedTestCommand(command) {
145
+ return /(?:^|[\s"'`])(?:npx\s+)?vitest(?:\s|$)|(?:^|[\s"'`])npm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])pnpm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])yarn(?:\.cmd)?\s+test(?:\s|$)/i.test(command);
146
+ }
147
+ function classifyShellCommandFailure(input) {
148
+ if (input.processCategory !== "nonzero-exit")
149
+ return input.processCategory;
150
+ if (!isIdentifiedTestCommand(input.command))
151
+ return "nonzero-exit";
152
+ const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
153
+ const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
154
+ if (!hasTimeoutMarker)
155
+ return "nonzero-exit";
156
+ const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
157
+ return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
158
+ }
126
159
  const NODE_STDOUT_MAX = 12_000;
127
160
  const NODE_STDERR_MAX = 8_000;
128
161
  const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
@@ -329,16 +362,21 @@ export async function executeShellCommand(input) {
329
362
  });
330
363
  });
331
364
  child.on("close", (code) => {
332
- let failureCategory = "nonzero-exit";
365
+ let processCategory = "nonzero-exit";
333
366
  if (timedOut) {
334
- failureCategory = "timeout";
367
+ processCategory = "timeout";
335
368
  }
336
369
  else if (code === 0) {
337
- failureCategory = "success";
370
+ processCategory = "success";
338
371
  }
339
372
  finish({
340
373
  exitCode: code,
341
- failureCategory,
374
+ failureCategory: classifyShellCommandFailure({
375
+ command: input.command,
376
+ processCategory,
377
+ stdout: formatBoundedOutput(stdout),
378
+ stderr: formatBoundedOutput(stderr),
379
+ }),
342
380
  ok: code === 0 && !timedOut,
343
381
  });
344
382
  });
@@ -2348,7 +2386,76 @@ async function executeFrontendLintBaseline(input, meta) {
2348
2386
  };
2349
2387
  }
2350
2388
  }
2351
- export async function executeDagShellNode(input, meta) {
2389
+ async function writeShellPresetProcessReceipt(input) {
2390
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
2391
+ schemaVersion: 1,
2392
+ kind: "shell-preset",
2393
+ preset: input.preset,
2394
+ ok: input.ok,
2395
+ durationMs: input.durationMs,
2396
+ steps: input.steps,
2397
+ });
2398
+ }
2399
+ async function ensureShellPresetProcessReceipt(input) {
2400
+ const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
2401
+ if (existsSync(receiptPath))
2402
+ return;
2403
+ if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
2404
+ return;
2405
+ }
2406
+ const detail = input.result.ok ? input.result.stdout : input.result.stderr;
2407
+ await writeShellPresetProcessReceipt({
2408
+ runDir: input.runDir,
2409
+ nodeId: input.nodeId,
2410
+ preset: input.preset,
2411
+ ok: input.result.ok,
2412
+ durationMs: input.result.durationMs,
2413
+ steps: [
2414
+ {
2415
+ id: "run",
2416
+ label: input.preset,
2417
+ ok: input.result.ok,
2418
+ detail: detail?.slice(0, 8 * 1024),
2419
+ durationMs: input.result.durationMs,
2420
+ },
2421
+ ],
2422
+ });
2423
+ }
2424
+ function namedShellHandler(shell) {
2425
+ if (!shell)
2426
+ return null;
2427
+ const record = shell;
2428
+ for (const key of [
2429
+ "finalWriteSetApprovalGate",
2430
+ "frontendTestStandardScenarios",
2431
+ "frontendTestEnvironmentProbe",
2432
+ "frontendBrowserToolPreflight",
2433
+ "frontendPrewriteGate",
2434
+ "frontendLintBaseline",
2435
+ "frontendVerificationBundle",
2436
+ "frontendReviewContext",
2437
+ "frontendTestCaseChecklist",
2438
+ "frontendTestCaseManifest",
2439
+ "frontendTestEvidenceValidation",
2440
+ "frontendTestResultFinalize",
2441
+ "frontendTestL5Report",
2442
+ "frontendTestHtmlReport",
2443
+ "frontendTestReports",
2444
+ "backendTestPipeline",
2445
+ "jsonArtifactGate",
2446
+ "projectGovernanceGate",
2447
+ "requirementCoverageGate",
2448
+ "verdictGate",
2449
+ "verifyEvidence",
2450
+ "repairArtifactGate",
2451
+ "jacocoCoverage",
2452
+ ]) {
2453
+ if (record[key] != null)
2454
+ return key;
2455
+ }
2456
+ return null;
2457
+ }
2458
+ async function executeDagShellNodeInner(input, meta) {
2352
2459
  const shell = input.task.shell;
2353
2460
  if (shell?.finalWriteSetApprovalGate) {
2354
2461
  const started = Date.now();
@@ -2395,28 +2502,81 @@ export async function executeDagShellNode(input, meta) {
2395
2502
  }
2396
2503
  if (shell?.frontendBrowserToolPreflight) {
2397
2504
  const started = Date.now();
2505
+ const steps = [];
2506
+ const finish = async (result) => {
2507
+ await writeShellPresetProcessReceipt({
2508
+ runDir: meta.runDir,
2509
+ nodeId: input.task.id,
2510
+ preset: "frontendBrowserToolPreflight",
2511
+ ok: result.ok,
2512
+ durationMs: result.durationMs,
2513
+ steps: steps.length > 0
2514
+ ? steps
2515
+ : [
2516
+ {
2517
+ id: "run",
2518
+ label: "浏览器工具能力探测",
2519
+ ok: result.ok,
2520
+ detail: result.ok ? result.stdout : result.stderr,
2521
+ durationMs: result.durationMs,
2522
+ },
2523
+ ],
2524
+ });
2525
+ return result;
2526
+ };
2398
2527
  try {
2399
2528
  if (resolvePiBackend() === "cli-only") {
2529
+ steps.push({
2530
+ id: "pi-backend",
2531
+ label: "Pi SDK 后端可用",
2532
+ ok: false,
2533
+ detail: "CODE_AGENT_PI_BACKEND=cli-only",
2534
+ });
2400
2535
  throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
2401
2536
  }
2537
+ steps.push({
2538
+ id: "pi-backend",
2539
+ label: "Pi SDK 后端可用",
2540
+ ok: true,
2541
+ });
2402
2542
  const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
2403
2543
  if (!sdkCapability.ok) {
2544
+ steps.push({
2545
+ id: "sdk-custom-tool",
2546
+ label: "Pi SDK custom-tool 能力",
2547
+ ok: false,
2548
+ detail: sdkCapability.detail,
2549
+ });
2404
2550
  throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
2405
2551
  }
2552
+ steps.push({
2553
+ id: "sdk-custom-tool",
2554
+ label: "Pi SDK custom-tool 能力",
2555
+ ok: true,
2556
+ });
2406
2557
  const toolProbe = await createPlaywrightCliTool({
2407
2558
  repoRoot: input.cwd,
2408
2559
  runDir: meta.runDir,
2409
2560
  nodeId: input.task.id,
2410
2561
  caseId: "frontend-browser-preflight",
2411
2562
  evidenceDir: "testcase/frontend/evidence/frontend-browser-preflight",
2412
- // Capability-only registration probe. The tested URL is selected later by
2413
- // retrieve-frontend-test-context-pi and validated by the environment shell.
2414
2563
  baseUrl: "http://localhost:5173/",
2415
2564
  inputRoot: "testcase/frontend/fixtures",
2416
2565
  });
2417
2566
  if (toolProbe.name !== "playwright_cli") {
2567
+ steps.push({
2568
+ id: "structured-tool",
2569
+ label: "注册 playwright_cli",
2570
+ ok: false,
2571
+ detail: toolProbe.name,
2572
+ });
2418
2573
  throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
2419
2574
  }
2575
+ steps.push({
2576
+ id: "structured-tool",
2577
+ label: "注册 playwright_cli",
2578
+ ok: true,
2579
+ });
2420
2580
  const launcher = resolvePlaywrightCliLauncher(input.cwd);
2421
2581
  const result = await defaultPlaywrightCliCommandRunner({
2422
2582
  executable: launcher.executable,
@@ -2429,8 +2589,19 @@ export async function executeDagShellNode(input, meta) {
2429
2589
  if (result.exitCode !== 0 ||
2430
2590
  result.timedOut ||
2431
2591
  !["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
2592
+ steps.push({
2593
+ id: "playwright-cli-help",
2594
+ label: "playwright-cli --help 合同",
2595
+ ok: false,
2596
+ detail: help.slice(0, 8 * 1024),
2597
+ });
2432
2598
  throw new Error("playwright-cli-contract-incompatible");
2433
2599
  }
2600
+ steps.push({
2601
+ id: "playwright-cli-help",
2602
+ label: "playwright-cli --help 合同",
2603
+ ok: true,
2604
+ });
2434
2605
  const capability = {
2435
2606
  schemaVersion: 1,
2436
2607
  status: "ready",
@@ -2441,22 +2612,28 @@ export async function executeDagShellNode(input, meta) {
2441
2612
  targetUrlAuthority: "retrieve-context-and-environment-probe",
2442
2613
  };
2443
2614
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
2444
- return {
2615
+ steps.push({
2616
+ id: "capability-artifact",
2617
+ label: "写出 frontend-browser-capability.json",
2618
+ ok: true,
2619
+ detail: JSON.stringify(capability, null, 2),
2620
+ });
2621
+ return await finish({
2445
2622
  ok: true,
2446
2623
  stdout: JSON.stringify(capability),
2447
2624
  stderr: "",
2448
2625
  failureCategory: "success",
2449
2626
  durationMs: Date.now() - started,
2450
- };
2627
+ });
2451
2628
  }
2452
2629
  catch (error) {
2453
- return {
2630
+ return await finish({
2454
2631
  ok: false,
2455
2632
  stdout: "",
2456
2633
  stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
2457
2634
  failureCategory: "tool-policy",
2458
2635
  durationMs: Date.now() - started,
2459
- };
2636
+ });
2460
2637
  }
2461
2638
  }
2462
2639
  if (shell?.frontendPrewriteGate) {
@@ -2878,6 +3055,7 @@ export async function executeDagShellNode(input, meta) {
2878
3055
  const cwd = resolveShellCwd(input.cwd, shell.cwd);
2879
3056
  const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
2880
3057
  const failFast = shell.failFast !== false;
3058
+ const attemptNumber = input.attempt ?? 1;
2881
3059
  const results = [];
2882
3060
  const executedCommandKeys = new Set();
2883
3061
  let beforeStatus;
@@ -2973,8 +3151,8 @@ export async function executeDagShellNode(input, meta) {
2973
3151
  dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2974
3152
  reportActivity: input.reportActivity,
2975
3153
  outputArtifacts: {
2976
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2977
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
3154
+ stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
3155
+ stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
2978
3156
  },
2979
3157
  });
2980
3158
  results.push(result);
@@ -3033,6 +3211,7 @@ export async function executeDagShellNode(input, meta) {
3033
3211
  schemaVersion: 1,
3034
3212
  runId: meta.runId,
3035
3213
  nodeId: input.task.id,
3214
+ attempt: attemptNumber,
3036
3215
  workspaceFingerprint: workspaceFingerprint ?? null,
3037
3216
  failFast,
3038
3217
  commands: results.map((result) => ({
@@ -3051,6 +3230,7 @@ export async function executeDagShellNode(input, meta) {
3051
3230
  subchecks: parseCheckRepoSubcheckDurations(result),
3052
3231
  })),
3053
3232
  };
3233
+ await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
3054
3234
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
3055
3235
  const summaryMarkdown = buildShellResultSummaryMarkdown({
3056
3236
  nodeId: input.task.id,
@@ -3102,8 +3282,24 @@ export async function executeDagShellNode(input, meta) {
3102
3282
  stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
3103
3283
  stderr: stderrParts.filter(Boolean).join("\n\n"),
3104
3284
  failureCategory,
3285
+ ...(firstFailure?.failureCategory
3286
+ ? { rawFailureCategory: firstFailure.failureCategory }
3287
+ : {}),
3105
3288
  durationMs: Date.now() - started,
3106
3289
  // extra field consumed by node-execution if supported; also embedded in stdout marker
3107
3290
  ...{ commandResults },
3108
3291
  };
3109
3292
  }
3293
+ export async function executeDagShellNode(input, meta) {
3294
+ const handler = namedShellHandler(input.task.shell);
3295
+ const result = await executeDagShellNodeInner(input, meta);
3296
+ if (handler) {
3297
+ await ensureShellPresetProcessReceipt({
3298
+ runDir: meta.runDir,
3299
+ nodeId: input.task.id,
3300
+ preset: handler,
3301
+ result,
3302
+ });
3303
+ }
3304
+ return result;
3305
+ }
@@ -1822,6 +1822,13 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
1822
1822
  source: "loop-agent",
1823
1823
  exclusionReason: "Not exposed to Operator Chat in M5.",
1824
1824
  },
1825
+ {
1826
+ command: "loop-agent init model-catalog",
1827
+ coverage: "excluded",
1828
+ action: null,
1829
+ source: "loop-agent",
1830
+ exclusionReason: "Not exposed to Operator Chat in M5.",
1831
+ },
1825
1832
  {
1826
1833
  command: "loop-agent init doctor",
1827
1834
  coverage: "excluded",
@@ -0,0 +1,100 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import semver from "semver";
3
+ import { acquireUpdateLock, readUpdateState, updateLockPath, updateStatePath, writeUpdateState, } from "./state.js";
4
+ export async function runConsoleDailyUpdateCheck(options) {
5
+ void options.writeStdout;
6
+ void options.packageRoot;
7
+ void options.cwd;
8
+ const writeStderr = options.writeStderr ?? ((chunk) => process.stderr.write(chunk));
9
+ const now = options.now ?? (() => new Date());
10
+ try {
11
+ if (!canPromptInteractively(options))
12
+ return;
13
+ const statePath = updateStatePath(options.homeDir);
14
+ const today = formatLocalDate(now());
15
+ const existing = await readUpdateState(statePath);
16
+ if (existing.lastPromptedLocalDate === today)
17
+ return;
18
+ const release = await acquireUpdateLock(updateLockPath(options.homeDir), {
19
+ now,
20
+ });
21
+ if (!release)
22
+ return;
23
+ try {
24
+ const state = await readUpdateState(statePath);
25
+ if (state.lastPromptedLocalDate === today)
26
+ return;
27
+ const check = await options.client.latestVersionCheck();
28
+ if (!check.ok)
29
+ return;
30
+ if (!semver.gt(check.version, options.currentVersion))
31
+ return;
32
+ const parsedLatest = semver.parse(check.version);
33
+ if (!parsedLatest || parsedLatest.prerelease.length > 0)
34
+ return;
35
+ await markPromptedToday(statePath, state, today);
36
+ const answer = await ask(options, `loop-agent 有新版本:${options.currentVersion} → ${check.version}\n现在更新到 ${check.version}?[y/N] `);
37
+ if (!isYes(answer))
38
+ return;
39
+ const proof = await options.client.canAutoInstall();
40
+ if (!proof.ok) {
41
+ writeStderr(`当前 loop-agent 不是由此 npm 的正式全局安装管理,无法自动升级。\n请运行:npm install -g @tea-agent/loop-agent@${check.version}\n`);
42
+ return;
43
+ }
44
+ const install = await options.client.installExact(check.version);
45
+ if (!install.ok) {
46
+ writeStderr(`loop-agent 更新失败:${install.reason}\n`);
47
+ return;
48
+ }
49
+ const verified = await options.client.verifyInstalled(check.version);
50
+ if (!verified.ok) {
51
+ writeStderr(`loop-agent 已尝试安装,但验证失败:${verified.reason}\n`);
52
+ return;
53
+ }
54
+ writeStderr(`loop-agent 已更新到 ${check.version};新版本将在下一次命令调用时完全生效。\n`);
55
+ }
56
+ finally {
57
+ await release().catch(() => undefined);
58
+ }
59
+ }
60
+ catch (error) {
61
+ writeStderr(`loop-agent 更新检查已跳过:${error instanceof Error ? error.message : String(error)}\n`);
62
+ }
63
+ }
64
+ function canPromptInteractively(options) {
65
+ if (!options.stdinIsTTY || !options.stderrIsTTY)
66
+ return false;
67
+ if (options.env.CI)
68
+ return false;
69
+ if (options.env.LOOP_AGENT_DISABLE_UPDATE_CHECK === "1")
70
+ return false;
71
+ return true;
72
+ }
73
+ function formatLocalDate(date) {
74
+ const year = date.getFullYear();
75
+ const month = String(date.getMonth() + 1).padStart(2, "0");
76
+ const day = String(date.getDate()).padStart(2, "0");
77
+ return `${year}-${month}-${day}`;
78
+ }
79
+ async function markPromptedToday(statePath, state, today) {
80
+ await writeUpdateState(statePath, {
81
+ ...state,
82
+ lastPromptedLocalDate: today,
83
+ });
84
+ }
85
+ async function ask(options, prompt) {
86
+ if (options.readLine) {
87
+ options.writeStderr?.(prompt);
88
+ return options.readLine(prompt);
89
+ }
90
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
91
+ try {
92
+ return await rl.question(prompt);
93
+ }
94
+ finally {
95
+ rl.close();
96
+ }
97
+ }
98
+ function isYes(answer) {
99
+ return /^(y|yes)$/i.test(answer.trim());
100
+ }
@@ -3,25 +3,48 @@ import { lstat, readFile, realpath } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import semver from "semver";
5
5
  const PACKAGE_NAME = "@tea-agent/loop-agent";
6
+ export const DEFAULT_NPM_QUERY_TIMEOUT_MS = 15_000;
6
7
  export function createNpmUpdateClient(options) {
7
8
  const npmCommand = options.npmCommand ?? "npm";
8
9
  const loopAgentCommand = options.loopAgentCommand ?? "loop-agent";
9
10
  const runner = options.runner ?? runCommand;
10
- const timeoutMs = options.timeoutMs ?? 1500;
11
+ const timeoutMs = options.timeoutMs ?? DEFAULT_NPM_QUERY_TIMEOUT_MS;
12
+ const cwd = options.cwd;
11
13
  const env = options.env;
14
+ const latestVersionCheck = async () => {
15
+ let result;
16
+ try {
17
+ result = await runner(npmCommand, ["view", PACKAGE_NAME, "dist-tags.latest", "--json"], { timeoutMs, cwd, env });
18
+ }
19
+ catch {
20
+ return { ok: false, reason: "spawn-failure" };
21
+ }
22
+ if (result.timedOut)
23
+ return { ok: false, reason: "timeout" };
24
+ if (result.exitCode === null) {
25
+ return { ok: false, reason: "spawn-failure" };
26
+ }
27
+ if (result.exitCode !== 0) {
28
+ return { ok: false, reason: "command-failed" };
29
+ }
30
+ const version = parseNpmViewLatest(result.stdout);
31
+ return version
32
+ ? { ok: true, version }
33
+ : { ok: false, reason: "invalid-response" };
34
+ };
12
35
  return {
13
36
  async latestVersion() {
14
- const result = await runner(npmCommand, ["view", PACKAGE_NAME, "dist-tags.latest", "--json"], { timeoutMs, env });
15
- if (result.exitCode !== 0 || result.timedOut)
16
- return undefined;
17
- return parseNpmViewLatest(result.stdout);
37
+ const result = await latestVersionCheck();
38
+ return result.ok ? result.version : undefined;
18
39
  },
40
+ latestVersionCheck,
19
41
  async canAutoInstall() {
20
42
  return proveGlobalNpmInstall({
21
43
  currentVersion: options.currentVersion,
22
44
  packageRoot: options.packageRoot,
23
45
  npmCommand,
24
46
  runner,
47
+ cwd,
25
48
  env,
26
49
  timeoutMs,
27
50
  });
@@ -29,17 +52,25 @@ export function createNpmUpdateClient(options) {
29
52
  async installExact(version) {
30
53
  if (!isStrictRelease(version))
31
54
  return { ok: false, reason: "invalid-version" };
32
- const result = await runner(npmCommand, buildExactInstallArgs(version), { timeoutMs: Math.max(timeoutMs, 120000), env });
55
+ const result = await runner(npmCommand, buildExactInstallArgs(version), {
56
+ timeoutMs: Math.max(timeoutMs, 120000),
57
+ cwd,
58
+ env,
59
+ });
33
60
  if (result.exitCode !== 0 || result.timedOut)
34
61
  return { ok: false, reason: "install-failed" };
35
62
  return { ok: true };
36
63
  },
37
64
  async verifyInstalled(version) {
38
- const list = await runner(npmCommand, ["list", "-g", PACKAGE_NAME, "--depth=0", "--json"], { timeoutMs, env });
65
+ const list = await runner(npmCommand, ["list", "-g", PACKAGE_NAME, "--depth=0", "--json"], { timeoutMs, cwd, env });
39
66
  if (list.exitCode !== 0 || !verifyNpmListVersion(list.stdout, version)) {
40
67
  return { ok: false, reason: "npm-list-version-mismatch" };
41
68
  }
42
- const pathVersion = await runner(loopAgentCommand, ["--version"], { timeoutMs, env });
69
+ const pathVersion = await runner(loopAgentCommand, ["--version"], {
70
+ timeoutMs,
71
+ cwd,
72
+ env,
73
+ });
43
74
  if (pathVersion.exitCode !== 0 || !verifyPathVersion(pathVersion.stdout, version)) {
44
75
  return { ok: false, reason: "path-version-mismatch" };
45
76
  }
@@ -80,6 +111,7 @@ export function verifyPathVersion(stdout, expectedVersion) {
80
111
  async function proveGlobalNpmInstall(options) {
81
112
  const root = await options.runner(options.npmCommand, ["root", "-g"], {
82
113
  timeoutMs: options.timeoutMs,
114
+ cwd: options.cwd,
83
115
  env: options.env,
84
116
  });
85
117
  if (root.exitCode !== 0 || root.timedOut)
@@ -113,6 +145,7 @@ function isStrictRelease(version) {
113
145
  async function runCommand(command, args, options) {
114
146
  return new Promise((resolve) => {
115
147
  const child = spawn(command, [...args], {
148
+ cwd: options.cwd,
116
149
  env: options.env ? { ...process.env, ...options.env } : process.env,
117
150
  stdio: ["ignore", "pipe", "pipe"],
118
151
  shell: false,