@zgeoff/atc 3.3.1 → 3.4.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 (206) hide show
  1. package/package.json +5 -3
  2. package/src/agents/agent-adapter.ts +10 -0
  3. package/src/agents/build-agent-adapters.ts +21 -3
  4. package/src/agents/build-cli-argv.ts +2 -4
  5. package/src/agents/claude-adapter.ts +13 -3
  6. package/src/agents/codex-adapter.ts +31 -2
  7. package/src/agents/gateway-adapter.ts +20 -8
  8. package/src/agents/grok-adapter.ts +16 -13
  9. package/src/agents/load-claude-config-bundle.ts +6 -6
  10. package/src/agents/print-codex-hook-file.ts +7 -3
  11. package/src/agents/print-grok-hook-file.ts +8 -3
  12. package/src/agents/render-sdk-message.ts +57 -0
  13. package/src/agents/resolve-agent-home.ts +7 -3
  14. package/src/agents/resolve-claude-global-config-path.ts +4 -4
  15. package/src/agents/start-claude-headless-run.ts +2 -57
  16. package/src/agents/update-claude-project-trust.ts +41 -30
  17. package/src/agents/with-claude-config-lock.ts +37 -12
  18. package/src/agents/write-atc-bridge.ts +10 -7
  19. package/src/agents/write-hook-settings.ts +13 -7
  20. package/src/client/boot-daemon.ts +122 -39
  21. package/src/client/dirs.ts +4 -2
  22. package/src/client/index.ts +17 -1
  23. package/src/client/log-client-event.ts +20 -0
  24. package/src/client/resolve-workspace-root.ts +10 -5
  25. package/src/client/spawn-picker.ts +200 -121
  26. package/src/client/ui.ts +7 -7
  27. package/src/clients.ts +41 -11
  28. package/src/daemon/daemon.ts +143 -33
  29. package/src/daemon/event-signal.ts +19 -3
  30. package/src/daemon/make-hook-runner.ts +82 -22
  31. package/src/daemon/permission-registry.ts +11 -6
  32. package/src/daemon/restore-fleet.ts +62 -17
  33. package/src/daemon/run-eject-handoff.ts +20 -2
  34. package/src/daemon/session-runtime.ts +0 -4
  35. package/src/daemon/sessions.ts +6 -0
  36. package/src/daemon/start-tcp-listener.ts +12 -10
  37. package/src/federation/daemon-caller.ts +25 -4
  38. package/src/federation/daemon-pool.ts +3 -1
  39. package/src/federation/open-gateway-caller.ts +6 -1
  40. package/src/gateway.ts +2 -179
  41. package/src/grants.ts +36 -11
  42. package/src/hook-report.ts +17 -3
  43. package/src/mcp/read-report-texts.ts +11 -7
  44. package/src/mcp/start-mcp-http-server.ts +5 -1
  45. package/src/mcp-http-server.ts +88 -32
  46. package/src/report.ts +21 -3
  47. package/src/run-bridge-tap.ts +36 -7
  48. package/src/run-config-migrate.ts +35 -11
  49. package/src/run-gateway-cli.ts +327 -0
  50. package/src/run-gateway.ts +51 -18
  51. package/src/shared/build-atc-command.ts +7 -3
  52. package/src/shared/bun-sqlite-driver.ts +10 -0
  53. package/src/shared/claim-daemon-lock.ts +10 -4
  54. package/src/shared/collect-dir-roots.ts +6 -6
  55. package/src/shared/collect-hooks.ts +7 -6
  56. package/src/shared/config.ts +19 -9
  57. package/src/shared/system-clock.ts +24 -0
  58. package/src/sources/github/collect-github-repos.ts +1 -1
  59. package/src/sources/github/read-git-protocol.ts +1 -1
  60. package/src/sources/github/run-gh.ts +24 -9
  61. package/src/store/state-store.ts +57 -7
  62. package/src/tap.ts +44 -13
  63. package/src/test-utils/build-mock-agent-adapter.ts +25 -0
  64. package/src/test-utils/build-mock-agent-entry.ts +32 -0
  65. package/src/test-utils/build-mock-auth-binding.ts +28 -0
  66. package/src/test-utils/build-mock-auth-profile.ts +62 -0
  67. package/src/test-utils/build-mock-daemon-record.ts +26 -0
  68. package/src/test-utils/build-mock-execution-target.ts +25 -0
  69. package/src/test-utils/build-mock-fleet-entry.ts +33 -0
  70. package/src/test-utils/build-mock-gateway-config.ts +32 -0
  71. package/src/test-utils/build-mock-imp-identity.ts +18 -0
  72. package/src/test-utils/build-mock-imp-session-request.ts +53 -0
  73. package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
  74. package/src/test-utils/build-mock-message-record.ts +26 -0
  75. package/src/test-utils/build-mock-mirror-session.ts +42 -0
  76. package/src/test-utils/build-mock-registry-daemon.ts +28 -0
  77. package/src/test-utils/build-mock-session-descriptor.ts +66 -0
  78. package/src/test-utils/build-mock-session.ts +43 -0
  79. package/src/test-utils/build-mock-sortable-session-view.ts +20 -0
  80. package/src/test-utils/build-mock-stored-event.ts +20 -0
  81. package/src/test-utils/build-mock-stored-row.ts +23 -0
  82. package/src/test-utils/build-stub-attention-adapter.ts +22 -0
  83. package/src/test-utils/build-stub-bridge-context.ts +20 -0
  84. package/src/test-utils/build-stub-brokered-agent-adapter.ts +96 -0
  85. package/src/test-utils/build-stub-brokered-gateway-adapter.ts +50 -0
  86. package/src/test-utils/build-stub-channel-opener.ts +45 -0
  87. package/src/test-utils/build-stub-claude-headless-run.ts +13 -0
  88. package/src/test-utils/build-stub-client-screens.ts +33 -0
  89. package/src/test-utils/build-stub-clock.ts +67 -0
  90. package/src/test-utils/build-stub-daemon-context.ts +81 -0
  91. package/src/test-utils/build-stub-daemon-requests.ts +103 -0
  92. package/src/test-utils/{fixture-dir-provider.ts → build-stub-dir-provider.ts} +20 -12
  93. package/src/test-utils/build-stub-execution-provider.ts +112 -0
  94. package/src/test-utils/build-stub-fleet-caller.ts +60 -0
  95. package/src/test-utils/build-stub-forking-gh.ts +10 -0
  96. package/src/test-utils/build-stub-gh.ts +50 -0
  97. package/src/test-utils/build-stub-handoff-daemon.ts +11 -0
  98. package/src/test-utils/build-stub-harness-relay.ts +50 -0
  99. package/src/test-utils/build-stub-headless-runner.ts +60 -0
  100. package/src/test-utils/build-stub-held-gh.ts +19 -0
  101. package/src/test-utils/build-stub-held-inbox-source.ts +36 -0
  102. package/src/test-utils/build-stub-held-provider.ts +58 -0
  103. package/src/test-utils/build-stub-held-zoxide.ts +13 -0
  104. package/src/test-utils/build-stub-host-hold.ts +31 -0
  105. package/src/test-utils/build-stub-host-provider.ts +26 -0
  106. package/src/test-utils/build-stub-log.ts +15 -0
  107. package/src/test-utils/build-stub-mcp-claude.ts +26 -0
  108. package/src/test-utils/build-stub-mcp-grok.ts +19 -0
  109. package/src/test-utils/build-stub-mcp-stdio-server.ts +20 -0
  110. package/src/test-utils/build-stub-peer-socket.ts +81 -0
  111. package/src/test-utils/build-stub-picker-host.ts +36 -0
  112. package/src/test-utils/build-stub-proxied-agent-adapter.ts +29 -0
  113. package/src/test-utils/build-stub-pty-provider.ts +53 -0
  114. package/src/test-utils/build-stub-recording-atc.ts +10 -0
  115. package/src/test-utils/build-stub-recording-filter.ts +9 -0
  116. package/src/test-utils/build-stub-recording-git.ts +8 -0
  117. package/src/test-utils/build-stub-signed-in-gh.ts +18 -0
  118. package/src/test-utils/build-stub-signed-out-gh.ts +8 -0
  119. package/src/test-utils/build-stub-soft-kill-provider.ts +29 -0
  120. package/src/test-utils/build-stub-targets.ts +58 -0
  121. package/src/test-utils/build-stub-terminal.ts +19 -0
  122. package/src/test-utils/build-stub-timeout-scheduler.ts +48 -0
  123. package/src/test-utils/build-stub-tui-claude.ts +47 -0
  124. package/src/test-utils/build-stub-tui-grok.ts +53 -0
  125. package/src/test-utils/build-stub-wait-clock.ts +23 -0
  126. package/src/test-utils/build-target-options-from-config.ts +37 -0
  127. package/src/test-utils/collect-process-tree.ts +75 -0
  128. package/src/test-utils/create-git-fixture.ts +117 -0
  129. package/src/test-utils/create-migrated-state-db.ts +12 -10
  130. package/src/test-utils/create-stub-bin.ts +19 -0
  131. package/src/test-utils/create-stub-claude.ts +75 -0
  132. package/src/test-utils/create-stub-codex.ts +31 -0
  133. package/src/test-utils/create-stub-composer.ts +65 -0
  134. package/src/test-utils/create-stub-echo-claude.ts +21 -0
  135. package/src/test-utils/create-stub-failing-agent-adapter.ts +111 -0
  136. package/src/test-utils/create-stub-grok.ts +44 -0
  137. package/src/test-utils/create-stub-guest-clis.ts +55 -0
  138. package/src/test-utils/create-stub-harness-guest.ts +35 -0
  139. package/src/test-utils/{fixture-imp-port.ts → create-stub-imp-port.ts} +144 -63
  140. package/src/test-utils/create-stub-recording-claude.ts +20 -0
  141. package/src/test-utils/create-stub-recording-codex.ts +20 -0
  142. package/src/test-utils/create-stub-shared-server-codex.ts +56 -0
  143. package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +15 -9
  144. package/src/test-utils/get-gateway-config.ts +3 -3
  145. package/src/test-utils/get-only-effect-ref.ts +21 -0
  146. package/src/test-utils/get-only-imp-name.ts +19 -0
  147. package/src/test-utils/get-records.ts +20 -0
  148. package/src/test-utils/get-string.ts +14 -0
  149. package/src/test-utils/keys.ts +26 -0
  150. package/src/test-utils/merge-deep.ts +36 -0
  151. package/src/test-utils/mock-overrides.ts +13 -0
  152. package/src/test-utils/open-repo-step.ts +24 -0
  153. package/src/test-utils/read-query-plan.ts +22 -0
  154. package/src/test-utils/register-test-cleanup.ts +63 -0
  155. package/src/test-utils/resolve-atc-command.ts +13 -0
  156. package/src/test-utils/resolve-gateway-command.ts +13 -0
  157. package/src/test-utils/run-atc.ts +56 -0
  158. package/src/test-utils/run-legacy-daemon.ts +27 -5
  159. package/src/test-utils/run-listener-daemon.ts +5 -10
  160. package/src/test-utils/run-mcp-authorization.ts +7 -4
  161. package/src/test-utils/run-source-daemon.ts +2 -2
  162. package/src/test-utils/seed-faker.ts +7 -0
  163. package/src/test-utils/send-lines-before-handshake.ts +35 -0
  164. package/src/test-utils/send-mcp-request.ts +25 -0
  165. package/src/test-utils/setup-mcp-home.ts +110 -0
  166. package/src/test-utils/setup-mcp-http.ts +26 -30
  167. package/src/test-utils/setup-temp-dir.ts +7 -6
  168. package/src/test-utils/spawn-claude-session.ts +34 -0
  169. package/src/test-utils/spawn-grok-session.ts +39 -0
  170. package/src/test-utils/start-cut-proxy.ts +29 -8
  171. package/src/test-utils/start-daemon-process.ts +193 -0
  172. package/src/test-utils/start-git-http-server.ts +28 -4
  173. package/src/test-utils/start-mcp-stdio.ts +190 -0
  174. package/src/test-utils/start-output-capture.ts +20 -0
  175. package/src/test-utils/start-stub-answering-listener.ts +65 -0
  176. package/src/test-utils/start-stub-closing-listener.ts +31 -0
  177. package/src/test-utils/start-stub-dropping-daemon.ts +105 -0
  178. package/src/test-utils/start-stub-impd-info.ts +38 -0
  179. package/src/test-utils/start-stub-impd.ts +143 -0
  180. package/src/test-utils/start-stub-legacy-daemon.ts +171 -0
  181. package/src/test-utils/start-stub-mcp-server.ts +55 -0
  182. package/src/test-utils/start-stub-recording-listener.ts +47 -0
  183. package/src/test-utils/start-stub-refusing-listener.ts +43 -0
  184. package/src/test-utils/start-stub-reporter-socket.ts +79 -0
  185. package/src/test-utils/start-stub-restore-daemon.ts +59 -0
  186. package/src/test-utils/start-stub-session-bridge.ts +100 -0
  187. package/src/test-utils/start-stub-silent-server.ts +38 -0
  188. package/src/test-utils/start-stub-source-daemon.ts +41 -0
  189. package/src/test-utils/start-stub-stalled-client.ts +61 -0
  190. package/src/test-utils/start-stub-stalled-listener.ts +44 -0
  191. package/src/test-utils/start-stub-tap.ts +38 -0
  192. package/src/test-utils/start-stub-tcp-daemon.ts +75 -0
  193. package/src/test-utils/start-stub-telemetry-collector.ts +32 -0
  194. package/src/test-utils/start-stub-unanswering-listener.ts +41 -0
  195. package/src/test-utils/start-stub-unanswering-unix-listener.ts +38 -0
  196. package/src/test-utils/start-test-daemon.ts +229 -0
  197. package/src/test-utils/start-tui-harness.ts +291 -0
  198. package/src/test-utils/subscribe-to-socket-lines.ts +81 -14
  199. package/src/test-utils/try-send-request.ts +29 -0
  200. package/src/test-utils/wait-for-event.ts +41 -0
  201. package/src/test-utils/wait-for.ts +15 -3
  202. package/src/verify-restored-fleet.ts +30 -17
  203. package/src/workspace/check-repository-access.ts +4 -0
  204. package/src/workspace/collect-remote-refs.ts +4 -2
  205. package/src/workspace/run-git.ts +6 -0
  206. package/src/test-utils/start-legacy-daemon.ts +0 -111
@@ -0,0 +1,19 @@
1
+ import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * Creates a stand-in command-line tool: the script, shebang included, as an
6
+ * executable file under the directory, which it creates when missing.
7
+ * Returns the script's path. Writing over an existing file replaces its
8
+ * content and makes it executable as well, so a test can swap a tool's
9
+ * behaviour mid-run.
10
+ */
11
+ export function createStubBin(dir: string, name: string, script: string): string {
12
+ const path = join(dir, name);
13
+
14
+ mkdirSync(dir, { recursive: true });
15
+ writeFileSync(path, script);
16
+ chmodSync(path, 0o755);
17
+
18
+ return path;
19
+ }
@@ -0,0 +1,75 @@
1
+ import { createStubBin } from './create-stub-bin';
2
+
3
+ interface StubClaudeConfig {
4
+ // The command atc runs as, which the stub's tap and restart go through.
5
+ readonly atc: readonly string[];
6
+
7
+ // The composer script the stub finishes in when the home asks for it.
8
+ readonly composer: string;
9
+ }
10
+
11
+ /**
12
+ * Creates a stand-in for the Claude CLI as `fake-claude` under the
13
+ * directory and returns its path. Like the real CLI, it runs the
14
+ * `SessionStart` hook command from the `--settings` file atc passed it
15
+ * through a shell, feeding each hook its JSON payload on stdin, so a session
16
+ * reports with the command line atc wrote for its agent. It prints its
17
+ * arguments, its TERM, and whether it inherited a parent-session variable,
18
+ * appends its atc session id to `$HOME/fake-claude-starts.log`, reports a
19
+ * `SessionStart` as `fake-1` and a `Notification` asking for permission, then
20
+ * echoes each input line as `GOT:<line>`.
21
+ *
22
+ * Files in `$HOME` steer a scenario: `fake-claude-dies-<id>` exits at once
23
+ * when the stub resumes that agent session; `fake-claude-hold-start` reports
24
+ * nothing and only echoes input; `fake-claude-composer` runs the composer
25
+ * instead; `fake-claude-gate` waits for one input line before it reports,
26
+ * and the first stub to find it removes it; `fake-claude-own-id` reports the
27
+ * atc session id as the agent session; `fake-claude-restart` runs
28
+ * `atc daemon restart` into `$HOME/restart.out` after `SessionStart`, and the
29
+ * first stub to find it removes it; `fake-claude-tap` runs `atc tap` into
30
+ * `$HOME/tap.jsonl`; `fake-claude-events.jsonl` holds more hook payloads to
31
+ * report, one per line, after the `Notification`; `fake-claude-exit` exits
32
+ * once they are reported.
33
+ */
34
+ export function createStubClaude(dir: string, config: StubClaudeConfig): string {
35
+ const atc = config.atc.map((part) => `"${part}"`).join(' ');
36
+
37
+ return createStubBin(
38
+ dir,
39
+ 'fake-claude',
40
+ `#!/usr/bin/env bash
41
+ echo "FAKE_CLAUDE_UP args: $@"
42
+ echo "FAKE_CLAUDE_TERM:[\${TERM-unset}]"
43
+ echo "FAKE_CLAUDE_PARENT:[\${CLAUDE_CODE_ATC_TEST-unset}]"
44
+ echo "$ATC_SESSION_ID" >> "$HOME/fake-claude-starts.log"
45
+ settings=""
46
+ resume=""
47
+ prev=""
48
+ for arg in "$@"; do
49
+ if [ "$prev" = "--settings" ]; then settings="$arg"; fi
50
+ if [ "$prev" = "--resume" ]; then resume="$arg"; fi
51
+ prev="$arg"
52
+ done
53
+ if [ -n "$resume" ] && [ -f "$HOME/fake-claude-dies-$resume" ]; then exit 0; fi
54
+ if [ -f "$HOME/fake-claude-hold-start" ]; then while read -r line; do echo "GOT:$line"; done; exit 0; fi
55
+ if [ -f "$HOME/fake-claude-composer" ]; then exec "${process.execPath}" "${config.composer}"; fi
56
+ hookCommand="$("${process.execPath}" -e 'const s = JSON.parse(require("fs").readFileSync(process.argv.at(-1), "utf8")); console.log(s.hooks.SessionStart[0].hooks[0].command)' "$settings" < /dev/null)"
57
+ hookReport() { sh -c "$hookCommand"; }
58
+ sessionID="fake-1"
59
+ if [ -f "$HOME/fake-claude-own-id" ]; then sessionID="$ATC_SESSION_ID"; fi
60
+ if [ -f "$HOME/fake-claude-gate" ]; then rm "$HOME/fake-claude-gate"; read -r _; fi
61
+ printf '{"hook_event_name":"SessionStart","session_id":"%s","transcript_path":"%s/fake-transcript.jsonl"}' "$sessionID" "$HOME" | hookReport
62
+ if [ -f "$HOME/fake-claude-restart" ]; then rm "$HOME/fake-claude-restart"; ${atc} daemon restart > "$HOME/restart.out" 2>&1; fi
63
+ if [ -f "$HOME/fake-claude-tap" ]; then ${atc} tap --session "$ATC_SESSION_ID" >> "$HOME/tap.jsonl" & fi
64
+ printf '{"hook_event_name":"Notification","session_id":"%s","message":"needs permission"}' "$sessionID" | hookReport
65
+ if [ -f "$HOME/fake-claude-events.jsonl" ]; then
66
+ while IFS= read -r ev; do
67
+ [ -n "$ev" ] || continue
68
+ printf '%s' "$ev" | hookReport
69
+ done < "$HOME/fake-claude-events.jsonl"
70
+ fi
71
+ if [ -f "$HOME/fake-claude-exit" ]; then exit 0; fi
72
+ while read -r line; do echo "GOT:$line"; done
73
+ `,
74
+ );
75
+ }
@@ -0,0 +1,31 @@
1
+ import { createStubBin } from './create-stub-bin';
2
+
3
+ interface StubCodexConfig {
4
+ // The command atc runs as; the stub reports each hook through its
5
+ // `hook-report --agent codex`, as the hook entries atc prints for Codex do.
6
+ readonly atc: readonly string[];
7
+
8
+ // The composer script the stub finishes in.
9
+ readonly composer: string;
10
+ }
11
+
12
+ /**
13
+ * Creates a stand-in for the Codex CLI as `fake-codex` under the directory
14
+ * and returns its path. It prints its arguments, reports a `SessionStart`
15
+ * as `fake-codex-1` with its working directory and a rollout under `$HOME`,
16
+ * then a `Stop` whose last message is `pong`, then runs the composer.
17
+ */
18
+ export function createStubCodex(dir: string, config: StubCodexConfig): string {
19
+ const report = `${config.atc.map((part) => `"${part}"`).join(' ')} hook-report --agent codex`;
20
+
21
+ return createStubBin(
22
+ dir,
23
+ 'fake-codex',
24
+ `#!/usr/bin/env bash
25
+ echo "FAKE_CODEX_UP args: $@"
26
+ printf '{"hook_event_name":"SessionStart","session_id":"fake-codex-1","transcript_path":"%s/fake-rollout.jsonl","cwd":"%s","source":"startup"}' "$HOME" "$PWD" | ${report}
27
+ printf '{"hook_event_name":"Stop","session_id":"fake-codex-1","transcript_path":"%s/fake-rollout.jsonl","last_assistant_message":"pong"}' "$HOME" | ${report}
28
+ exec "${process.execPath}" "${config.composer}"
29
+ `,
30
+ );
31
+ }
@@ -0,0 +1,65 @@
1
+ import { createStubBin } from './create-stub-bin';
2
+
3
+ /**
4
+ * Creates the composer the stub agents finish in, as `fake-composer.js`
5
+ * under the directory, and returns its path; run it with bun. It is modelled
6
+ * on the TUIs Codex and Grok draw: raw mode with bracketed paste on. Each
7
+ * read is one input event batch. A bracketed paste lands in the composer
8
+ * whole, newlines kept; a lone CR outside a paste submits; a lone LF is
9
+ * Ctrl-J and adds a newline; any other read of more than one byte is a paste
10
+ * burst whose line breaks stay in the composer. A read that ends partway
11
+ * into a paste marker holds that part for the next read. It prints
12
+ * `FAKE_COMPOSER_READY` once paste mode is on, every byte received so far as
13
+ * `RECEIVED:<json>` after each read, and each submission as `SUBMIT:<json>`.
14
+ */
15
+ export function createStubComposer(dir: string): string {
16
+ return createStubBin(
17
+ dir,
18
+ 'fake-composer.js',
19
+ String.raw`const OPEN = '\u001B[200~';
20
+ const CLOSE = '\u001B[201~';
21
+ let composer = '';
22
+ let pasting = false;
23
+ let held = '';
24
+ let received = '';
25
+ process.stdin.setRawMode(true);
26
+ process.stdout.write('\u001B[?2004hFAKE_COMPOSER_READY\r\n');
27
+ process.stdin.on('data', (buf) => {
28
+ const chunk = buf.toString('utf8');
29
+ received += chunk;
30
+ process.stdout.write('RECEIVED:' + JSON.stringify(received) + '\r\n');
31
+ let rest = held + chunk;
32
+ held = '';
33
+ const cut = rest.lastIndexOf('\u001B');
34
+ const tail = cut === -1 ? '' : rest.slice(cut);
35
+ if (tail !== '' && tail.length < CLOSE.length && (OPEN.startsWith(tail) || CLOSE.startsWith(tail))) {
36
+ held = tail;
37
+ rest = rest.slice(0, cut);
38
+ }
39
+ while (rest !== '') {
40
+ if (pasting) {
41
+ const end = rest.indexOf(CLOSE);
42
+ composer += (end === -1 ? rest : rest.slice(0, end)).replaceAll('\r', '\n');
43
+ pasting = end === -1;
44
+ rest = end === -1 ? '' : rest.slice(end + CLOSE.length);
45
+ continue;
46
+ }
47
+ if (rest.startsWith(OPEN)) {
48
+ pasting = true;
49
+ rest = rest.slice(OPEN.length);
50
+ continue;
51
+ }
52
+ const next = rest.indexOf(OPEN);
53
+ const plain = next === -1 ? rest : rest.slice(0, next);
54
+ rest = next === -1 ? '' : rest.slice(next);
55
+ if (plain === '\r') {
56
+ process.stdout.write('SUBMIT:' + JSON.stringify(composer) + '\r\n');
57
+ composer = '';
58
+ } else {
59
+ composer += plain.replaceAll('\r', '\n');
60
+ }
61
+ }
62
+ });
63
+ `,
64
+ );
65
+ }
@@ -0,0 +1,21 @@
1
+ import { createStubBin } from './create-stub-bin';
2
+
3
+ /**
4
+ * Creates a stand-in for the Claude CLI as `fake-claude` under the directory
5
+ * and returns its path. It prints `UP:<pid>`, then reads one line at a time
6
+ * and prints `GOT:<line>:<pid>` after each, so a test sees which process
7
+ * took its input. A `quit` line ends it with exit code 3.
8
+ */
9
+ export function createStubEchoClaude(dir: string): string {
10
+ return createStubBin(
11
+ dir,
12
+ 'fake-claude',
13
+ `#!/usr/bin/env bash
14
+ echo "UP:$$"
15
+ while read -r line; do
16
+ if [ "$line" = "quit" ]; then exit 3; fi
17
+ echo "GOT:$line:$$"
18
+ done
19
+ `,
20
+ );
21
+ }
@@ -0,0 +1,111 @@
1
+ import type { AgentAdapter } from '../agents/agent-adapter';
2
+ import { buildMockAgentAdapter } from './build-mock-agent-adapter';
3
+
4
+ interface Plan {
5
+ readonly bin: string;
6
+ readonly args: readonly string[];
7
+ }
8
+
9
+ // A named pipe the first spawn's process writes its pid to once it is ready
10
+ // to be killed, and how long the first failing read waits for that pid.
11
+ interface ReadyPipe {
12
+ readonly path: string;
13
+ readonly timeoutMs: number;
14
+ }
15
+
16
+ interface FailingAgentAdapterConfig {
17
+ // What the first spawn runs, and what every later spawn runs.
18
+ readonly firstPlan: Plan;
19
+ readonly laterPlan: Plan;
20
+
21
+ // How many reads of the headless runner throw once the first spawn is
22
+ // planned: one fails the start after its process started, and a second
23
+ // fails the kill that takes the process back.
24
+ readonly failedReads: number;
25
+
26
+ // The pipe the first failing read waits on. Null fails at once.
27
+ readonly ready: ReadyPipe | null;
28
+ }
29
+
30
+ /**
31
+ * An agent adapter whose first spawn fails after its process has started:
32
+ * the daemon reads the adapter's headless runner once the process runs, and
33
+ * that read throws, as many times as the config holds, while every other
34
+ * read finds no headless runner. Every member but the spawn plan and the
35
+ * headless runner is the mock adapter's.
36
+ *
37
+ * With a ready pipe, the stub makes the pipe at that path, and the first
38
+ * failing read blocks until a process writes its pid there, which the
39
+ * headless runner read is synchronous for. A read that finds no writer
40
+ * within the timeout throws instead of failing the start. `getReadyPID`
41
+ * returns the pid that read took. `countPlans` reads how many spawns the
42
+ * adapter has planned.
43
+ */
44
+ export function createStubFailingAgentAdapter(config: FailingAgentAdapterConfig) {
45
+ let planned = 0;
46
+ let readsToFail = 0;
47
+ let readyPID: number | null = null;
48
+
49
+ if (config.ready !== null) {
50
+ createPipe(config.ready.path);
51
+ }
52
+
53
+ const adapter: AgentAdapter = {
54
+ ...buildMockAgentAdapter(),
55
+ planSpawn: () => {
56
+ planned += 1;
57
+
58
+ if (planned > 1) {
59
+ return { bin: config.laterPlan.bin, args: [...config.laterPlan.args] };
60
+ }
61
+
62
+ readsToFail = config.failedReads;
63
+
64
+ return { bin: config.firstPlan.bin, args: [...config.firstPlan.args] };
65
+ },
66
+ get headlessRunner() {
67
+ if (readsToFail === 0) {
68
+ return null;
69
+ }
70
+
71
+ if (readsToFail === config.failedReads && config.ready !== null) {
72
+ readyPID = readPID(config.ready);
73
+ }
74
+
75
+ readsToFail -= 1;
76
+ throw new Error('adapter failed after the process started');
77
+ },
78
+ };
79
+
80
+ return {
81
+ adapter,
82
+ countPlans: () => planned,
83
+ getReadyPID: (): number => {
84
+ if (readyPID === null) {
85
+ throw new Error('no process has written its pid to the ready pipe');
86
+ }
87
+
88
+ return readyPID;
89
+ },
90
+ };
91
+ }
92
+
93
+ function createPipe(path: string): void {
94
+ const made = Bun.spawnSync(['mkfifo', path]);
95
+
96
+ if (made.exitCode !== 0) {
97
+ throw new Error(`mkfifo could not make ${path} (${made.stderr.toString().trim()})`);
98
+ }
99
+ }
100
+
101
+ // Reading the pipe blocks until a writer opens it and closes it, so the
102
+ // read returns once the process has written its pid.
103
+ function readPID(ready: ReadyPipe): number {
104
+ const read = Bun.spawnSync({ cmd: ['cat', ready.path], timeout: ready.timeoutMs });
105
+
106
+ if (read.exitedDueToTimeout === true) {
107
+ throw new Error(`no process wrote ${ready.path} within ${String(ready.timeoutMs)}ms`);
108
+ }
109
+
110
+ return Number(read.stdout.toString().trim());
111
+ }
@@ -0,0 +1,44 @@
1
+ import { createStubBin } from './create-stub-bin';
2
+
3
+ interface StubGrokConfig {
4
+ // The command atc runs as; the stub reports each hook through its
5
+ // `hook-report --agent grok`, as the hook file atc prints for Grok does.
6
+ readonly atc: readonly string[];
7
+
8
+ // The composer script the stub finishes in.
9
+ readonly composer: string;
10
+ }
11
+
12
+ /**
13
+ * Creates a stand-in for the Grok CLI as `fake-grok` under the directory and
14
+ * returns its path. It prints its arguments, reports a `session_start` as
15
+ * `fake-grok-1` with its working directory and a `notification` asking
16
+ * permission to edit, prints `FAKE_GROK_HOOKS_DONE` once its reports are
17
+ * sent, then runs the composer. Files in `$HOME` steer a scenario:
18
+ * `fake-grok-hold-start` reports nothing and only echoes input lines as
19
+ * `GOT:<line>`; `fake-grok-events.jsonl` holds the hook payloads to report
20
+ * after `session_start` in place of the notification, one per line.
21
+ */
22
+ export function createStubGrok(dir: string, config: StubGrokConfig): string {
23
+ const report = `${config.atc.map((part) => `"${part}"`).join(' ')} hook-report --agent grok`;
24
+
25
+ return createStubBin(
26
+ dir,
27
+ 'fake-grok',
28
+ `#!/usr/bin/env bash
29
+ echo "FAKE_GROK_UP args: $@"
30
+ if [ -f "$HOME/fake-grok-hold-start" ]; then while read -r line; do echo "GOT:$line"; done; exit 0; fi
31
+ printf '{"hookEventName":"session_start","sessionId":"fake-grok-1","cwd":"%s"}' "$PWD" | ${report}
32
+ if [ -f "$HOME/fake-grok-events.jsonl" ]; then
33
+ while IFS= read -r ev; do
34
+ [ -n "$ev" ] || continue
35
+ printf '%s' "$ev" | ${report}
36
+ done < "$HOME/fake-grok-events.jsonl"
37
+ else
38
+ printf '{"hookEventName":"notification","sessionId":"fake-grok-1","notificationType":"permission_prompt","message":"allow edit?"}' | ${report}
39
+ fi
40
+ echo "FAKE_GROK_HOOKS_DONE"
41
+ exec "${process.execPath}" "${config.composer}"
42
+ `,
43
+ );
44
+ }
@@ -0,0 +1,55 @@
1
+ import { join } from 'node:path';
2
+ import { createStubBin } from './create-stub-bin';
3
+
4
+ /**
5
+ * Creates the command-line tools a remote host runs for a session, as
6
+ * scripts under the directory: `atc` runs the atc CLI from this source tree,
7
+ * and `claude` is a stand-in agent that prints `UP:<pid>`, then reads one
8
+ * command per line and prints `GOT:<line>` after each, reporting through
9
+ * that atc:
10
+ *
11
+ * - `start <id>`: a SessionStart for agent session `<id>`, with a
12
+ * transcript at a path only the host holds;
13
+ * - `notify <text>`: a Notification carrying the text;
14
+ * - `nested <id>`: a SessionStart for agent session `<id>` from a Codex
15
+ * harness nested inside the session;
16
+ * - `forge <session>`: a Notification as atc session `<session>` instead
17
+ * of its own;
18
+ * - `tap <file>`: the session's tap, run in the background and printing
19
+ * into the file;
20
+ * - `answer <message> <text>`: the message answered with the text;
21
+ * - `note <text>`: a note labelled `progress`.
22
+ *
23
+ * Returns each tool's path.
24
+ */
25
+ export function createStubGuestCLIs(dir: string) {
26
+ const cliPath = join(import.meta.dir, '..', 'cli.ts');
27
+
28
+ const atc = createStubBin(
29
+ dir,
30
+ 'atc',
31
+ `#!/bin/sh\nexec "${process.execPath}" "${cliPath}" "$@"\n`,
32
+ );
33
+
34
+ const claude = createStubBin(
35
+ dir,
36
+ 'claude',
37
+ `#!/usr/bin/env bash
38
+ echo "UP:$$"
39
+ while read -r line; do
40
+ case "$line" in
41
+ start\\ *) printf '{"hook_event_name":"SessionStart","session_id":"%s","transcript_path":"/guest/only/transcript.jsonl"}' "\${line#start }" | "${atc}" hook-report --agent claude ;;
42
+ notify\\ *) printf '{"hook_event_name":"Notification","message":"%s"}' "\${line#notify }" | "${atc}" hook-report --agent claude ;;
43
+ nested\\ *) printf '{"hook_event_name":"SessionStart","session_id":"%s","source":"startup"}' "\${line#nested }" | "${atc}" hook-report --agent codex ;;
44
+ forge\\ *) echo '{"hook_event_name":"Notification","message":"forged"}' | ATC_SESSION_ID="\${line#forge }" "${atc}" hook-report --agent claude ;;
45
+ tap\\ *) "${atc}" tap --session "$ATC_SESSION_ID" >> "\${line#tap }" 2>&1 & ;;
46
+ answer\\ *) rest="\${line#answer }"; printf '%s' "\${rest#* }" | "${atc}" report answered --messages "\${rest%% *}" ;;
47
+ note\\ *) printf '%s' "\${line#note }" | "${atc}" report note --label progress ;;
48
+ esac
49
+ echo "GOT:$line"
50
+ done
51
+ `,
52
+ );
53
+
54
+ return { atc, claude };
55
+ }
@@ -0,0 +1,35 @@
1
+ import { join } from 'node:path';
2
+ import { createStubBin } from './create-stub-bin';
3
+
4
+ /**
5
+ * Creates a stand-in guest program for a harness to run, under the
6
+ * directory: a script that prints its pid and the terminal size it started
7
+ * at as `UP:<pid> START:<rows> <cols>`, echoes each line it reads as
8
+ * `GOT:<line>`, prints `SIZE:<rows> <cols>` on `size`, and exits 3 on
9
+ * `quit`. On `later` it waits for a line on the named pipe at `burstPath`,
10
+ * then prints 300000 `x` bytes, more than impd's ring keeps, a newline, and
11
+ * `BURST_DONE`. Returns the script's path and the pipe's.
12
+ */
13
+ export function createStubHarnessGuest(dir: string) {
14
+ const burstPath = join(dir, 'burst');
15
+
16
+ Bun.spawnSync(['mkfifo', burstPath]);
17
+
18
+ const path = createStubBin(
19
+ dir,
20
+ 'harness',
21
+ `#!/usr/bin/env bash
22
+ echo "UP:$$ START:$(stty size)"
23
+ while read -r line; do
24
+ if [ "$line" = "later" ]; then
25
+ (cat "${burstPath}" > /dev/null; head -c 300000 /dev/zero | tr '\\0' 'x'; echo; echo "BURST_DONE") &
26
+ fi
27
+ if [ "$line" = "size" ]; then echo "SIZE:$(stty size)"; fi
28
+ if [ "$line" = "quit" ]; then exit 3; fi
29
+ echo "GOT:$line"
30
+ done
31
+ `,
32
+ );
33
+
34
+ return { path, burstPath };
35
+ }