@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.
- package/package.json +5 -3
- package/src/agents/agent-adapter.ts +10 -0
- package/src/agents/build-agent-adapters.ts +21 -3
- package/src/agents/build-cli-argv.ts +2 -4
- package/src/agents/claude-adapter.ts +13 -3
- package/src/agents/codex-adapter.ts +31 -2
- package/src/agents/gateway-adapter.ts +20 -8
- package/src/agents/grok-adapter.ts +16 -13
- package/src/agents/load-claude-config-bundle.ts +6 -6
- package/src/agents/print-codex-hook-file.ts +7 -3
- package/src/agents/print-grok-hook-file.ts +8 -3
- package/src/agents/render-sdk-message.ts +57 -0
- package/src/agents/resolve-agent-home.ts +7 -3
- package/src/agents/resolve-claude-global-config-path.ts +4 -4
- package/src/agents/start-claude-headless-run.ts +2 -57
- package/src/agents/update-claude-project-trust.ts +41 -30
- package/src/agents/with-claude-config-lock.ts +37 -12
- package/src/agents/write-atc-bridge.ts +10 -7
- package/src/agents/write-hook-settings.ts +13 -7
- package/src/client/boot-daemon.ts +122 -39
- package/src/client/dirs.ts +4 -2
- package/src/client/index.ts +17 -1
- package/src/client/log-client-event.ts +20 -0
- package/src/client/resolve-workspace-root.ts +10 -5
- package/src/client/spawn-picker.ts +200 -121
- package/src/client/ui.ts +7 -7
- package/src/clients.ts +41 -11
- package/src/daemon/daemon.ts +143 -33
- package/src/daemon/event-signal.ts +19 -3
- package/src/daemon/make-hook-runner.ts +82 -22
- package/src/daemon/permission-registry.ts +11 -6
- package/src/daemon/restore-fleet.ts +62 -17
- package/src/daemon/run-eject-handoff.ts +20 -2
- package/src/daemon/session-runtime.ts +0 -4
- package/src/daemon/sessions.ts +6 -0
- package/src/daemon/start-tcp-listener.ts +12 -10
- package/src/federation/daemon-caller.ts +25 -4
- package/src/federation/daemon-pool.ts +3 -1
- package/src/federation/open-gateway-caller.ts +6 -1
- package/src/gateway.ts +2 -179
- package/src/grants.ts +36 -11
- package/src/hook-report.ts +17 -3
- package/src/mcp/read-report-texts.ts +11 -7
- package/src/mcp/start-mcp-http-server.ts +5 -1
- package/src/mcp-http-server.ts +88 -32
- package/src/report.ts +21 -3
- package/src/run-bridge-tap.ts +36 -7
- package/src/run-config-migrate.ts +35 -11
- package/src/run-gateway-cli.ts +327 -0
- package/src/run-gateway.ts +51 -18
- package/src/shared/build-atc-command.ts +7 -3
- package/src/shared/bun-sqlite-driver.ts +10 -0
- package/src/shared/claim-daemon-lock.ts +10 -4
- package/src/shared/collect-dir-roots.ts +6 -6
- package/src/shared/collect-hooks.ts +7 -6
- package/src/shared/config.ts +19 -9
- package/src/shared/system-clock.ts +24 -0
- package/src/sources/github/collect-github-repos.ts +1 -1
- package/src/sources/github/read-git-protocol.ts +1 -1
- package/src/sources/github/run-gh.ts +24 -9
- package/src/store/state-store.ts +57 -7
- package/src/tap.ts +44 -13
- package/src/test-utils/build-mock-agent-adapter.ts +25 -0
- package/src/test-utils/build-mock-agent-entry.ts +32 -0
- package/src/test-utils/build-mock-auth-binding.ts +28 -0
- package/src/test-utils/build-mock-auth-profile.ts +62 -0
- package/src/test-utils/build-mock-daemon-record.ts +26 -0
- package/src/test-utils/build-mock-execution-target.ts +25 -0
- package/src/test-utils/build-mock-fleet-entry.ts +33 -0
- package/src/test-utils/build-mock-gateway-config.ts +32 -0
- package/src/test-utils/build-mock-imp-identity.ts +18 -0
- package/src/test-utils/build-mock-imp-session-request.ts +53 -0
- package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
- package/src/test-utils/build-mock-message-record.ts +26 -0
- package/src/test-utils/build-mock-mirror-session.ts +42 -0
- package/src/test-utils/build-mock-registry-daemon.ts +28 -0
- package/src/test-utils/build-mock-session-descriptor.ts +66 -0
- package/src/test-utils/build-mock-session.ts +43 -0
- package/src/test-utils/build-mock-sortable-session-view.ts +20 -0
- package/src/test-utils/build-mock-stored-event.ts +20 -0
- package/src/test-utils/build-mock-stored-row.ts +23 -0
- package/src/test-utils/build-stub-attention-adapter.ts +22 -0
- package/src/test-utils/build-stub-bridge-context.ts +20 -0
- package/src/test-utils/build-stub-brokered-agent-adapter.ts +96 -0
- package/src/test-utils/build-stub-brokered-gateway-adapter.ts +50 -0
- package/src/test-utils/build-stub-channel-opener.ts +45 -0
- package/src/test-utils/build-stub-claude-headless-run.ts +13 -0
- package/src/test-utils/build-stub-client-screens.ts +33 -0
- package/src/test-utils/build-stub-clock.ts +67 -0
- package/src/test-utils/build-stub-daemon-context.ts +81 -0
- package/src/test-utils/build-stub-daemon-requests.ts +103 -0
- package/src/test-utils/{fixture-dir-provider.ts → build-stub-dir-provider.ts} +20 -12
- package/src/test-utils/build-stub-execution-provider.ts +112 -0
- package/src/test-utils/build-stub-fleet-caller.ts +60 -0
- package/src/test-utils/build-stub-forking-gh.ts +10 -0
- package/src/test-utils/build-stub-gh.ts +50 -0
- package/src/test-utils/build-stub-handoff-daemon.ts +11 -0
- package/src/test-utils/build-stub-harness-relay.ts +50 -0
- package/src/test-utils/build-stub-headless-runner.ts +60 -0
- package/src/test-utils/build-stub-held-gh.ts +19 -0
- package/src/test-utils/build-stub-held-inbox-source.ts +36 -0
- package/src/test-utils/build-stub-held-provider.ts +58 -0
- package/src/test-utils/build-stub-held-zoxide.ts +13 -0
- package/src/test-utils/build-stub-host-hold.ts +31 -0
- package/src/test-utils/build-stub-host-provider.ts +26 -0
- package/src/test-utils/build-stub-log.ts +15 -0
- package/src/test-utils/build-stub-mcp-claude.ts +26 -0
- package/src/test-utils/build-stub-mcp-grok.ts +19 -0
- package/src/test-utils/build-stub-mcp-stdio-server.ts +20 -0
- package/src/test-utils/build-stub-peer-socket.ts +81 -0
- package/src/test-utils/build-stub-picker-host.ts +36 -0
- package/src/test-utils/build-stub-proxied-agent-adapter.ts +29 -0
- package/src/test-utils/build-stub-pty-provider.ts +53 -0
- package/src/test-utils/build-stub-recording-atc.ts +10 -0
- package/src/test-utils/build-stub-recording-filter.ts +9 -0
- package/src/test-utils/build-stub-recording-git.ts +8 -0
- package/src/test-utils/build-stub-signed-in-gh.ts +18 -0
- package/src/test-utils/build-stub-signed-out-gh.ts +8 -0
- package/src/test-utils/build-stub-soft-kill-provider.ts +29 -0
- package/src/test-utils/build-stub-targets.ts +58 -0
- package/src/test-utils/build-stub-terminal.ts +19 -0
- package/src/test-utils/build-stub-timeout-scheduler.ts +48 -0
- package/src/test-utils/build-stub-tui-claude.ts +47 -0
- package/src/test-utils/build-stub-tui-grok.ts +53 -0
- package/src/test-utils/build-stub-wait-clock.ts +23 -0
- package/src/test-utils/build-target-options-from-config.ts +37 -0
- package/src/test-utils/collect-process-tree.ts +75 -0
- package/src/test-utils/create-git-fixture.ts +117 -0
- package/src/test-utils/create-migrated-state-db.ts +12 -10
- package/src/test-utils/create-stub-bin.ts +19 -0
- package/src/test-utils/create-stub-claude.ts +75 -0
- package/src/test-utils/create-stub-codex.ts +31 -0
- package/src/test-utils/create-stub-composer.ts +65 -0
- package/src/test-utils/create-stub-echo-claude.ts +21 -0
- package/src/test-utils/create-stub-failing-agent-adapter.ts +111 -0
- package/src/test-utils/create-stub-grok.ts +44 -0
- package/src/test-utils/create-stub-guest-clis.ts +55 -0
- package/src/test-utils/create-stub-harness-guest.ts +35 -0
- package/src/test-utils/{fixture-imp-port.ts → create-stub-imp-port.ts} +144 -63
- package/src/test-utils/create-stub-recording-claude.ts +20 -0
- package/src/test-utils/create-stub-recording-codex.ts +20 -0
- package/src/test-utils/create-stub-shared-server-codex.ts +56 -0
- package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +15 -9
- package/src/test-utils/get-gateway-config.ts +3 -3
- package/src/test-utils/get-only-effect-ref.ts +21 -0
- package/src/test-utils/get-only-imp-name.ts +19 -0
- package/src/test-utils/get-records.ts +20 -0
- package/src/test-utils/get-string.ts +14 -0
- package/src/test-utils/keys.ts +26 -0
- package/src/test-utils/merge-deep.ts +36 -0
- package/src/test-utils/mock-overrides.ts +13 -0
- package/src/test-utils/open-repo-step.ts +24 -0
- package/src/test-utils/read-query-plan.ts +22 -0
- package/src/test-utils/register-test-cleanup.ts +63 -0
- package/src/test-utils/resolve-atc-command.ts +13 -0
- package/src/test-utils/resolve-gateway-command.ts +13 -0
- package/src/test-utils/run-atc.ts +56 -0
- package/src/test-utils/run-legacy-daemon.ts +27 -5
- package/src/test-utils/run-listener-daemon.ts +5 -10
- package/src/test-utils/run-mcp-authorization.ts +7 -4
- package/src/test-utils/run-source-daemon.ts +2 -2
- package/src/test-utils/seed-faker.ts +7 -0
- package/src/test-utils/send-lines-before-handshake.ts +35 -0
- package/src/test-utils/send-mcp-request.ts +25 -0
- package/src/test-utils/setup-mcp-home.ts +110 -0
- package/src/test-utils/setup-mcp-http.ts +26 -30
- package/src/test-utils/setup-temp-dir.ts +7 -6
- package/src/test-utils/spawn-claude-session.ts +34 -0
- package/src/test-utils/spawn-grok-session.ts +39 -0
- package/src/test-utils/start-cut-proxy.ts +29 -8
- package/src/test-utils/start-daemon-process.ts +193 -0
- package/src/test-utils/start-git-http-server.ts +28 -4
- package/src/test-utils/start-mcp-stdio.ts +190 -0
- package/src/test-utils/start-output-capture.ts +20 -0
- package/src/test-utils/start-stub-answering-listener.ts +65 -0
- package/src/test-utils/start-stub-closing-listener.ts +31 -0
- package/src/test-utils/start-stub-dropping-daemon.ts +105 -0
- package/src/test-utils/start-stub-impd-info.ts +38 -0
- package/src/test-utils/start-stub-impd.ts +143 -0
- package/src/test-utils/start-stub-legacy-daemon.ts +171 -0
- package/src/test-utils/start-stub-mcp-server.ts +55 -0
- package/src/test-utils/start-stub-recording-listener.ts +47 -0
- package/src/test-utils/start-stub-refusing-listener.ts +43 -0
- package/src/test-utils/start-stub-reporter-socket.ts +79 -0
- package/src/test-utils/start-stub-restore-daemon.ts +59 -0
- package/src/test-utils/start-stub-session-bridge.ts +100 -0
- package/src/test-utils/start-stub-silent-server.ts +38 -0
- package/src/test-utils/start-stub-source-daemon.ts +41 -0
- package/src/test-utils/start-stub-stalled-client.ts +61 -0
- package/src/test-utils/start-stub-stalled-listener.ts +44 -0
- package/src/test-utils/start-stub-tap.ts +38 -0
- package/src/test-utils/start-stub-tcp-daemon.ts +75 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +32 -0
- package/src/test-utils/start-stub-unanswering-listener.ts +41 -0
- package/src/test-utils/start-stub-unanswering-unix-listener.ts +38 -0
- package/src/test-utils/start-test-daemon.ts +229 -0
- package/src/test-utils/start-tui-harness.ts +291 -0
- package/src/test-utils/subscribe-to-socket-lines.ts +81 -14
- package/src/test-utils/try-send-request.ts +29 -0
- package/src/test-utils/wait-for-event.ts +41 -0
- package/src/test-utils/wait-for.ts +15 -3
- package/src/verify-restored-fleet.ts +30 -17
- package/src/workspace/check-repository-access.ts +4 -0
- package/src/workspace/collect-remote-refs.ts +4 -2
- package/src/workspace/run-git.ts +6 -0
- 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
|
+
}
|