@zgeoff/atc 3.3.1 → 3.4.0

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 (179) hide show
  1. package/package.json +4 -2
  2. package/src/agents/agent-adapter.ts +10 -0
  3. package/src/agents/build-cli-argv.ts +2 -4
  4. package/src/agents/claude-adapter.ts +13 -3
  5. package/src/agents/codex-adapter.ts +11 -2
  6. package/src/agents/gateway-adapter.ts +20 -8
  7. package/src/agents/grok-adapter.ts +16 -13
  8. package/src/agents/load-claude-config-bundle.ts +6 -6
  9. package/src/agents/print-codex-hook-file.ts +7 -3
  10. package/src/agents/print-grok-hook-file.ts +8 -3
  11. package/src/agents/resolve-agent-home.ts +7 -3
  12. package/src/agents/resolve-claude-global-config-path.ts +4 -4
  13. package/src/agents/update-claude-project-trust.ts +41 -30
  14. package/src/agents/with-claude-config-lock.ts +13 -1
  15. package/src/agents/write-atc-bridge.ts +10 -7
  16. package/src/agents/write-hook-settings.ts +13 -7
  17. package/src/client/boot-daemon.ts +48 -15
  18. package/src/client/dirs.ts +4 -2
  19. package/src/client/index.ts +17 -1
  20. package/src/client/log-client-event.ts +20 -0
  21. package/src/client/spawn-picker.ts +200 -121
  22. package/src/client/ui.ts +7 -7
  23. package/src/clients.ts +41 -11
  24. package/src/daemon/daemon.ts +143 -33
  25. package/src/daemon/event-signal.ts +19 -3
  26. package/src/daemon/make-hook-runner.ts +82 -22
  27. package/src/daemon/permission-registry.ts +11 -6
  28. package/src/daemon/restore-fleet.ts +62 -17
  29. package/src/daemon/run-eject-handoff.ts +20 -2
  30. package/src/daemon/sessions.ts +6 -0
  31. package/src/daemon/start-tcp-listener.ts +12 -10
  32. package/src/federation/daemon-caller.ts +25 -4
  33. package/src/federation/daemon-pool.ts +3 -1
  34. package/src/federation/open-gateway-caller.ts +6 -1
  35. package/src/grants.ts +36 -11
  36. package/src/hook-report.ts +17 -3
  37. package/src/mcp/read-report-texts.ts +11 -7
  38. package/src/mcp/start-mcp-http-server.ts +5 -1
  39. package/src/mcp-http-server.ts +85 -32
  40. package/src/report.ts +21 -3
  41. package/src/run-bridge-tap.ts +36 -7
  42. package/src/run-config-migrate.ts +35 -11
  43. package/src/run-gateway.ts +4 -2
  44. package/src/shared/build-atc-command.ts +7 -3
  45. package/src/shared/bun-sqlite-driver.ts +10 -0
  46. package/src/shared/claim-daemon-lock.ts +10 -4
  47. package/src/shared/collect-dir-roots.ts +6 -6
  48. package/src/shared/collect-hooks.ts +7 -6
  49. package/src/shared/config.ts +19 -9
  50. package/src/shared/system-clock.ts +24 -0
  51. package/src/sources/github/collect-github-repos.ts +1 -1
  52. package/src/sources/github/read-git-protocol.ts +1 -1
  53. package/src/sources/github/run-gh.ts +15 -9
  54. package/src/store/state-store.ts +57 -7
  55. package/src/tap.ts +44 -13
  56. package/src/test-utils/build-mock-agent-adapter.ts +25 -0
  57. package/src/test-utils/build-mock-agent-entry.ts +32 -0
  58. package/src/test-utils/build-mock-auth-binding.ts +28 -0
  59. package/src/test-utils/build-mock-auth-profile.ts +62 -0
  60. package/src/test-utils/build-mock-daemon-record.ts +26 -0
  61. package/src/test-utils/build-mock-fleet-entry.ts +33 -0
  62. package/src/test-utils/build-mock-gateway-config.ts +32 -0
  63. package/src/test-utils/build-mock-imp-identity.ts +18 -0
  64. package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
  65. package/src/test-utils/build-mock-message-record.ts +22 -0
  66. package/src/test-utils/build-mock-mirror-session.ts +42 -0
  67. package/src/test-utils/build-mock-registry-daemon.ts +28 -0
  68. package/src/test-utils/build-mock-session-descriptor.ts +66 -0
  69. package/src/test-utils/build-mock-session.ts +43 -0
  70. package/src/test-utils/build-mock-sortable-session-view.ts +20 -0
  71. package/src/test-utils/build-stub-attention-adapter.ts +22 -0
  72. package/src/test-utils/build-stub-bridge-context.ts +20 -0
  73. package/src/test-utils/build-stub-brokered-agent-adapter.ts +96 -0
  74. package/src/test-utils/build-stub-channel-opener.ts +45 -0
  75. package/src/test-utils/build-stub-claude-headless-run.ts +13 -0
  76. package/src/test-utils/build-stub-client-screens.ts +33 -0
  77. package/src/test-utils/build-stub-clock.ts +67 -0
  78. package/src/test-utils/build-stub-daemon-context.ts +81 -0
  79. package/src/test-utils/build-stub-daemon-requests.ts +103 -0
  80. package/src/test-utils/{fixture-dir-provider.ts → build-stub-dir-provider.ts} +20 -12
  81. package/src/test-utils/build-stub-execution-provider.ts +112 -0
  82. package/src/test-utils/build-stub-fleet-caller.ts +60 -0
  83. package/src/test-utils/build-stub-forking-gh.ts +10 -0
  84. package/src/test-utils/build-stub-gh.ts +50 -0
  85. package/src/test-utils/build-stub-handoff-daemon.ts +11 -0
  86. package/src/test-utils/build-stub-harness-relay.ts +50 -0
  87. package/src/test-utils/build-stub-headless-runner.ts +60 -0
  88. package/src/test-utils/build-stub-held-gh.ts +19 -0
  89. package/src/test-utils/build-stub-held-inbox-source.ts +36 -0
  90. package/src/test-utils/build-stub-held-provider.ts +58 -0
  91. package/src/test-utils/build-stub-held-zoxide.ts +13 -0
  92. package/src/test-utils/build-stub-host-hold.ts +31 -0
  93. package/src/test-utils/build-stub-host-provider.ts +26 -0
  94. package/src/test-utils/{fixture-imp-port.ts → build-stub-imp-port.ts} +119 -63
  95. package/src/test-utils/build-stub-log.ts +15 -0
  96. package/src/test-utils/build-stub-mcp-claude.ts +26 -0
  97. package/src/test-utils/build-stub-mcp-grok.ts +19 -0
  98. package/src/test-utils/build-stub-peer-socket.ts +81 -0
  99. package/src/test-utils/build-stub-picker-host.ts +36 -0
  100. package/src/test-utils/build-stub-proxied-agent-adapter.ts +29 -0
  101. package/src/test-utils/build-stub-pty-provider.ts +53 -0
  102. package/src/test-utils/build-stub-recording-atc.ts +10 -0
  103. package/src/test-utils/build-stub-signed-in-gh.ts +18 -0
  104. package/src/test-utils/build-stub-signed-out-gh.ts +8 -0
  105. package/src/test-utils/build-stub-soft-kill-provider.ts +29 -0
  106. package/src/test-utils/build-stub-targets.ts +58 -0
  107. package/src/test-utils/build-stub-terminal.ts +19 -0
  108. package/src/test-utils/build-stub-timeout-scheduler.ts +48 -0
  109. package/src/test-utils/build-stub-tui-claude.ts +47 -0
  110. package/src/test-utils/build-stub-tui-grok.ts +53 -0
  111. package/src/test-utils/build-stub-wait-clock.ts +23 -0
  112. package/src/test-utils/build-target-options-from-config.ts +37 -0
  113. package/src/test-utils/collect-process-tree.ts +75 -0
  114. package/src/test-utils/create-git-fixture.ts +120 -0
  115. package/src/test-utils/create-migrated-state-db.ts +12 -10
  116. package/src/test-utils/create-stub-bin.ts +19 -0
  117. package/src/test-utils/create-stub-claude.ts +75 -0
  118. package/src/test-utils/create-stub-codex.ts +31 -0
  119. package/src/test-utils/create-stub-composer.ts +65 -0
  120. package/src/test-utils/create-stub-failing-agent-adapter.ts +111 -0
  121. package/src/test-utils/create-stub-grok.ts +44 -0
  122. package/src/test-utils/create-stub-guest-clis.ts +55 -0
  123. package/src/test-utils/create-stub-harness-guest.ts +35 -0
  124. package/src/test-utils/create-stub-recording-claude.ts +20 -0
  125. package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +5 -5
  126. package/src/test-utils/get-gateway-config.ts +3 -3
  127. package/src/test-utils/get-only-effect-ref.ts +21 -0
  128. package/src/test-utils/get-only-imp-name.ts +19 -0
  129. package/src/test-utils/get-records.ts +20 -0
  130. package/src/test-utils/get-string.ts +14 -0
  131. package/src/test-utils/keys.ts +26 -0
  132. package/src/test-utils/merge-deep.ts +36 -0
  133. package/src/test-utils/mock-overrides.ts +13 -0
  134. package/src/test-utils/open-repo-step.ts +24 -0
  135. package/src/test-utils/read-query-plan.ts +22 -0
  136. package/src/test-utils/resolve-atc-command.ts +13 -0
  137. package/src/test-utils/resolve-gateway-command.ts +13 -0
  138. package/src/test-utils/run-atc.ts +56 -0
  139. package/src/test-utils/run-legacy-daemon.ts +27 -5
  140. package/src/test-utils/run-listener-daemon.ts +5 -10
  141. package/src/test-utils/run-source-daemon.ts +2 -2
  142. package/src/test-utils/seed-faker.ts +7 -0
  143. package/src/test-utils/send-lines-before-handshake.ts +35 -0
  144. package/src/test-utils/send-mcp-request.ts +25 -0
  145. package/src/test-utils/setup-mcp-home.ts +99 -0
  146. package/src/test-utils/setup-mcp-http.ts +19 -31
  147. package/src/test-utils/spawn-claude-session.ts +34 -0
  148. package/src/test-utils/spawn-grok-session.ts +39 -0
  149. package/src/test-utils/start-cut-proxy.ts +25 -8
  150. package/src/test-utils/start-daemon-process.ts +192 -0
  151. package/src/test-utils/start-git-http-server.ts +21 -3
  152. package/src/test-utils/start-mcp-stdio.ts +195 -0
  153. package/src/test-utils/start-output-capture.ts +20 -0
  154. package/src/test-utils/start-stub-dropping-daemon.ts +103 -0
  155. package/src/test-utils/start-stub-impd-info.ts +32 -0
  156. package/src/test-utils/start-stub-impd.ts +138 -0
  157. package/src/test-utils/start-stub-legacy-daemon.ts +166 -0
  158. package/src/test-utils/start-stub-mcp-server.ts +50 -0
  159. package/src/test-utils/start-stub-refusing-listener.ts +38 -0
  160. package/src/test-utils/start-stub-reporter-socket.ts +77 -0
  161. package/src/test-utils/start-stub-restore-daemon.ts +58 -0
  162. package/src/test-utils/start-stub-session-bridge.ts +98 -0
  163. package/src/test-utils/start-stub-silent-server.ts +33 -0
  164. package/src/test-utils/start-stub-source-daemon.ts +39 -0
  165. package/src/test-utils/start-stub-stalled-listener.ts +40 -0
  166. package/src/test-utils/start-stub-tap.ts +38 -0
  167. package/src/test-utils/start-stub-tcp-daemon.ts +70 -0
  168. package/src/test-utils/start-stub-telemetry-collector.ts +26 -0
  169. package/src/test-utils/start-test-daemon.ts +215 -0
  170. package/src/test-utils/start-tui-harness.ts +284 -0
  171. package/src/test-utils/subscribe-to-socket-lines.ts +59 -12
  172. package/src/test-utils/try-send-request.ts +29 -0
  173. package/src/test-utils/wait-for-event.ts +41 -0
  174. package/src/test-utils/wait-for.ts +15 -3
  175. package/src/verify-restored-fleet.ts +30 -17
  176. package/src/workspace/check-repository-access.ts +4 -0
  177. package/src/workspace/collect-remote-refs.ts +4 -2
  178. package/src/workspace/run-git.ts +6 -0
  179. package/src/test-utils/start-legacy-daemon.ts +0 -111
@@ -11,6 +11,8 @@ import { getBuild } from '../shared/get-build';
11
11
  import { isProcessAlive } from '../shared/is-process-alive';
12
12
  import { makeSingleFlight } from '../shared/make-single-flight';
13
13
  import { spawnATCDetached } from '../shared/spawn-atc-detached';
14
+ import { systemClock } from '../shared/system-clock';
15
+ import type { Clock } from '../shared/system-clock';
14
16
  import { toAgentID } from '../shared/to-agent-id';
15
17
  import { DaemonClient } from './daemon-client';
16
18
  import { formatProtocolMismatch } from './format-protocol-mismatch';
@@ -42,6 +44,15 @@ export interface DaemonBootOptions {
42
44
  // manager starts beside a managed daemon sets it, so the two never race
43
45
  // for the state directory.
44
46
  readonly waitForDaemonMs?: number;
47
+
48
+ // Called once per boot, when a waiting boot first finds no daemon and
49
+ // starts to wait instead of starting one; a retry after a protocol
50
+ // mismatch that waits again does not call it a second time.
51
+ readonly onWaitForDaemon?: () => void;
52
+
53
+ // The time and the timers a waiting boot reads for its deadline and its
54
+ // polls; the wall clock when absent.
55
+ readonly clock?: Clock;
45
56
  }
46
57
 
47
58
  /**
@@ -59,10 +70,23 @@ export interface DaemonBootOptions {
59
70
  * its own boot would otherwise flag daemons that are already current.
60
71
  */
61
72
  export async function bootDaemonClient(options: DaemonBootOptions = {}): Promise<DaemonBoot> {
73
+ let waited = false;
74
+ const clock = options.clock ?? systemClock;
75
+
62
76
  const wait =
63
77
  options.waitForDaemonMs === undefined
64
78
  ? null
65
- : { deadline: Date.now() + options.waitForDaemonMs, timeoutMs: options.waitForDaemonMs };
79
+ : {
80
+ deadline: clock.now() + options.waitForDaemonMs,
81
+ timeoutMs: options.waitForDaemonMs,
82
+ clock,
83
+ onWait: () => {
84
+ if (!waited) {
85
+ waited = true;
86
+ options.onWaitForDaemon?.();
87
+ }
88
+ },
89
+ };
66
90
 
67
91
  for (let attempt = 0; attempt < 2; attempt++) {
68
92
  const build = getBuild();
@@ -142,10 +166,15 @@ async function openOrBootDaemon(): Promise<OpenedDaemon> {
142
166
  interface DaemonWait {
143
167
  readonly deadline: number;
144
168
  readonly timeoutMs: number;
169
+ readonly clock: Clock;
170
+
171
+ // Called on every miss; the boot reports only the first to its caller.
172
+ readonly onWait: () => void;
145
173
  }
146
174
 
147
175
  /**
148
176
  * Polls the known sockets until a daemon answers, and never starts one.
177
+ * Each miss reports that the wait goes on.
149
178
  */
150
179
  async function waitForDaemon(wait: DaemonWait): Promise<OpenedDaemon> {
151
180
  for (;;) {
@@ -155,11 +184,17 @@ async function waitForDaemon(wait: DaemonWait): Promise<OpenedDaemon> {
155
184
  return opened;
156
185
  }
157
186
 
158
- if (Date.now() >= wait.deadline) {
187
+ wait.onWait();
188
+
189
+ if (wait.clock.now() >= wait.deadline) {
159
190
  throw new Error(formatWaitFailure(wait.timeoutMs));
160
191
  }
161
192
 
162
- await Bun.sleep(100);
193
+ const polled = Promise.withResolvers<void>();
194
+
195
+ wait.clock.schedule(polled.resolve, 100);
196
+
197
+ await polled.promise;
163
198
  }
164
199
  }
165
200
 
@@ -172,21 +207,19 @@ async function waitForHello(
172
207
  sendHello: () => Promise<Readonly<Record<string, unknown>>>,
173
208
  wait: DaemonWait,
174
209
  ): Promise<Readonly<Record<string, unknown>>> {
175
- let timer: ReturnType<typeof setTimeout> | undefined;
176
-
177
- const expired = new Promise<never>((_resolve, reject) => {
178
- timer = setTimeout(
179
- () => {
180
- reject(new Error(formatWaitFailure(wait.timeoutMs)));
181
- },
182
- Math.max(0, wait.deadline - Date.now()),
183
- );
184
- });
210
+ const expired = Promise.withResolvers<never>();
211
+
212
+ const cancel = wait.clock.schedule(
213
+ () => {
214
+ expired.reject(new Error(formatWaitFailure(wait.timeoutMs)));
215
+ },
216
+ Math.max(0, wait.deadline - wait.clock.now()),
217
+ );
185
218
 
186
219
  try {
187
- return await Promise.race([sendHello(), expired]);
220
+ return await Promise.race([sendHello(), expired.promise]);
188
221
  } finally {
189
- clearTimeout(timer);
222
+ cancel();
190
223
  }
191
224
  }
192
225
 
@@ -8,13 +8,15 @@ import { collectRootDirs } from '../shared/collect-root-dirs';
8
8
  * to find a directory: where the client runs, then the daemon's spawn
9
9
  * history (most recent first), then the configured roots, then zoxide's
10
10
  * frecency list. A path that no longer exists is dropped, a duplicate keeps
11
- * its first position, and an empty merge falls back to the home directory.
11
+ * its first position, and an empty merge falls back to the home directory:
12
+ * `home` when given, otherwise the account's.
12
13
  */
13
14
  export interface DirSources {
14
15
  readonly cwd: string;
15
16
  readonly recent: readonly string[];
16
17
  readonly roots: readonly string[];
17
18
  readonly zoxide: readonly string[];
19
+ readonly home?: string;
18
20
  }
19
21
 
20
22
  export function collectDirs(sources: DirSources): string[] {
@@ -35,7 +37,7 @@ export function collectDirs(sources: DirSources): string[] {
35
37
  }
36
38
 
37
39
  if (found.length === 0) {
38
- found.push(homedir());
40
+ found.push(sources.home ?? homedir());
39
41
  }
40
42
 
41
43
  return found;
@@ -7,7 +7,7 @@ import type { EventMsg } from '../protocol/protocol';
7
7
  import { sortGroupedSessionViews } from '../protocol/sort-grouped-session-views';
8
8
  import { sortSessionViews } from '../protocol/sort-session-views';
9
9
  import type { AgentID } from '../shared/agent-id';
10
- import { loadConfig } from '../shared/config';
10
+ import { configFile, loadConfig } from '../shared/config';
11
11
  import { makeSingleFlight } from '../shared/make-single-flight';
12
12
  import { bootDaemonClient } from './boot-daemon';
13
13
  import { buildClientMachine } from './build-client-machine';
@@ -16,6 +16,7 @@ import { collectTargetPicks } from './collect-target-picks';
16
16
  import { findFuzzyScore, formatDir } from './dirs';
17
17
  import type { ProtocolMismatch } from './format-protocol-mismatch';
18
18
  import { KEY, isDown, isUp, planTextEdit } from './keys';
19
+ import { logClientEvent } from './log-client-event';
19
20
  import { parseDaemonEvent } from './parse-daemon-event';
20
21
  import { pickTabTarget } from './pick-tab-target';
21
22
  import { resolveAgentMetadata } from './resolve-agent-metadata';
@@ -341,6 +342,15 @@ const picker = new SpawnPicker<MirrorSession>({
341
342
  toMirrorSession,
342
343
  upsertMirror,
343
344
  hasDaemonFeature,
345
+ write: (chunk) => {
346
+ process.stdout.write(chunk);
347
+ },
348
+ cwd: process.cwd(),
349
+ configPath: configFile,
350
+ onDropAnswer: (kind) => {
351
+ logClientEvent(`dropped ${kind} answer`);
352
+ },
353
+ log: logClientEvent,
344
354
  });
345
355
 
346
356
  function openPicker(resume: boolean) {
@@ -721,6 +731,12 @@ function applyOverlayKey(buf: Buffer) {
721
731
  return;
722
732
  }
723
733
 
734
+ if (ch === 'H') {
735
+ logClientEvent('ignored H on a session that cannot eject');
736
+
737
+ return;
738
+ }
739
+
724
740
  if (ch === 'P' && sel !== undefined && sel.resumable && (sel.kind === 'headless' || !sel.alive)) {
725
741
  void adoptSession(sel.id);
726
742
 
@@ -0,0 +1,20 @@
1
+ import { appendFileSync } from 'node:fs';
2
+
3
+ /**
4
+ * Appends one line, a decision the client took without drawing anything, to
5
+ * the file `ATC_CLIENT_LOG` holds, and does nothing when the variable is
6
+ * unset or empty. A write that fails is dropped, so the log never stops the
7
+ * client. It writes off the screen because the client's stdout and stderr
8
+ * are the terminal it draws on.
9
+ */
10
+ export function logClientEvent(line: string): void {
11
+ const path = process.env['ATC_CLIENT_LOG'];
12
+
13
+ if (path === undefined || path === '') {
14
+ return;
15
+ }
16
+
17
+ try {
18
+ appendFileSync(path, `${line}\n`);
19
+ } catch {}
20
+ }