@zgeoff/atc 3.4.1 → 3.5.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 (86) hide show
  1. package/package.json +1 -1
  2. package/src/agents/agent-adapter.ts +4 -0
  3. package/src/agents/build-claude-query-options.ts +1 -0
  4. package/src/agents/build-headless-env.ts +4 -0
  5. package/src/agents/claude-adapter.ts +4 -3
  6. package/src/agents/gateway-adapter.ts +2 -2
  7. package/src/agents/plan-claude-line-input.ts +44 -0
  8. package/src/client/daemon-client.ts +6 -1
  9. package/src/collect-restart-env.ts +6 -3
  10. package/src/daemon/build-scoped-context.ts +2 -0
  11. package/src/daemon/check-session-scope.ts +235 -0
  12. package/src/daemon/daemon-connection.ts +74 -0
  13. package/src/daemon/daemon-context.ts +21 -0
  14. package/src/daemon/daemon.ts +21 -0
  15. package/src/daemon/find-host-branch.ts +28 -0
  16. package/src/daemon/materialize-workspace.ts +6 -1
  17. package/src/daemon/merge-recorded-scope.ts +50 -0
  18. package/src/daemon/published-records.ts +269 -0
  19. package/src/daemon/request-access-classes.ts +1 -0
  20. package/src/daemon/run-host-git.ts +52 -0
  21. package/src/daemon/sessions.ts +185 -53
  22. package/src/daemon/start-headless-turn.ts +3 -0
  23. package/src/federation/id-rules.ts +11 -2
  24. package/src/mcp/mcp-tools.ts +51 -1
  25. package/src/mcp/require-daemon-features.ts +1 -0
  26. package/src/mcp/run-tool.ts +14 -0
  27. package/src/protocol/daemon-features.ts +5 -0
  28. package/src/protocol/parse-declared-scope.ts +158 -0
  29. package/src/protocol/protocol.ts +1 -0
  30. package/src/protocol/published-record.ts +62 -0
  31. package/src/protocol/request-param-schemas.ts +16 -0
  32. package/src/store/parse-published-record.ts +59 -0
  33. package/src/store/run-migrations.ts +23 -0
  34. package/src/store/state-store.ts +32 -0
  35. package/src/test-utils/build-mock-published-record.ts +39 -0
  36. package/src/test-utils/build-stub-brokered-agent-adapter.ts +3 -1
  37. package/src/test-utils/build-stub-daemon-context.ts +2 -0
  38. package/src/test-utils/build-stub-daemon-requests.ts +3 -3
  39. package/src/test-utils/build-stub-harness-relay.ts +2 -2
  40. package/src/test-utils/build-stub-held-gh.ts +5 -7
  41. package/src/test-utils/build-stub-held-inbox-source.ts +2 -2
  42. package/src/test-utils/build-stub-held-zoxide.ts +4 -6
  43. package/src/test-utils/build-stub-host-hold.ts +7 -6
  44. package/src/test-utils/build-stub-launch-admission.ts +20 -0
  45. package/src/test-utils/build-stub-pregranted-broker-host.ts +22 -0
  46. package/src/test-utils/build-stub-terminal.ts +3 -3
  47. package/src/test-utils/create-git-fixture.ts +2 -2
  48. package/src/test-utils/create-stub-claude.ts +3 -1
  49. package/src/test-utils/create-stub-imp-port.ts +46 -37
  50. package/src/test-utils/create-stub-systemd.ts +6 -6
  51. package/src/test-utils/isolate-home.ts +8 -5
  52. package/src/test-utils/read-query-plan.ts +10 -6
  53. package/src/test-utils/register-test-cleanup.ts +10 -10
  54. package/src/test-utils/{run-legacy-daemon.ts → run-stub-legacy-daemon.ts} +1 -1
  55. package/src/test-utils/setup-mcp-home.ts +7 -6
  56. package/src/test-utils/setup-mcp-http.ts +6 -6
  57. package/src/test-utils/setup-temp-dir.ts +4 -10
  58. package/src/test-utils/start-cut-proxy.ts +2 -4
  59. package/src/test-utils/start-daemon-process.ts +13 -13
  60. package/src/test-utils/start-git-http-server.ts +2 -4
  61. package/src/test-utils/start-mcp-stdio.ts +5 -5
  62. package/src/test-utils/start-stub-answering-listener.ts +3 -3
  63. package/src/test-utils/start-stub-closing-listener.ts +3 -3
  64. package/src/test-utils/start-stub-dropping-daemon.ts +2 -3
  65. package/src/test-utils/start-stub-impd-info.ts +3 -7
  66. package/src/test-utils/start-stub-impd.ts +3 -5
  67. package/src/test-utils/start-stub-legacy-daemon.ts +2 -3
  68. package/src/test-utils/start-stub-mcp-server.ts +3 -5
  69. package/src/test-utils/start-stub-recording-listener.ts +3 -3
  70. package/src/test-utils/start-stub-refusing-listener.ts +3 -3
  71. package/src/test-utils/start-stub-reporter-socket.ts +3 -9
  72. package/src/test-utils/start-stub-restore-daemon.ts +2 -7
  73. package/src/test-utils/start-stub-session-bridge.ts +3 -9
  74. package/src/test-utils/start-stub-silent-server.ts +3 -6
  75. package/src/test-utils/start-stub-source-daemon.ts +3 -3
  76. package/src/test-utils/start-stub-stalled-client.ts +3 -3
  77. package/src/test-utils/start-stub-stalled-listener.ts +3 -3
  78. package/src/test-utils/start-stub-tcp-daemon.ts +3 -3
  79. package/src/test-utils/start-stub-telemetry-collector.ts +2 -2
  80. package/src/test-utils/start-stub-unanswering-listener.ts +3 -3
  81. package/src/test-utils/start-stub-unanswering-unix-listener.ts +3 -3
  82. package/src/test-utils/start-test-daemon.ts +62 -18
  83. package/src/test-utils/start-tui-harness.ts +17 -10
  84. package/src/test-utils/subscribe-to-socket-lines.ts +5 -9
  85. package/src/test-utils/{can-bind-addresses.ts → try-bind-addresses.ts} +4 -3
  86. /package/src/test-utils/{run-source-daemon.ts → run-stub-source-daemon.ts} +0 -0
@@ -42,31 +42,67 @@ interface TestDaemonConfig {
42
42
  // The temp directory's name prefix.
43
43
  readonly prefix?: string;
44
44
  readonly options?: TestDaemonOptionsBuilder;
45
+
46
+ // Whether each boot opens the main client; it does unless this is false.
47
+ readonly mainClient?: boolean;
48
+ }
49
+
50
+ /**
51
+ * A running test daemon and the calls that drive it.
52
+ */
53
+ interface TestDaemon extends TestDaemonPaths {
54
+ readonly build: string;
55
+ readonly logs: string[];
56
+ readonly events: EventMsg[];
57
+ readonly daemon: DaemonHandle;
58
+ readonly client: DaemonClient;
59
+ readonly openClient: (hello?: Readonly<Record<string, unknown>>) => Promise<DaemonClient>;
60
+ readonly openTCPClient: () => Promise<DaemonClient>;
61
+ readonly sendHookLines: (...lines: readonly Readonly<Record<string, unknown>>[]) => Promise<void>;
62
+ readonly stop: () => Promise<void>;
63
+ readonly restart: (options?: TestDaemonOptionsBuilder) => Promise<void>;
64
+ readonly dispose: () => Promise<void>;
65
+ }
66
+
67
+ /**
68
+ * The harness a config starts: one that opens no main client has neither
69
+ * that client nor the events it would collect.
70
+ */
71
+ type StartedTestDaemon<Config extends TestDaemonConfig> = Config extends {
72
+ readonly mainClient: false;
45
73
  }
74
+ ? Omit<TestDaemon, 'client' | 'events'>
75
+ : TestDaemon;
46
76
 
47
77
  // The build string the daemon and every client the harness opens send in
48
78
  // their handshake.
49
79
  const BUILD = 'atc/test-build';
50
80
 
51
81
  /**
52
- * A real daemon in a fresh temp directory, with a client that has already
53
- * sent its handshake. The options builder chooses what the daemon wires;
54
- * the harness sets the paths and the build, and collects the daemon's log
55
- * lines unless the options set their own log. `events` collects every event
56
- * the main client receives, across restarts. `openClient` opens another
57
- * client over the unix socket and sends its handshake with the given
58
- * params; `openTCPClient` connects to the TCP listener and sends nothing, so
59
- * the test drives that handshake itself. `sendHookLines` writes reporter
60
- * lines to the reporter socket and resolves once the daemon has closed the
61
- * connection. `stop` closes every client and stops the daemon; `restart`
62
- * does the same, then boots on the same paths and state with the options
63
- * given or the last ones, and opens a new main client. Disposal stops what
64
- * is running and removes the directory. That disposal runs once the current
65
- * test finishes, so it must run inside a test; disposing sooner runs it
66
- * then, and a second disposal does nothing. A first boot that fails runs it
82
+ * A real daemon in a fresh temp directory, with a main client that has
83
+ * already sent its handshake unless the config turns that client off, which
84
+ * leaves every connection the daemon counts to the test. The options
85
+ * builder chooses what the daemon wires; the harness sets the paths and the
86
+ * build, and collects the daemon's log lines unless the options set their
87
+ * own log. `events` collects every event the main client receives, across
88
+ * restarts. `openClient` opens another client over the unix socket and
89
+ * sends its handshake with the given params; `openTCPClient` connects to
90
+ * the TCP listener and sends nothing, so the test drives that handshake
91
+ * itself. `sendHookLines` writes reporter lines to the reporter socket and
92
+ * resolves once the daemon has closed the connection. `stop` closes every
93
+ * client and stops the daemon; `restart` does the same, then boots on the
94
+ * same paths and state with the options given or the last ones, and opens
95
+ * a new main client unless the config turns it off. `dispose` stops what is
96
+ * running and removes the directory. It runs once the current test
97
+ * finishes, so the harness must start inside a test; calling it sooner runs
98
+ * it then, and a second call does nothing. A first boot that fails runs it
67
99
  * before the start rejects.
68
100
  */
69
- export async function startTestDaemon(config: TestDaemonConfig = {}) {
101
+ export function startTestDaemon<const Config extends TestDaemonConfig = TestDaemonConfig>(
102
+ config?: Config,
103
+ ): Promise<StartedTestDaemon<Config>>;
104
+
105
+ export async function startTestDaemon(config: TestDaemonConfig = {}): Promise<TestDaemon> {
70
106
  const tmp = setupTempDir(config.prefix ?? 'atc-test-daemon-');
71
107
 
72
108
  // Registered after the directory, so it releases first: the daemon stops
@@ -75,7 +111,7 @@ export async function startTestDaemon(config: TestDaemonConfig = {}) {
75
111
 
76
112
  const dispose = registerTestCleanup(() => stack.disposeAsync());
77
113
 
78
- stack.use(tmp);
114
+ stack.defer(tmp.teardown);
79
115
 
80
116
  const paths: TestDaemonPaths = {
81
117
  dir: tmp.dir,
@@ -142,6 +178,10 @@ export async function startTestDaemon(config: TestDaemonConfig = {}) {
142
178
 
143
179
  live = daemon;
144
180
 
181
+ if (config.mainClient === false) {
182
+ return { daemon, client: null };
183
+ }
184
+
145
185
  const client = await openClient();
146
186
 
147
187
  client.onEvent = (event) => {
@@ -168,6 +208,10 @@ export async function startTestDaemon(config: TestDaemonConfig = {}) {
168
208
  return current.daemon;
169
209
  },
170
210
  get client(): DaemonClient {
211
+ if (current.client === null) {
212
+ throw new Error('the test daemon started without a main client');
213
+ }
214
+
171
215
  return current.client;
172
216
  },
173
217
  openClient,
@@ -224,6 +268,6 @@ export async function startTestDaemon(config: TestDaemonConfig = {}) {
224
268
 
225
269
  current = await boot();
226
270
  },
227
- [Symbol.asyncDispose]: dispose,
271
+ dispose,
228
272
  };
229
273
  }
@@ -32,16 +32,17 @@ interface TUIHarnessOptions {
32
32
  * starts a new client on the same home and stops capturing the old one.
33
33
  * `read` returns every byte the client drew since the last `reset`, and
34
34
  * `waitFor` polls that capture for a needle. The client appends what it
35
- * decides without drawing to a log: `markClientLog` returns a cursor into
36
- * it, and `waitForClientLog` polls for a line written after that cursor.
35
+ * decides without drawing to a log: `countClientLogLines` returns how many
36
+ * lines it holds, and `waitForClientLog` polls for a line written after
37
+ * that many.
37
38
  * `writeConfig` writes the home's config: the stand-in binaries and the git
38
39
  * transports the fixture repositories need, with the fields given laid over
39
40
  * them. `env` is the environment the client runs with, so a daemon started
40
41
  * with it serves the client. A wait made before the client draws anything
41
- * gets `bootMs` for that first byte. Disposal stops the client and the
42
- * daemon in the home and removes it. That disposal runs once the current
43
- * test finishes, so it must run inside a test; disposing sooner runs it
44
- * then, and a second disposal does nothing.
42
+ * gets `bootMs` for that first byte. `stop` kills every client a boot
43
+ * started, stops the daemon in the home, and removes the home. That stop runs once the current test
44
+ * finishes, so it must run inside a test; calling `stop` sooner runs it
45
+ * then, and a second stop does nothing.
45
46
  */
46
47
  export function startTUIHarness(options: TUIHarnessOptions = {}) {
47
48
  const tmp = setupTempDir('atc-tui-');
@@ -50,9 +51,9 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
50
51
  // daemon stop before their home is removed.
51
52
  const owned = new AsyncDisposableStack();
52
53
 
53
- const dispose = registerTestCleanup(() => owned.disposeAsync());
54
+ const stop = registerTestCleanup(() => owned.disposeAsync());
54
55
 
55
- owned.use(tmp);
56
+ owned.defer(tmp.teardown);
56
57
 
57
58
  // The client boots with this home as its cwd and lists it first in the
58
59
  // picker, so the path is resolved the way the client reports it.
@@ -129,6 +130,12 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
129
130
  env,
130
131
  });
131
132
 
133
+ // Every client a boot starts is killed before the daemon stops, so a
134
+ // client a later boot replaced never outlives the test.
135
+ owned.defer(() => {
136
+ booted.kill();
137
+ });
138
+
132
139
  const exit = Promise.withResolvers<number>();
133
140
 
134
141
  booted.onExit((event) => {
@@ -206,7 +213,7 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
206
213
  return exited;
207
214
  },
208
215
 
209
- markClientLog(): number {
216
+ countClientLogLines(): number {
210
217
  return readClientLog(clientLogPath).length;
211
218
  },
212
219
 
@@ -225,7 +232,7 @@ export function startTUIHarness(options: TUIHarnessOptions = {}) {
225
232
  );
226
233
  },
227
234
 
228
- [Symbol.asyncDispose]: dispose,
235
+ stop,
229
236
  };
230
237
  }
231
238
 
@@ -11,7 +11,7 @@ interface SocketLines {
11
11
 
12
12
  readonly write: (data: string) => void;
13
13
  readonly waitForLine: (count?: number, timeoutMs?: number) => Promise<string[]>;
14
- readonly [Symbol.asyncDispose]: () => Promise<void>;
14
+ readonly unsubscribe: () => void;
15
15
  }
16
16
 
17
17
  interface SocketLinesOptions {
@@ -35,8 +35,8 @@ interface SocketLinesOptions {
35
35
  * the lines, throwing when `timeoutMs` passes first or the connection
36
36
  * closes short of the count; a stub clock passed as `now` and `wait` steps
37
37
  * that timeout without waiting it out. The connection ends once the current
38
- * test finishes, so it must run inside a test; disposal ends it sooner, and
39
- * a second end does nothing.
38
+ * test finishes, so it must run inside a test; `unsubscribe` ends it
39
+ * sooner, and a second end does nothing.
40
40
  */
41
41
  export async function subscribeToSocketLines(
42
42
  path: string,
@@ -72,7 +72,7 @@ export async function subscribeToSocketLines(
72
72
  },
73
73
  });
74
74
 
75
- const stop = registerTestCleanup(() => {
75
+ const unsubscribe = registerTestCleanup(() => {
76
76
  socket.end();
77
77
  });
78
78
 
@@ -118,10 +118,6 @@ export async function subscribeToSocketLines(
118
118
 
119
119
  return requireLines(count);
120
120
  },
121
- [Symbol.asyncDispose]: () => {
122
- stop();
123
-
124
- return Promise.resolve();
125
- },
121
+ unsubscribe,
126
122
  };
127
123
  }
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Whether this host can bind a socket to every given address, tried by
3
- * listening on a kernel-chosen port of each and closing it at once. A host
2
+ * Tries to bind a socket to every given address on this host, by listening
3
+ * on a kernel-chosen port of each and closing it at once, and returns whether
4
+ * every bind succeeded. A host
4
5
  * without an address, such as stock macOS for any loopback address past
5
6
  * 127.0.0.1, refuses the bind with EADDRNOTAVAIL.
6
7
  */
7
- export function canBindAddresses(hostnames: readonly string[]): boolean {
8
+ export function tryBindAddresses(hostnames: readonly string[]): boolean {
8
9
  for (const hostname of hostnames) {
9
10
  try {
10
11
  const listener = Bun.listen({ hostname, port: 0, socket: { data() {} } });