@tea-agent/loop-agent 0.38.2 → 0.38.4

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 (192) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +171 -96
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +4 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +428 -27
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/commands/task-advance.js +31 -0
  17. package/dist/executors/dag-pi-executor.js +76 -10
  18. package/dist/executors/pi-event-serializer.js +42 -1
  19. package/dist/executors/pi-executor.js +51 -0
  20. package/dist/executors/pi-extension-resolver.js +233 -0
  21. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  22. package/dist/executors/pi-sdk-executor.js +187 -59
  23. package/dist/executors/shell-executor.js +265 -15
  24. package/dist/executors/shell-write-guard.js +7 -0
  25. package/dist/shared/dag-prompt-override.js +28 -0
  26. package/dist/shared/operator/capabilities.js +32 -0
  27. package/dist/shared/playwright-cli-command-policy.js +15 -0
  28. package/dist/shared/update/console-notifier.js +100 -0
  29. package/dist/{cli → shared}/update/npm-client.js +41 -8
  30. package/dist/{cli → shared}/update/state.js +41 -13
  31. package/dist/task/contract/constants.js +1 -0
  32. package/dist/task/contract/diff.js +26 -0
  33. package/dist/task/contract/project.js +5 -0
  34. package/dist/task/contract/schema.js +6 -1
  35. package/dist/task/source-prepare/build-draft.js +15 -0
  36. package/dist/task/source-prepare/parse-intent.js +24 -4
  37. package/dist/worker/cli.js +31 -1
  38. package/dist/worker/console/chat/routes.js +1 -0
  39. package/dist/worker/console/dag-execution-receipt.js +33 -0
  40. package/dist/worker/console/operator-actions.js +116 -0
  41. package/dist/worker/console/operator-user-error.js +2 -2
  42. package/dist/worker/console/pi-readiness.js +4 -4
  43. package/dist/worker/console/routes.js +43 -2
  44. package/dist/worker/console/security.js +63 -9
  45. package/dist/worker/console/server.js +36 -5
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  78. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  79. package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  105. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  106. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  107. package/dist/worker/observe/node-input.js +74 -3
  108. package/dist/worker/observe/node-process.js +377 -0
  109. package/dist/worker/observe/routes.js +36 -3
  110. package/dist/worker/observe/shell-handler-keys.js +34 -0
  111. package/dist/worker/observe/static/api.js +66 -0
  112. package/dist/worker/observe/static/constants.js +8 -1
  113. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  114. package/dist/worker/observe/static/format.js +23 -0
  115. package/dist/worker/observe/static/markdown-render.js +20 -1
  116. package/dist/worker/observe/static/operator-chrome.js +4 -0
  117. package/dist/worker/observe/static/state.js +13 -2
  118. package/dist/worker/observe/static/styles.css +73 -0
  119. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  120. package/dist/worker/observe/static/views/dag.js +8 -1
  121. package/dist/worker/observe/static/views/failures.js +5 -2
  122. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  123. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +76 -6
  124. package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
  125. package/dist/workflows/dag/contract-output-registry.js +15 -0
  126. package/dist/workflows/dag/failure-category.js +12 -0
  127. package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
  128. package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
  129. package/dist/workflows/dag/frontend-repair.js +14 -0
  130. package/dist/workflows/dag/frontend-risk.js +15 -2
  131. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  132. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  133. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  134. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  135. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  136. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  137. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  138. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  139. package/dist/workflows/dag/init-hybrid.js +196 -117
  140. package/dist/workflows/dag/node-execution.js +139 -10
  141. package/dist/workflows/dag/prompt.js +15 -1
  142. package/dist/workflows/dag/rerun-plan.js +134 -3
  143. package/dist/workflows/dag/rerun-run.js +58 -6
  144. package/dist/workflows/dag/retry-policy.js +91 -0
  145. package/dist/workflows/dag/structured-output-repair.js +712 -0
  146. package/dist/workflows/dag/types.js +32 -1
  147. package/dist/workflows/dag/validate.js +41 -1
  148. package/docs/init-surface.manifest.json +5 -0
  149. package/docs/operations/README.md +1 -1
  150. package/docs/operations/local-development-environment.md +1 -5
  151. package/docs/skills/vetted-skill-registry.md +14 -0
  152. package/docs/templates/README.md +1 -1
  153. package/docs/templates/agent-dag.schema.json +44 -0
  154. package/docs/templates/backend-test-dag.json +2 -2
  155. package/docs/templates/frontend-test-dag.json +8 -10
  156. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  157. package/docs/templates/init-managed-agents.md +13 -3
  158. package/harness.json +2 -2
  159. package/package.json +12 -4
  160. package/scripts/next-info.mjs +356 -0
  161. package/scripts/next-publish-gate.mjs +238 -0
  162. package/scripts/release-source-binding.mjs +251 -0
  163. package/skills/analyze-product-requirements/SKILL.md +8 -5
  164. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  165. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  166. package/skills/analyze-product-requirements/references/example.md +50 -0
  167. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  168. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  169. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  170. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  171. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  172. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  173. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  174. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  175. package/skills/codebase-scout/SKILL.md +1 -1
  176. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  178. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  179. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  180. package/skills/improve-codebase-architecture/deepening.md +37 -0
  181. package/skills/improve-codebase-architecture/html-report.md +123 -0
  182. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  183. package/skills/improve-codebase-architecture/language.md +53 -0
  184. package/skills/loop-agent/references/command-reference.md +11 -4
  185. package/skills/playwright-cli/SKILL.md +22 -0
  186. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  187. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
  188. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
  189. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
  190. package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
  191. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
  192. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
@@ -4,7 +4,7 @@ import { appendFile, lstat, mkdir, readFile, realpath, stat, } from "node:fs/pro
4
4
  import path from "node:path";
5
5
  import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
6
6
  import { resolvePlaywrightCliLauncher } from "./playwright-cli-launcher.js";
7
- import { PLAYWRIGHT_CLI_ALLOWED_COMMANDS, isPlaywrightCliCommand, } from "../shared/playwright-cli-command-policy.js";
7
+ import { PLAYWRIGHT_CLI_ALLOWED_COMMANDS, isPlaywrightCliCommand, parsePlaywrightCliFindMatchCount, } from "../shared/playwright-cli-command-policy.js";
8
8
  export { PLAYWRIGHT_CLI_ALLOWED_COMMANDS, isPlaywrightCliCommand, };
9
9
  /** Logical capability name only; execution always uses the verified JS launcher. */
10
10
  export const PLAYWRIGHT_CLI_EXECUTABLE = "playwright-cli";
@@ -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;
@@ -137,8 +144,10 @@ export function summarizePlaywrightCliReceipts(receipts) {
137
144
  if (PLAYWRIGHT_CLI_INTERACTION_COMMANDS.has(receipt.command) && succeeded) {
138
145
  hasInteractionOrAssertion = true;
139
146
  }
140
- // A successful find is the only model-visible semantic assertion in v1.
141
- if (receipt.command === "find" && succeeded) {
147
+ // A successful find (or find-unique, its semantic wrapper) is the only
148
+ // model-visible semantic assertion in v1.
149
+ if ((receipt.command === "find" || receipt.command === "find-unique") &&
150
+ succeeded) {
142
151
  hasMeaningfulAssertion = true;
143
152
  if (ordered && sawOpen)
144
153
  sawMeaningfulAssertionAfterOpen = true;
@@ -523,6 +532,17 @@ export function preparePlaywrightCliArgv(input, ctx) {
523
532
  else if (command === "pdf" || command === "snapshot") {
524
533
  args = normalizePdfOrSnapshotArgs(command, rawArgs, ctx);
525
534
  }
535
+ else if (command === "find-unique") {
536
+ // find-unique is a controller-side semantic wrapper: the underlying CLI
537
+ // only knows `find`; uniqueness (matchCount === 1) is judged by the
538
+ // executor after parsing the find output. Require exactly one text
539
+ // literal — a unique-control assertion is meaningless with multiple.
540
+ const literals = rawArgs.filter((arg) => !arg.startsWith("-"));
541
+ if (rawArgs.length !== 1 || literals.length !== 1) {
542
+ throw new PlaywrightCliPolicyError("find-unique-args", "find-unique requires exactly one text literal and no flags");
543
+ }
544
+ args = literals;
545
+ }
526
546
  else {
527
547
  args = rewriteArgsForPaths(command, rawArgs, ctx);
528
548
  }
@@ -818,7 +838,10 @@ export async function executePlaywrightCliCommand(input, ctx, signal) {
818
838
  };
819
839
  }
820
840
  let executable = PLAYWRIGHT_CLI_EXECUTABLE;
821
- let argv = [command, ...args];
841
+ // find-unique executes as the underlying CLI `find` command; uniqueness is
842
+ // a controller-side judgment on the parsed match count.
843
+ const cliCommand = command === "find-unique" ? "find" : command;
844
+ let argv = [cliCommand, ...args];
822
845
  try {
823
846
  await validatePlaywrightCliOutputPath(command, args, ctx);
824
847
  await validatePlaywrightCliInputFiles(command, args, ctx);
@@ -827,7 +850,7 @@ export async function executePlaywrightCliCommand(input, ctx, signal) {
827
850
  if (!ctx.runCommand) {
828
851
  const launcher = resolvePlaywrightCliLauncher(ctx.repoRoot);
829
852
  executable = launcher.executable;
830
- argv = [...launcher.argvPrefix, command, ...args];
853
+ argv = [...launcher.argvPrefix, cliCommand, ...args];
831
854
  }
832
855
  }
833
856
  catch (error) {
@@ -887,6 +910,19 @@ export async function executePlaywrightCliCommand(input, ctx, signal) {
887
910
  }
888
911
  const finishedAt = new Date();
889
912
  const combined = `${runResult.stdout}\n${runResult.stderr}`;
913
+ // find-family commands report their match count in stdout with exit code
914
+ // 0 even on zero matches — parse the count so receipts carry the real
915
+ // assertion signal for finalize-side audits.
916
+ const matchCount = command === "find" || command === "find-unique"
917
+ ? parsePlaywrightCliFindMatchCount(runResult.stdout || "")
918
+ : undefined;
919
+ // find-unique fails closed unless exactly one match was found (including
920
+ // zero and unparseable outputs).
921
+ const findUniqueFailed = command === "find-unique" && matchCount !== 1 && !runResult.timedOut;
922
+ const effectiveExitCode = findUniqueFailed && runResult.exitCode === 0 ? 1 : runResult.exitCode;
923
+ const effectiveStderr = findUniqueFailed
924
+ ? `${runResult.stderr}${runResult.stderr ? "\n" : ""}find-unique: expected exactly 1 match, got ${matchCount === undefined ? "unparseable output" : `${matchCount}`}`
925
+ : runResult.stderr;
890
926
  const receipt = {
891
927
  schemaVersion: 1,
892
928
  caseId: ctx.caseId,
@@ -895,25 +931,28 @@ export async function executePlaywrightCliCommand(input, ctx, signal) {
895
931
  argsRedacted: redactPlaywrightCliArgs(command, args),
896
932
  startedAt: startedAt.toISOString(),
897
933
  finishedAt: finishedAt.toISOString(),
898
- exitCode: runResult.exitCode,
934
+ exitCode: effectiveExitCode,
899
935
  durationMs: finishedAt.getTime() - startedAt.getTime(),
900
936
  outputSha256: createHash("sha256").update(combined).digest("hex"),
901
937
  toolVersion: ctx.toolVersion ?? "unknown",
902
938
  timedOut: runResult.timedOut || undefined,
939
+ ...(matchCount !== undefined ? { matchCount } : {}),
903
940
  errorClass: runResult.timedOut
904
941
  ? "timeout"
905
942
  : runResult.signal === "abort"
906
943
  ? "abort"
907
- : runResult.exitCode === 0
944
+ : effectiveExitCode === 0
908
945
  ? undefined
909
- : "nonzero-exit",
946
+ : findUniqueFailed
947
+ ? "find-unique-count-mismatch"
948
+ : "nonzero-exit",
910
949
  };
911
950
  await appendReceipt(ctx, receipt);
912
951
  return {
913
- ok: runResult.exitCode === 0 && !runResult.timedOut,
914
- exitCode: runResult.exitCode,
952
+ ok: effectiveExitCode === 0 && !runResult.timedOut,
953
+ exitCode: effectiveExitCode,
915
954
  stdout: runResult.stdout,
916
- stderr: runResult.stderr,
955
+ stderr: effectiveStderr,
917
956
  timedOut: runResult.timedOut,
918
957
  receipt,
919
958
  errorClass: receipt.errorClass,
@@ -1,8 +1,16 @@
1
1
  import { appendFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from "./pi-executor.js";
3
+ import { computeExtensionToolAllowlistExtras } from "./pi-extension-resolver.js";
4
+ import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractPiEventStopReason, extractAssistantTextFromPiJson, } from "./pi-executor.js";
4
5
  import { serializeSessionEvent } from "./pi-event-serializer.js";
5
6
  import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
7
+ /** Same-session recovery after a transport stall. Exactly one continue per SDK attempt. */
8
+ export const PI_SDK_STALL_CONTINUE_MAX = 1;
9
+ /**
10
+ * Fixed follow-up sent after aborting a stalled prompt.
11
+ * Kept short so the model can resume without repeating the original task payload.
12
+ */
13
+ export const PI_SDK_STALL_CONTINUE_PROMPT = "继续。上一轮因长时间无 provider 活动已被中止;请从中断处接着完成,不要重复已完成的步骤。若此前没有有效输出,请重新执行原任务。";
6
14
  let sdkSessionFactoryOverride;
7
15
  let sdkImportOverrideForTests;
8
16
  let sdkModuleOverrideForTests;
@@ -44,15 +52,15 @@ export async function resetPiSdkReuseScopeForTests() {
44
52
  export function setPiSdkSessionFactoryForTests(factory) {
45
53
  sdkSessionFactoryOverride = factory;
46
54
  }
47
- /** Test hook: simulate SDK import success/failure without relying on optionalDependency install state. */
55
+ /** Test hook: simulate SDK import success/failure without relying on installed SDK state. */
48
56
  export function setPiSdkImportOverrideForTests(fn) {
49
57
  sdkImportOverrideForTests = fn;
50
58
  }
51
- /** Test hook: inject a mock Pi SDK module to exercise resolveSdkSessionFactory without optionalDependency. */
59
+ /** Test hook: inject a mock Pi SDK module to exercise resolveSdkSessionFactory without loading the real SDK. */
52
60
  export function setPiSdkModuleOverrideForTests(fn) {
53
61
  sdkModuleOverrideForTests = fn;
54
62
  }
55
- /** Check whether the Pi SDK optional dependency satisfies the 0.80.10 runtime contract. */
63
+ /** Check whether the Pi SDK runtime dependency satisfies the 0.83.0 runtime contract. */
56
64
  export async function checkPiSdkAvailability(_repoRoot) {
57
65
  if (sdkSessionFactoryOverride) {
58
66
  return { ok: true, detail: "pi SDK session factory override active" };
@@ -68,10 +76,10 @@ export async function checkPiSdkAvailability(_repoRoot) {
68
76
  typeof ModelRuntime?.create !== "function") {
69
77
  return {
70
78
  ok: false,
71
- detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.80.10 contract)",
79
+ detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.83.0 contract)",
72
80
  };
73
81
  }
74
- return { ok: true, detail: "pi SDK 0.80.10 contract available" };
82
+ return { ok: true, detail: "pi SDK 0.83.0 contract available" };
75
83
  }
76
84
  catch (error) {
77
85
  const message = error instanceof Error ? error.message : String(error);
@@ -222,6 +230,9 @@ async function createSdkSession(sdk, input, shared) {
222
230
  noContextFiles: true,
223
231
  noSkills: true,
224
232
  noExtensions: true,
233
+ ...(input.additionalExtensionPaths && input.additionalExtensionPaths.length > 0
234
+ ? { additionalExtensionPaths: input.additionalExtensionPaths }
235
+ : {}),
225
236
  ...(settingsManager ? { settingsManager } : {}),
226
237
  appendSystemPromptOverride: (base) => [
227
238
  ...base,
@@ -229,6 +240,19 @@ async function createSdkSession(sdk, input, shared) {
229
240
  ],
230
241
  });
231
242
  await loader.reload();
243
+ // Discover extension-registered tool names BEFORE session creation (plan
244
+ // 2026-08-21 D5): verified against the real SDK — passing extension tool
245
+ // names in createAgentSession `tools` activates them, while a post-create
246
+ // setActiveToolsByName pin leaves the active set unchanged. Merge filtered
247
+ // extras into the create-time allowlist; protected builtins, the deny
248
+ // surface, and write-capable extension tools never join.
249
+ const extensionToolExtras = [];
250
+ if (input.additionalExtensionPaths &&
251
+ input.additionalExtensionPaths.length > 0) {
252
+ const registered = collectLoaderExtensionToolNames(loader);
253
+ extensionToolExtras.push(...computeExtensionToolAllowlistExtras(registered.map((name) => ({ name })), input.toolNames));
254
+ }
255
+ const sessionToolNames = [...input.toolNames, ...extensionToolExtras];
232
256
  if (input.requireWriterCustomTools) {
233
257
  if (!Array.isArray(input.customTools) || input.customTools.length === 0) {
234
258
  throw new Error("pi writer tool policy missing customTools; refusing to create uncontrolled writer session");
@@ -238,7 +262,7 @@ async function createSdkSession(sdk, input, shared) {
238
262
  cwd: input.cwd,
239
263
  sessionManager: SessionManager.inMemory(input.cwd),
240
264
  resourceLoader: loader,
241
- tools: input.toolNames,
265
+ tools: sessionToolNames,
242
266
  modelRuntime,
243
267
  ...(settingsManager ? { settingsManager } : {}),
244
268
  ...(model ? { model } : {}),
@@ -247,6 +271,22 @@ async function createSdkSession(sdk, input, shared) {
247
271
  ? { customTools: input.customTools }
248
272
  : {}),
249
273
  });
274
+ if (extensionToolExtras.length > 0) {
275
+ // Fail closed when merged extension tools did not activate: a silent
276
+ // pin failure must never let the node run without its declared tools.
277
+ const activeTools = created.session.agent?.state?.tools;
278
+ if (!Array.isArray(activeTools)) {
279
+ throw new Error("pi SDK session does not expose agent.state.tools; refusing unverifiable extension-tool activation");
280
+ }
281
+ const activeNames = new Set(activeTools
282
+ .map((tool) => typeof tool?.name === "string" ? tool.name : undefined)
283
+ .filter((name) => Boolean(name)));
284
+ for (const extensionToolName of extensionToolExtras) {
285
+ if (!activeNames.has(extensionToolName)) {
286
+ throw new Error(`pi SDK extension-tool activation mismatch for ${extensionToolName}: tool did not activate at session creation`);
287
+ }
288
+ }
289
+ }
250
290
  const customToolNames = (input.customTools ?? [])
251
291
  .map((tool) => isRecord(tool) && typeof tool.name === "string" ? tool.name : undefined)
252
292
  .filter((name) => Boolean(name));
@@ -259,7 +299,7 @@ async function createSdkSession(sdk, input, shared) {
259
299
  .map((tool) => typeof tool?.name === "string" ? tool.name : undefined)
260
300
  .filter((name) => Boolean(name)));
261
301
  for (const customToolName of customToolNames) {
262
- const expectedActive = input.toolNames.includes(customToolName);
302
+ const expectedActive = sessionToolNames.includes(customToolName);
263
303
  if (activeNames.has(customToolName) !== expectedActive) {
264
304
  throw new Error(`pi SDK custom-tool activation mismatch for ${customToolName}: expectedActive=${expectedActive}`);
265
305
  }
@@ -267,6 +307,47 @@ async function createSdkSession(sdk, input, shared) {
267
307
  }
268
308
  return created.session;
269
309
  }
310
+ /**
311
+ * Collect tool names registered by loader-resolved extensions. The SDK exposes
312
+ * each extension's `tools` as a Record (name → definition) or Map; both shapes
313
+ * are handled. Missing getExtensions (older SDK) yields no extras — the node
314
+ * then runs with the baseline allowlist only.
315
+ */
316
+ function collectLoaderExtensionToolNames(loader) {
317
+ const getExtensions = loader?.getExtensions;
318
+ if (typeof getExtensions !== "function")
319
+ return [];
320
+ let parsed;
321
+ try {
322
+ parsed = getExtensions.call(loader);
323
+ }
324
+ catch {
325
+ return [];
326
+ }
327
+ if (!parsed || typeof parsed !== "object")
328
+ return [];
329
+ const extensions = parsed.extensions;
330
+ if (!Array.isArray(extensions))
331
+ return [];
332
+ const names = [];
333
+ for (const extension of extensions) {
334
+ if (!extension || typeof extension !== "object")
335
+ continue;
336
+ const tools = extension.tools;
337
+ if (tools instanceof Map) {
338
+ for (const key of tools.keys()) {
339
+ if (typeof key === "string")
340
+ names.push(key);
341
+ }
342
+ }
343
+ else if (tools && typeof tools === "object") {
344
+ for (const key of Object.keys(tools)) {
345
+ names.push(key);
346
+ }
347
+ }
348
+ }
349
+ return names;
350
+ }
270
351
  /**
271
352
  * Throttle high-noise Pi SDK events from session-events.jsonl.
272
353
  * Always keeps tool lifecycle, turn/agent end, assistant messages, and errors.
@@ -404,25 +485,6 @@ function extractSdkUsageSample(event) {
404
485
  const responseKey = responseKeyCandidates.find((value) => typeof value === "string" && value.length > 0);
405
486
  return responseKey ? { responseKey, tokens } : { tokens };
406
487
  }
407
- /** Extract a non-empty stop reason from a terminal SDK event.
408
- * Probes multiple field shapes across SDK versions so a missing field fails
409
- * open (undefined) rather than misclassifying an attempt. */
410
- function readStopReason(event) {
411
- const message = isRecord(event.message) ? event.message : undefined;
412
- const candidates = [
413
- message?.stop_reason,
414
- message?.stopReason,
415
- event.stopReason,
416
- event.stop_reason,
417
- ];
418
- for (const candidate of candidates) {
419
- if (typeof candidate === "string" &&
420
- candidate.trim().length > 0) {
421
- return candidate.trim();
422
- }
423
- }
424
- return undefined;
425
- }
426
488
  function aggregateSdkTokenUsage(samples) {
427
489
  const identified = new Map();
428
490
  let anonymousMaximum = 0;
@@ -444,6 +506,8 @@ function aggregateSdkTokenUsage(samples) {
444
506
  * When reuseScope is active, only shared auth/model resources are reused; each attempt still
445
507
  * creates and disposes its own session and resource loader. Session reuse across steps is
446
508
  * deferred until a proven reset/isolation strategy exists.
509
+ * A transport stall may abort the current prompt and send one continue on the
510
+ * same session; that is in-attempt recovery, not cross-step session reuse.
447
511
  */
448
512
  export async function executeSingleSdkAttempt(options) {
449
513
  const modelConfig = options.modelConfig;
@@ -494,7 +558,7 @@ export async function executeSingleSdkAttempt(options) {
494
558
  type === "message_end" ||
495
559
  type === "agent_end" ||
496
560
  type === "agent_settled") {
497
- const candidate = readStopReason(event);
561
+ const candidate = extractPiEventStopReason(event);
498
562
  if (candidate)
499
563
  stopReason = candidate;
500
564
  }
@@ -555,20 +619,37 @@ export async function executeSingleSdkAttempt(options) {
555
619
  ? { customTools: writerCustomTools }
556
620
  : {}),
557
621
  ...(requireWriterCustomTools ? { requireWriterCustomTools: true } : {}),
622
+ ...(options.piExtensionPaths && options.piExtensionPaths.length > 0
623
+ ? { additionalExtensionPaths: options.piExtensionPaths }
624
+ : {}),
558
625
  });
559
626
  let resolveStall;
560
627
  let resolveSettlement;
561
628
  let settled = false;
562
- const settlementPromise = new Promise((resolve) => {
563
- resolveSettlement = resolve;
564
- });
565
- const stallPromise = stallTimeoutMs > 0
566
- ? new Promise((resolve) => {
629
+ let abortConfirmed = true;
630
+ const persistRuntimeNote = (event) => {
631
+ const line = serializeSessionEvent(event);
632
+ stdoutPreview.append(`${line}\n`);
633
+ stdoutCollector.append(`${line}\n`);
634
+ sessionEventAppender?.append(line, event);
635
+ };
636
+ const resetSettlement = () => {
637
+ settled = false;
638
+ return new Promise((resolve) => {
639
+ resolveSettlement = resolve;
640
+ });
641
+ };
642
+ const resetStallPromise = () => {
643
+ if (stallTimeoutMs <= 0) {
644
+ resolveStall = undefined;
645
+ return null;
646
+ }
647
+ return new Promise((resolve) => {
567
648
  resolveStall = resolve;
568
- })
569
- : null;
649
+ });
650
+ };
570
651
  const armStallWatchdog = () => {
571
- if (!stallPromise || !resolveStall)
652
+ if (stallTimeoutMs <= 0 || !resolveStall)
572
653
  return;
573
654
  if (stallHandle)
574
655
  clearTimeout(stallHandle);
@@ -609,17 +690,6 @@ export async function executeSingleSdkAttempt(options) {
609
690
  stdoutCollector.append(`${line}\n`);
610
691
  sessionEventAppender?.append(line, event);
611
692
  });
612
- const filePrefix = options.attachedFiles
613
- .map((file) => `@${file}`)
614
- .join(" ");
615
- const promptMessage = filePrefix
616
- ? `${filePrefix}\n${options.userMessage}`
617
- : options.userMessage;
618
- const promptPromise = session.prompt(promptMessage);
619
- // A settlement event is permitted to win the race while prompt() remains
620
- // pending. Observe a later rejection so it never becomes unhandled.
621
- void promptPromise.catch(() => { });
622
- armStallWatchdog();
623
693
  const timeoutPromise = timeoutMs > 0
624
694
  ? new Promise((resolve) => {
625
695
  timeoutHandle = setTimeout(() => {
@@ -627,21 +697,79 @@ export async function executeSingleSdkAttempt(options) {
627
697
  }, timeoutMs);
628
698
  })
629
699
  : null;
630
- const raced = await Promise.race([
631
- promptPromise.then(() => "done"),
632
- settlementPromise,
633
- ...(timeoutPromise ? [timeoutPromise] : []),
634
- ...(stallPromise ? [stallPromise] : []),
635
- ]);
636
- if (raced !== "done" && raced !== "settled") {
637
- timedOut = true;
638
- appendStderr(raced === "stall"
639
- ? `pi SDK step stalled after ${stallTimeoutMs}ms with no provider activity`
640
- : `pi SDK step timed out after ${timeoutMs}ms`);
641
- const abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
700
+ const runTurn = async (userMessage) => {
701
+ const settlementPromise = resetSettlement();
702
+ const stallPromise = resetStallPromise();
703
+ const promptPromise = session.prompt(userMessage);
704
+ // A settlement event is permitted to win the race while prompt() remains
705
+ // pending. Observe a later rejection so it never becomes unhandled.
706
+ void promptPromise.catch(() => { });
707
+ armStallWatchdog();
708
+ return Promise.race([
709
+ promptPromise.then(() => "done"),
710
+ settlementPromise,
711
+ ...(timeoutPromise ? [timeoutPromise] : []),
712
+ ...(stallPromise ? [stallPromise] : []),
713
+ ]);
714
+ };
715
+ const disposeSession = async () => {
642
716
  disposeAttempted = true;
643
717
  const disposeConfirmed = await runSessionActionWithGrace("dispose", () => session.dispose());
644
718
  terminationConfirmed = abortConfirmed && disposeConfirmed;
719
+ };
720
+ const failClosedAfterWait = async (reason, extraStderr, abortFirst = true) => {
721
+ timedOut = true;
722
+ appendStderr(reason === "stall"
723
+ ? `pi SDK step stalled after ${stallTimeoutMs}ms with no provider activity`
724
+ : `pi SDK step timed out after ${timeoutMs}ms`);
725
+ if (extraStderr)
726
+ appendStderr(extraStderr);
727
+ if (abortFirst) {
728
+ abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
729
+ }
730
+ await disposeSession();
731
+ };
732
+ const filePrefix = options.attachedFiles
733
+ .map((file) => `@${file}`)
734
+ .join(" ");
735
+ const promptMessage = filePrefix
736
+ ? `${filePrefix}\n${options.userMessage}`
737
+ : options.userMessage;
738
+ let continuesSent = 0;
739
+ let raced = await runTurn(promptMessage);
740
+ if (raced === "stall") {
741
+ abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
742
+ const withinAbsoluteTimeout = timeoutMs <= 0 || Date.now() - startedAt < timeoutMs;
743
+ const canContinue = abortConfirmed &&
744
+ writeToolCallCount === 0 &&
745
+ withinAbsoluteTimeout &&
746
+ continuesSent < PI_SDK_STALL_CONTINUE_MAX;
747
+ if (canContinue) {
748
+ // The aborted turn's stopReason must not leak onto a recovered continue.
749
+ stopReason = undefined;
750
+ continuesSent += 1;
751
+ persistRuntimeNote({
752
+ type: "loop-agent-stall-continue",
753
+ prompt: PI_SDK_STALL_CONTINUE_PROMPT,
754
+ maxContinues: PI_SDK_STALL_CONTINUE_MAX,
755
+ });
756
+ raced = await runTurn(PI_SDK_STALL_CONTINUE_PROMPT);
757
+ if (raced !== "done" && raced !== "settled") {
758
+ await failClosedAfterWait(raced === "stall" ? "stall" : "absolute-timeout", raced === "stall"
759
+ ? "pi SDK stall continue did not recover; treating as timeout"
760
+ : "pi SDK stall continue hit the absolute timeout", true);
761
+ }
762
+ }
763
+ else {
764
+ await failClosedAfterWait("stall", !abortConfirmed
765
+ ? undefined
766
+ : writeToolCallCount > 0
767
+ ? "pi SDK stall continue skipped because write tools already ran"
768
+ : "pi SDK stall continue skipped because absolute timeout elapsed", false);
769
+ }
770
+ }
771
+ else if (raced === "absolute-timeout") {
772
+ await failClosedAfterWait("absolute-timeout");
645
773
  }
646
774
  }
647
775
  catch (error) {