@selesai/code 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/dist/core/diagnostics.d.ts +1 -0
  2. package/dist/core/diagnostics.d.ts.map +1 -1
  3. package/dist/core/diagnostics.js.map +1 -1
  4. package/dist/core/package-manager.d.ts +10 -0
  5. package/dist/core/package-manager.d.ts.map +1 -1
  6. package/dist/core/package-manager.js +93 -8
  7. package/dist/core/package-manager.js.map +1 -1
  8. package/dist/core/package-manager.test.d.ts +2 -0
  9. package/dist/core/package-manager.test.d.ts.map +1 -0
  10. package/dist/core/package-manager.test.js +20 -0
  11. package/dist/core/package-manager.test.js.map +1 -0
  12. package/dist/core/resource-loader.d.ts +3 -0
  13. package/dist/core/resource-loader.d.ts.map +1 -1
  14. package/dist/core/resource-loader.js +10 -0
  15. package/dist/core/resource-loader.js.map +1 -1
  16. package/dist/core/settings-manager.d.ts +1 -0
  17. package/dist/core/settings-manager.d.ts.map +1 -1
  18. package/dist/core/settings-manager.js.map +1 -1
  19. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  20. package/dist/extensions/caveman/index.js +116 -0
  21. package/dist/extensions/caveman/package.json +8 -0
  22. package/dist/extensions/caveman/test/extension.test.js +185 -0
  23. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  24. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  25. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  26. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  27. package/dist/extensions/pi-intercom/config.ts +2 -2
  28. package/dist/extensions/pi-intercom/index.ts +2 -2
  29. package/dist/extensions/pi-intercom/package.json +2 -2
  30. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  31. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  32. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  33. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  34. package/dist/extensions/pi-powerline-footer/index.ts +9 -16
  35. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  36. package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
  37. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  38. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  39. package/dist/extensions/pi-subagents/README.md +224 -52
  40. package/dist/extensions/pi-subagents/install.mjs +93 -0
  41. package/dist/extensions/pi-subagents/package.json +4 -5
  42. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  43. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  44. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  45. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  46. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  47. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  48. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  49. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  50. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  51. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  52. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  53. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  54. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  55. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  56. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  57. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  58. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  59. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  61. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  62. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  63. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  64. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  66. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  67. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  68. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  69. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  70. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  71. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  72. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  73. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  74. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  75. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  76. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  77. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  78. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  79. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  80. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  81. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  87. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  88. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  89. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  90. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  92. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  93. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  94. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  95. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  96. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  97. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  98. package/dist/extensions/pi-subagents/src/shared/utils.ts +100 -15
  99. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  100. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  101. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  102. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  103. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  104. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  105. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  106. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  107. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  108. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  109. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  110. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  111. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  112. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  113. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  114. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  115. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  116. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  117. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  118. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  119. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  120. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  121. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  122. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  123. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  124. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  125. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  126. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  127. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  128. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  129. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  130. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  131. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  132. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  133. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  134. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  135. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  136. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  137. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  138. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  139. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  140. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  141. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  142. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  143. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  144. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  145. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  146. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  147. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  148. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  149. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  150. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  152. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  154. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  155. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  156. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  157. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  158. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  159. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  160. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  161. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  162. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  163. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  164. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  165. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  166. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  167. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  168. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  169. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  170. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  171. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  172. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  173. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  174. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  175. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  176. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  177. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  178. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  179. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  180. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  181. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  182. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  183. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  184. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  185. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  186. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  187. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  188. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  189. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  190. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  191. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  192. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  193. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  194. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  195. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  196. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  197. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  198. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  199. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  200. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  201. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  202. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  203. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  204. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  205. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  206. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  207. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  208. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  209. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  210. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  211. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  212. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  213. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  215. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  216. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  217. package/dist/main.d.ts.map +1 -1
  218. package/dist/main.js +4 -0
  219. package/dist/main.js.map +1 -1
  220. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  221. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  222. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  223. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  224. package/dist/skills/caveman/SKILL.md +49 -0
  225. package/dist/skills/handoff-text/SKILL.md +13 -0
  226. package/docs/docs.json +4 -0
  227. package/docs/extensions.md +2 -0
  228. package/docs/settings.md +15 -0
  229. package/docs/shared-host-extensions.md +109 -0
  230. package/package.json +1 -1
@@ -0,0 +1,159 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { afterEach, beforeEach, describe, it } from "node:test";
6
+ import { waitForImportedAsyncRoot } from "../../src/runs/background/chain-root-attachment.ts";
7
+
8
+ let tempDir: string;
9
+
10
+ function writeJson(filePath: string, value: unknown): void {
11
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
12
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
13
+ }
14
+
15
+ function root(runId = "root-run", index = 0) {
16
+ return {
17
+ runId,
18
+ index,
19
+ asyncDir: path.join(tempDir, runId),
20
+ resultPath: path.join(tempDir, "results", `${runId}.json`),
21
+ };
22
+ }
23
+
24
+ describe("async chain root attachment", () => {
25
+ beforeEach(() => {
26
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-chain-root-attachment-"));
27
+ });
28
+
29
+ afterEach(() => {
30
+ fs.rmSync(tempDir, { recursive: true, force: true });
31
+ });
32
+
33
+ it("imports an already completed async child result", async () => {
34
+ const importedRoot = root();
35
+ const sessionFile = path.join(tempDir, "child.jsonl");
36
+ fs.writeFileSync(sessionFile, "", "utf-8");
37
+ writeJson(path.join(importedRoot.asyncDir, "status.json"), {
38
+ runId: importedRoot.runId,
39
+ mode: "single",
40
+ state: "complete",
41
+ startedAt: 1,
42
+ steps: [{ agent: "worker", status: "complete", sessionFile }],
43
+ });
44
+ writeJson(importedRoot.resultPath, {
45
+ state: "complete",
46
+ success: true,
47
+ results: [{ agent: "worker", output: "root output", success: true, sessionFile }],
48
+ });
49
+
50
+ const result = await waitForImportedAsyncRoot(importedRoot, { pollIntervalMs: 1 });
51
+
52
+ assert.deepEqual({
53
+ agent: result.agent,
54
+ output: result.output,
55
+ exitCode: result.exitCode,
56
+ sessionFile: result.sessionFile,
57
+ }, {
58
+ agent: "worker",
59
+ output: "root output",
60
+ exitCode: 0,
61
+ sessionFile,
62
+ });
63
+ });
64
+
65
+ it("waits for a running async child to write its terminal result", async () => {
66
+ const importedRoot = root();
67
+ writeJson(path.join(importedRoot.asyncDir, "status.json"), {
68
+ runId: importedRoot.runId,
69
+ mode: "single",
70
+ state: "running",
71
+ startedAt: 1,
72
+ steps: [{ agent: "worker", status: "running" }],
73
+ });
74
+
75
+ const waiting = waitForImportedAsyncRoot(importedRoot, { pollIntervalMs: 5 });
76
+ setTimeout(() => {
77
+ writeJson(importedRoot.resultPath, {
78
+ state: "complete",
79
+ success: true,
80
+ results: [{ agent: "worker", output: "late root output", success: true }],
81
+ });
82
+ }, 20);
83
+
84
+ const result = await waiting;
85
+
86
+ assert.equal(result.output, "late root output");
87
+ assert.equal(result.exitCode, 0);
88
+ });
89
+
90
+ it("imports a failed root as a failed first chain step", async () => {
91
+ const importedRoot = root();
92
+ writeJson(path.join(importedRoot.asyncDir, "status.json"), {
93
+ runId: importedRoot.runId,
94
+ mode: "single",
95
+ state: "failed",
96
+ startedAt: 1,
97
+ error: "root failed",
98
+ steps: [{ agent: "worker", status: "failed", error: "root failed" }],
99
+ });
100
+ writeJson(importedRoot.resultPath, {
101
+ state: "failed",
102
+ success: false,
103
+ summary: "root failed",
104
+ results: [{ agent: "worker", output: "root failed", error: "root failed", success: false }],
105
+ });
106
+
107
+ const result = await waitForImportedAsyncRoot(importedRoot, { pollIntervalMs: 1 });
108
+
109
+ assert.equal(result.exitCode, 1);
110
+ assert.equal(result.error, "root failed");
111
+ assert.equal(result.output, "root failed");
112
+ });
113
+
114
+ it("fails a terminal root that never produced a result file", async () => {
115
+ const importedRoot = root();
116
+ writeJson(path.join(importedRoot.asyncDir, "status.json"), {
117
+ runId: importedRoot.runId,
118
+ mode: "single",
119
+ state: "complete",
120
+ startedAt: 1,
121
+ steps: [{ agent: "worker", status: "complete" }],
122
+ });
123
+
124
+ const result = await waitForImportedAsyncRoot(importedRoot, {
125
+ pollIntervalMs: 1,
126
+ terminalResultGraceMs: 0,
127
+ });
128
+
129
+ assert.equal(result.exitCode, 1);
130
+ assert.match(result.error ?? "", /ended without a result file/);
131
+ });
132
+
133
+ it("stops waiting when the parent timeout aborts an attached root", async () => {
134
+ const importedRoot = root();
135
+ writeJson(path.join(importedRoot.asyncDir, "status.json"), {
136
+ runId: importedRoot.runId,
137
+ mode: "single",
138
+ state: "running",
139
+ startedAt: 1,
140
+ steps: [{ agent: "worker", status: "running" }],
141
+ });
142
+ let timedOut = false;
143
+ const waiting = waitForImportedAsyncRoot(importedRoot, {
144
+ pollIntervalMs: 1,
145
+ shouldAbort: () => timedOut,
146
+ timeoutMessage: "parent timed out",
147
+ });
148
+ setTimeout(() => {
149
+ timedOut = true;
150
+ }, 10);
151
+
152
+ const result = await waiting;
153
+
154
+ assert.equal(result.exitCode, 1);
155
+ assert.equal(result.timedOut, true);
156
+ assert.equal(result.error, "parent timed out");
157
+ assert.equal(result.output, "parent timed out");
158
+ });
159
+ });
@@ -0,0 +1,229 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { parseChain, parseJsonChain, serializeChain, serializeJsonChain } from "../../src/agents/chain-serializer.ts";
4
+
5
+ const chainContent = `---
6
+ name: review-chain
7
+ description: Review chain
8
+ ---
9
+
10
+ ## reviewer
11
+ output: report.md
12
+ outputMode: file-only
13
+
14
+ Review the diff
15
+ `;
16
+
17
+ describe("chain serializer", () => {
18
+ it("round-trips step outputMode", () => {
19
+ const parsed = parseChain(chainContent, "project", "/tmp/review-chain.md");
20
+
21
+ assert.equal(parsed.steps[0]?.outputMode, "file-only");
22
+ assert.match(serializeChain(parsed), /outputMode: file-only/);
23
+ });
24
+
25
+ it("round-trips phase, label, as, and path-based outputSchema", () => {
26
+ const parsed = parseChain(`---
27
+ name: review-chain
28
+ description: Review chain
29
+ ---
30
+
31
+ ## reviewer
32
+ phase: Review
33
+ label: correctness pass
34
+ as: correctnessFindings
35
+ outputSchema: ./schemas/finding.schema.json
36
+
37
+ Review the diff
38
+ `, "project", "/tmp/review-chain.md");
39
+
40
+ assert.equal(parsed.steps[0]?.phase, "Review");
41
+ assert.equal(parsed.steps[0]?.label, "correctness pass");
42
+ assert.equal(parsed.steps[0]?.as, "correctnessFindings");
43
+ assert.equal(parsed.steps[0]?.outputSchema, "./schemas/finding.schema.json");
44
+ const serialized = serializeChain(parsed);
45
+ assert.match(serialized, /phase: Review/);
46
+ assert.match(serialized, /label: correctness pass/);
47
+ assert.match(serialized, /as: correctnessFindings/);
48
+ assert.match(serialized, /outputSchema: \.\/schemas\/finding\.schema\.json/);
49
+ });
50
+
51
+ it("round-trips markdown chain toolBudget", () => {
52
+ const parsed = parseChain(`---
53
+ name: review-chain
54
+ description: Review chain
55
+ ---
56
+
57
+ ## reviewer
58
+ toolBudget: {"soft":3,"hard":5,"block":["read","grep"]}
59
+
60
+ Review the diff
61
+ `, "project", "/tmp/review-chain.md");
62
+
63
+ assert.deepEqual(parsed.steps[0]?.toolBudget, { soft: 3, hard: 5, block: ["read", "grep"] });
64
+ assert.match(serializeChain(parsed), /toolBudget: \{"soft":3,"hard":5,"block":\["read","grep"\]\}/);
65
+ });
66
+
67
+ it("rejects invalid markdown chain toolBudget", () => {
68
+ assert.throws(
69
+ () => parseChain(`---
70
+ name: review-chain
71
+ description: Review chain
72
+ ---
73
+
74
+ ## reviewer
75
+ toolBudget: {"soft":6,"hard":5}
76
+
77
+ Review the diff
78
+ `, "project", "/tmp/review-chain.md"),
79
+ /toolBudget for step 'reviewer'\.soft must be <= toolBudget for step 'reviewer'\.hard/,
80
+ );
81
+ });
82
+
83
+ it("rejects inline outputSchema values in markdown chains", () => {
84
+ assert.throws(
85
+ () => parseChain(`---
86
+ name: review-chain
87
+ description: Review chain
88
+ ---
89
+
90
+ ## reviewer
91
+ outputSchema: {"type":"object"}
92
+
93
+ Review the diff
94
+ `, "project", "/tmp/review-chain.md"),
95
+ /Inline outputSchema values are not supported/,
96
+ );
97
+ });
98
+
99
+ it("rejects invalid dynamic JSON chains with useful diagnostics", () => {
100
+ assert.throws(
101
+ () => parseJsonChain(JSON.stringify({
102
+ name: "bad-dynamic-review",
103
+ description: "Bad dynamic targets",
104
+ chain: [
105
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
106
+ {
107
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
108
+ parallel: [{ agent: "reviewer", task: "Review" }],
109
+ collect: { as: "reviews" },
110
+ },
111
+ ],
112
+ }), "project", "/tmp/bad-dynamic-review.chain.json"),
113
+ /static parallel arrays/,
114
+ );
115
+ assert.throws(
116
+ () => parseJsonChain(JSON.stringify({ name: "bad", description: "Bad", chain: [1] }), "project", "/tmp/bad.chain.json"),
117
+ /step 1 must be an object/,
118
+ );
119
+ });
120
+
121
+ it("serializes JSON chains back to JSON", () => {
122
+ const parsed = parseJsonChain(JSON.stringify({
123
+ name: "dynamic-review",
124
+ package: "code-analysis",
125
+ description: "Review dynamic targets",
126
+ chain: [
127
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
128
+ {
129
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
130
+ parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
131
+ collect: { as: "reviews" },
132
+ },
133
+ ],
134
+ }), "project", "/tmp/dynamic-review.chain.json");
135
+
136
+ const serialized = serializeJsonChain(parsed);
137
+ assert.doesNotMatch(serialized, /^---/);
138
+ const reparsed = JSON.parse(serialized) as { name?: string; package?: string; chain?: Array<{ collect?: { as?: string } }> };
139
+ assert.equal(reparsed.name, "dynamic-review");
140
+ assert.equal(reparsed.package, "code-analysis");
141
+ assert.equal(reparsed.chain?.[1]?.collect?.as, "reviews");
142
+ });
143
+
144
+ it("parses declarative JSON chains with dynamic fanout toolBudget", () => {
145
+ const parsed = parseJsonChain(JSON.stringify({
146
+ name: "dynamic-review",
147
+ description: "Review dynamic targets",
148
+ chain: [
149
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" }, toolBudget: { hard: 4 } },
150
+ {
151
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
152
+ parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" }, toolBudget: { soft: 3, hard: 5 } },
153
+ collect: { as: "reviews" },
154
+ },
155
+ ],
156
+ }), "project", "/tmp/dynamic-review.chain.json");
157
+
158
+ assert.deepEqual((parsed.steps[0] as { toolBudget?: unknown }).toolBudget, { hard: 4 });
159
+ assert.deepEqual((parsed.steps[1] as { parallel?: { toolBudget?: unknown } }).parallel?.toolBudget, { soft: 3, hard: 5 });
160
+ });
161
+
162
+ it("rejects invalid JSON chain toolBudget", () => {
163
+ assert.throws(
164
+ () => parseJsonChain(JSON.stringify({
165
+ name: "bad-tool-budget",
166
+ description: "Bad tool budget",
167
+ chain: [{ agent: "worker", toolBudget: { hard: 0 } }],
168
+ }), "project", "/tmp/bad-tool-budget.chain.json"),
169
+ /step 1 toolBudget\.hard must be an integer >= 1/,
170
+ );
171
+ assert.throws(
172
+ () => parseJsonChain(JSON.stringify({
173
+ name: "bad-dynamic-tool-budget",
174
+ description: "Bad dynamic tool budget",
175
+ chain: [
176
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
177
+ { expand: { from: { output: "targets", path: "/items" }, maxItems: 4 }, parallel: { agent: "worker", toolBudget: { hard: 3, block: [] } }, collect: { as: "reviews" } },
178
+ ],
179
+ }), "project", "/tmp/bad-dynamic-tool-budget.chain.json"),
180
+ /step 2 dynamic template toolBudget\.block must contain at least one tool name/,
181
+ );
182
+ });
183
+
184
+ it("parses declarative JSON chains with dynamic fanout", () => {
185
+ const parsed = parseJsonChain(JSON.stringify({
186
+ name: "dynamic-review",
187
+ description: "Review dynamic targets",
188
+ chain: [
189
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
190
+ {
191
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
192
+ parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
193
+ collect: { as: "reviews" },
194
+ },
195
+ ],
196
+ }), "project", "/tmp/dynamic-review.chain.json");
197
+
198
+ assert.equal(parsed.name, "dynamic-review");
199
+ assert.equal(parsed.steps.length, 2);
200
+ assert.deepEqual((parsed.steps[1] as { collect?: { as?: string } }).collect, { as: "reviews" });
201
+ assert.deepEqual((parsed.steps[0] as { outputSchema?: unknown }).outputSchema, { type: "object" });
202
+ });
203
+
204
+ it("parses and validates acceptance in JSON chains", () => {
205
+ const parsed = parseJsonChain(JSON.stringify({
206
+ name: "accepted-chain",
207
+ description: "Chain with acceptance gates",
208
+ chain: [
209
+ { agent: "worker", task: "Fix bug", acceptance: { level: "checked", evidence: ["changed-files", "commands-run"] } },
210
+ {
211
+ parallel: [
212
+ { agent: "reviewer", task: "Review", acceptance: "attested" },
213
+ ],
214
+ },
215
+ ],
216
+ }), "project", "/tmp/accepted-chain.chain.json");
217
+
218
+ assert.deepEqual((parsed.steps[0] as { acceptance?: unknown }).acceptance, { level: "checked", evidence: ["changed-files", "commands-run"] });
219
+ assert.equal(((parsed.steps[1] as { parallel?: Array<{ acceptance?: unknown }> }).parallel?.[0]?.acceptance), "attested");
220
+ assert.throws(
221
+ () => parseJsonChain(JSON.stringify({
222
+ name: "bad-acceptance",
223
+ description: "Bad acceptance",
224
+ chain: [{ agent: "worker", acceptance: { level: "none" } }],
225
+ }), "project", "/tmp/bad-acceptance.chain.json"),
226
+ /step 1 acceptance\.reason is required/,
227
+ );
228
+ });
229
+ });
@@ -0,0 +1,225 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { createChildTranscriptWriter, CHILD_TRANSCRIPT_ARTIFACT_VERSION } from "../../src/shared/child-transcript.ts";
7
+
8
+ function tmpDir(): string {
9
+ return fs.mkdtempSync(path.join(os.tmpdir(), "child-transcript-test-"));
10
+ }
11
+
12
+ function readRecords(file: string): Record<string, unknown>[] {
13
+ return fs.readFileSync(file, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line) as Record<string, unknown>);
14
+ }
15
+
16
+ describe("createChildTranscriptWriter", () => {
17
+ it("writes a versioned initial user message record with run metadata", () => {
18
+ const dir = tmpDir();
19
+ const transcriptPath = path.join(dir, "out", "run-1_worker_transcript.jsonl");
20
+ const writer = createChildTranscriptWriter({
21
+ transcriptPath,
22
+ source: "foreground",
23
+ runId: "run-1",
24
+ agent: "worker",
25
+ childIndex: 0,
26
+ cwd: "/repo",
27
+ });
28
+ writer.writeInitialUserMessage("do the thing");
29
+
30
+ assert.equal(writer.getError(), undefined);
31
+ const records = readRecords(transcriptPath);
32
+ assert.equal(records.length, 1);
33
+ const record = records[0]!;
34
+ assert.equal(record.version, CHILD_TRANSCRIPT_ARTIFACT_VERSION);
35
+ assert.equal(record.recordType, "message");
36
+ assert.equal(record.source, "foreground");
37
+ assert.equal(record.runId, "run-1");
38
+ assert.equal(record.agent, "worker");
39
+ assert.equal(record.childIndex, 0);
40
+ assert.equal(record.cwd, "/repo");
41
+ assert.equal(record.sourceEventType, "initial_prompt");
42
+ assert.equal(record.role, "user");
43
+ assert.equal(record.text, "do the thing");
44
+ assert.equal(typeof record.ts, "number");
45
+ assert.equal(typeof record.timestamp, "string");
46
+ });
47
+
48
+ it("omits childIndex when not provided", () => {
49
+ const dir = tmpDir();
50
+ const transcriptPath = path.join(dir, "run_transcript.jsonl");
51
+ const writer = createChildTranscriptWriter({
52
+ transcriptPath,
53
+ source: "async",
54
+ runId: "run-2",
55
+ agent: "reviewer",
56
+ cwd: "/repo",
57
+ });
58
+ writer.writeInitialUserMessage("review it");
59
+ const record = readRecords(transcriptPath)[0]!;
60
+ assert.equal(Object.prototype.hasOwnProperty.call(record, "childIndex"), false);
61
+ assert.equal(record.source, "async");
62
+ });
63
+
64
+ it("writes message records for message_end and tool_result_end events", () => {
65
+ const dir = tmpDir();
66
+ const transcriptPath = path.join(dir, "transcript.jsonl");
67
+ const writer = createChildTranscriptWriter({
68
+ transcriptPath,
69
+ source: "async",
70
+ runId: "run-3",
71
+ agent: "worker",
72
+ childIndex: 1,
73
+ cwd: "/repo",
74
+ });
75
+ writer.writeChildEvent({
76
+ type: "message_end",
77
+ message: {
78
+ role: "assistant",
79
+ content: [{ type: "text", text: "all done" }],
80
+ model: "gpt-5.5",
81
+ stopReason: "end_turn",
82
+ usage: { input: 10, output: 5, cost: { total: 0.01 } },
83
+ },
84
+ });
85
+ writer.writeChildEvent({
86
+ type: "tool_result_end",
87
+ message: { role: "toolResult", toolName: "bash", content: [{ type: "text", text: "ok" }] },
88
+ });
89
+
90
+ const records = readRecords(transcriptPath);
91
+ assert.equal(records.length, 2);
92
+ assert.equal(records[0]!.recordType, "message");
93
+ assert.equal(records[0]!.sourceEventType, "message_end");
94
+ assert.equal(records[0]!.role, "assistant");
95
+ assert.equal(records[0]!.text, "all done");
96
+ assert.equal(records[0]!.model, "gpt-5.5");
97
+ assert.equal(records[0]!.stopReason, "end_turn");
98
+ assert.deepEqual(records[0]!.usage, { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: 0.01 });
99
+ assert.equal(records[1]!.recordType, "message");
100
+ assert.equal(records[1]!.sourceEventType, "tool_result_end");
101
+ assert.equal(records[1]!.role, "toolResult");
102
+ });
103
+
104
+ it("writes tool_start and tool_end records and ignores unhandled event types", () => {
105
+ const dir = tmpDir();
106
+ const transcriptPath = path.join(dir, "transcript.jsonl");
107
+ const writer = createChildTranscriptWriter({
108
+ transcriptPath,
109
+ source: "foreground",
110
+ runId: "run-4",
111
+ agent: "worker",
112
+ cwd: "/repo",
113
+ });
114
+ writer.writeChildEvent({ type: "tool_execution_start", toolName: "bash", args: { command: "ls" } });
115
+ writer.writeChildEvent({ type: "tool_execution_start", toolName: "read" });
116
+ writer.writeChildEvent({ type: "tool_execution_end", toolName: "bash" });
117
+ writer.writeChildEvent({ type: "some_unhandled_event" });
118
+
119
+ const records = readRecords(transcriptPath);
120
+ assert.equal(records.length, 3);
121
+ assert.equal(records[0]!.recordType, "tool_start");
122
+ assert.equal(records[0]!.sourceEventType, "tool_execution_start");
123
+ assert.equal(records[0]!.toolName, "bash");
124
+ assert.equal(typeof records[0]!.argsPreview, "string");
125
+ assert.ok(String(records[0]!.argsPreview).length > 0);
126
+ assert.equal(records[1]!.recordType, "tool_start");
127
+ assert.equal(Object.prototype.hasOwnProperty.call(records[1]!, "argsPreview"), false);
128
+ assert.equal(records[2]!.recordType, "tool_end");
129
+ assert.equal(records[2]!.toolName, "bash");
130
+ });
131
+
132
+ it("skips blank stdout/stderr lines and splits multi-line stderr text", () => {
133
+ const dir = tmpDir();
134
+ const transcriptPath = path.join(dir, "transcript.jsonl");
135
+ const writer = createChildTranscriptWriter({
136
+ transcriptPath,
137
+ source: "async",
138
+ runId: "run-5",
139
+ agent: "worker",
140
+ cwd: "/repo",
141
+ });
142
+ writer.writeStdoutLine("first line");
143
+ writer.writeStdoutLine(" ");
144
+ writer.writeStderrText("warn one\nwarn two\n\n");
145
+
146
+ const records = readRecords(transcriptPath);
147
+ assert.equal(records.length, 3);
148
+ assert.equal(records[0]!.recordType, "stdout");
149
+ assert.equal(records[0]!.text, "first line");
150
+ assert.equal(records[1]!.recordType, "stderr");
151
+ assert.equal(records[1]!.text, "warn one");
152
+ assert.equal(records[2]!.recordType, "stderr");
153
+ assert.equal(records[2]!.text, "warn two");
154
+ });
155
+
156
+ it("truncates after the byte cap and stops writing further records without erroring", () => {
157
+ const dir = tmpDir();
158
+ const transcriptPath = path.join(dir, "transcript.jsonl");
159
+ const writer = createChildTranscriptWriter({
160
+ transcriptPath,
161
+ source: "foreground",
162
+ runId: "run-6",
163
+ agent: "worker",
164
+ cwd: "/repo",
165
+ maxBytes: 900,
166
+ });
167
+ writer.writeInitialUserMessage("seed record that fits within the cap");
168
+ // This single line exceeds the remaining budget and triggers truncation.
169
+ writer.writeStdoutLine("x".repeat(5_000));
170
+ // Further writes must be no-ops.
171
+ writer.writeStdoutLine("after truncation");
172
+
173
+ assert.equal(writer.getError(), undefined);
174
+ const records = readRecords(transcriptPath);
175
+ const types = records.map((r) => r.recordType);
176
+ assert.ok(types.includes("message"), "seed message should be present");
177
+ assert.ok(types.includes("truncated"), "a truncated marker should be recorded");
178
+ assert.ok(!types.includes("stdout"), "no stdout records should be written once truncated");
179
+ });
180
+
181
+ it("reserves space for a truncation marker before accepting another record", () => {
182
+ const dir = tmpDir();
183
+ const transcriptPath = path.join(dir, "transcript.jsonl");
184
+ const writer = createChildTranscriptWriter({
185
+ transcriptPath,
186
+ source: "foreground",
187
+ runId: "r",
188
+ agent: "a",
189
+ cwd: "/repo",
190
+ maxBytes: 420,
191
+ });
192
+ writer.writeStdoutLine("a");
193
+ writer.writeStdoutLine("b");
194
+ writer.writeStdoutLine("after truncation");
195
+
196
+ assert.equal(writer.getError(), undefined);
197
+ const records = readRecords(transcriptPath);
198
+ assert.deepEqual(records.map((record) => record.recordType), ["stdout", "truncated"]);
199
+ assert.equal(records[0]!.text, "a");
200
+ assert.equal(records[1]!.maxBytes, 420);
201
+ assert.ok(fs.statSync(transcriptPath).size <= 420);
202
+ });
203
+
204
+ it("reports an init error and drops subsequent writes when the transcript path cannot be created", () => {
205
+ const dir = tmpDir();
206
+ // Precreate a regular file where a directory is expected so mkdirSync fails deterministically.
207
+ const blocker = path.join(dir, "blocker-file");
208
+ fs.writeFileSync(blocker, "blocks the transcript dir", "utf-8");
209
+ const transcriptPath = path.join(blocker, "transcript.jsonl");
210
+
211
+ const writer = createChildTranscriptWriter({
212
+ transcriptPath,
213
+ source: "foreground",
214
+ runId: "run-7",
215
+ agent: "worker",
216
+ cwd: "/repo",
217
+ });
218
+ writer.writeInitialUserMessage("should not be written");
219
+
220
+ const error = writer.getError();
221
+ assert.ok(typeof error === "string" && error.length > 0, "an init error should be reported");
222
+ assert.ok(error.includes(transcriptPath) || error.includes("blocker-file"), "error should reference the transcript path");
223
+ assert.equal(fs.existsSync(transcriptPath), false);
224
+ });
225
+ });