@bastani/atomic 0.5.28-0 → 0.5.28-2

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.
@@ -10,7 +10,6 @@
10
10
  "defaultMode": "bypassPermissions"
11
11
  },
12
12
  "disabledMcpjsonServers": [
13
- "github",
14
13
  "azure-devops"
15
14
  ],
16
15
  "enabledPlugins": {
@@ -8,11 +8,16 @@
8
8
  "headers": {
9
9
  "Authorization": "Bearer ${env:GITHUB_PERSONAL_ACCESS_TOKEN}"
10
10
  },
11
- "enabled": false
11
+ "enabled": true
12
12
  },
13
13
  "azure-devops": {
14
14
  "type": "local",
15
- "command": ["bunx", "-y", "@azure-devops/mcp", "<your-org>"],
15
+ "command": [
16
+ "bunx",
17
+ "-y",
18
+ "@azure-devops/mcp",
19
+ "<your-org>"
20
+ ],
16
21
  "enabled": false
17
22
  }
18
23
  }
@@ -45,13 +45,24 @@ export declare function claudeHookDirs(): {
45
45
  queue: string;
46
46
  release: string;
47
47
  hil: string;
48
+ pid: string;
48
49
  };
49
50
  /** Options for {@link claudeStopHookCommand}. Primarily used by tests to shrink the wait budget. */
50
51
  export interface ClaudeStopHookOptions {
51
52
  /** Maximum time the hook waits for a queued follow-up prompt before letting Claude stop. */
52
53
  waitTimeoutMs?: number;
53
- /** Polling interval for queue/release detection. */
54
+ /**
55
+ * Interval for the polling fallback that runs alongside the `fs.watch`
56
+ * watchers in case an inotify/FSEvent notification gets dropped. In the
57
+ * happy path, watcher events fire on create and the poll never matches.
58
+ */
54
59
  pollIntervalMs?: number;
60
+ /**
61
+ * Interval at which the hook checks whether the atomic workflow process
62
+ * that owns this session is still alive. Coarser than `pollIntervalMs`
63
+ * because atomic crashing is rare and `process.kill(pid, 0)` is a syscall.
64
+ */
65
+ livenessIntervalMs?: number;
55
66
  }
56
67
  /**
57
68
  * Handler for the hidden `_claude-stop-hook` subcommand.
@@ -1 +1 @@
1
- {"version":3,"file":"claude-stop-hook.d.ts","sourceRoot":"","sources":["../../../src/commands/cli/claude-stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAOH,yEAAyE;AACzE,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAeD;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQhG;AAED,oGAAoG;AACpG,MAAM,WAAW,qBAAqB;IACpC,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAKD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAsGjB"}
1
+ {"version":3,"file":"claude-stop-hook.d.ts","sourceRoot":"","sources":["../../../src/commands/cli/claude-stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAQH,yEAAyE;AACzE,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAeD;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAcA;AAED,oGAAoG;AACpG,MAAM,WAAW,qBAAqB;IACpC,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAsFD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAyMjB"}
@@ -288,6 +288,26 @@ export declare class HeadlessClaudeClientWrapper {
288
288
  start(): Promise<string>;
289
289
  stop(): Promise<void>;
290
290
  }
291
+ /**
292
+ * Resolve the `claude` CLI binary for headless SDK queries.
293
+ *
294
+ * Pins the SDK to the same binary interactive stages already spawn via tmux
295
+ * (`AGENT_CONFIG.claude.cmd` on PATH), bypassing
296
+ * `@anthropic-ai/claude-agent-sdk`'s built-in resolver. That resolver probes
297
+ * optional native packages in a fixed order — on Linux it tries
298
+ * `linux-${arch}-musl` before `linux-${arch}` and returns whichever
299
+ * `require.resolve` finds first — so on a glibc host where both optional
300
+ * packages got installed (Bun installs every optionalDependency by default)
301
+ * it picks the musl binary, which can't exec because its dynamic linker
302
+ * (`/lib/ld-musl-*.so.1`) is absent. The SDK surfaces the resulting ENOENT
303
+ * as a misleading "Claude Code native binary not found" error.
304
+ *
305
+ * `chatCommand` and `workflowCommand` already fail fast when `claude` isn't
306
+ * on PATH (see `isCommandInstalled` in each), so in practice this lookup
307
+ * always succeeds. The throw here is a belt-and-suspenders guard that
308
+ * prefers a clear failure over silently falling back to the SDK's resolver.
309
+ */
310
+ export declare function resolveHeadlessClaudeBin(): string;
291
311
  /**
292
312
  * Headless session wrapper for Claude stages. Uses the Agent SDK's `query()`
293
313
  * directly instead of tmux pane operations. Implements the same `query()`
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/claude.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/claude.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,23 @@
4
4
  * Checks that Copilot workflow source files use the runtime-managed
5
5
  * `s.client` and `s.session` instead of manual SDK client creation.
6
6
  */
7
+ /**
8
+ * Env inherited by the Copilot CLI subprocess the SDK spawns.
9
+ *
10
+ * `NODE_NO_WARNINGS=1` silences the
11
+ * `ExperimentalWarning: SQLite is an experimental feature` banner that
12
+ * Node prints via the CLI's bundled `require("node:sqlite")`. The SDK
13
+ * pipes the subprocess's stderr through `process.stderr` with a
14
+ * `[CLI subprocess]` prefix, so without this override the warning
15
+ * leaks into every `atomic chat -a copilot` and `atomic workflow -a
16
+ * copilot` invocation.
17
+ *
18
+ * The SDK uses `options.env ?? process.env` as-is (no merge) when
19
+ * spawning, so we must fold the existing env in ourselves. Returns a
20
+ * fresh object per call so callers can layer additional env without
21
+ * mutating shared state.
22
+ */
23
+ export declare function copilotSubprocessEnv(): Record<string, string | undefined>;
7
24
  /**
8
25
  * Validate a Copilot workflow source file for common mistakes.
9
26
  */
@@ -1 +1 @@
1
- {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/copilot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,eAAO,MAAM,uBAAuB,+DAelC,CAAC"}
1
+ {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../src/sdk/providers/copilot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAEzE;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,+DAelC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/sdk/runtime/executor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/sdk/runtime/executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,KAAK,EACV,kBAAkB,EAMlB,SAAS,EAET,YAAY,EAMb,MAAM,aAAa,CAAC;AA0ErB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5C,MAAM,WAAW,kBAAkB;IACjC,uCAAuC;IACvC,UAAU,EAAE,kBAAkB,CAAC;IAC/B,iBAAiB;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAoDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,SAAS,CAgB1D;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,IAAI,OAAO,CAKtD;AAyBD;;;;;GAKG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAMhD;AAuFD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKzC;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAMzC;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBxB;AAMD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAkFf;AAoDD,gGAAgG;AAChG,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAOvE;AA2OD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,CA0CrE;AAOD;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EAClC,OAAO,EAAE,yBAAyB,EAClC,UAAU,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACrC,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAuB5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;CAC5D;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,aAAa,CAAC,gBAAgB,CAAC,EACvC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAChC,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,EAAE,CACA,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC3C,MAAM,IAAI,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,wBAAwB,EACjC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAChC,MAAM,IAAI,CA0BZ;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,wBAAwB,EACjC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAChC,MAAM,IAAI,CAwBZ;AAsFD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,EAAE,GACf,MAAM,EAAE,CAMV;AAslBD,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAqMrD"}
@@ -21,6 +21,13 @@ export interface AgentConfig {
21
21
  source: string;
22
22
  destination: string;
23
23
  merge: boolean;
24
+ /**
25
+ * Top-level keys to strip from the source before copying or merging.
26
+ * Useful when a key is project-local by design (e.g. Claude's
27
+ * `disabledMcpjsonServers`) and must not leak into a global
28
+ * destination like `~/.claude/settings.json`.
29
+ */
30
+ excludeConfigKeys?: readonly string[];
24
31
  }>;
25
32
  }
26
33
  declare const AGENT_KEYS: readonly ["claude", "opencode", "copilot"];
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../../src/services/config/definitions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,WAAW;IAC1B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mFAAmF;IACnF,gBAAgB,EAAE,KAAK,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,OAAO,CAAC;KAChB,CAAC,CAAC;CACJ;AAED,QAAA,MAAM,UAAU,4CAA6C,CAAC;AAC9D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,QAAQ,EAAE,WAAW,CA2DtD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,QAAQ,CAEzD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,WAAW,CAEzD;AAED,wBAAgB,YAAY,IAAI,QAAQ,EAAE,CAEzC"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../../src/services/config/definitions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,WAAW;IAC1B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mFAAmF;IACnF,gBAAgB,EAAE,KAAK,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,OAAO,CAAC;QACf;;;;;WAKG;QACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KACvC,CAAC,CAAC;CACJ;AAED,QAAA,MAAM,UAAU,4CAA6C,CAAC;AAC9D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,QAAQ,EAAE,WAAW,CA8DtD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,QAAQ,CAEzD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,WAAW,CAEzD;AAED,wBAAgB,YAAY,IAAI,QAAQ,EAAE,CAEzC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.5.28-0",
3
+ "version": "0.5.28-2",
4
4
  "description": "Configuration management CLI and SDK for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -75,11 +75,11 @@
75
75
  "typescript-language-server": "^5.1.3"
76
76
  },
77
77
  "dependencies": {
78
- "@anthropic-ai/claude-agent-sdk": "^0.2.116",
78
+ "@anthropic-ai/claude-agent-sdk": "^0.2.117",
79
79
  "@clack/prompts": "^1.2.0",
80
80
  "@commander-js/extra-typings": "^14.0.0",
81
81
  "@github/copilot-sdk": "^0.2.2",
82
- "@opencode-ai/sdk": "^1.14.19",
82
+ "@opencode-ai/sdk": "^1.14.20",
83
83
  "@opentui/core": "^0.1.102",
84
84
  "@opentui/react": "^0.1.102",
85
85
  "commander": "^14.0.3",
@@ -22,6 +22,7 @@ import { getCopilotScmDisableFlags } from "../../../services/config/scm-sync.ts"
22
22
  import { ensureProjectSetup } from "../init/index.ts";
23
23
  import { COLORS } from "../../../theme/colors.ts";
24
24
  import { isCommandInstalled } from "../../../services/system/detect.ts";
25
+ import { checkAgentAuth, printAuthError } from "../../../services/system/auth.ts";
25
26
  import {
26
27
  ensureAtomicGlobalAgentConfigs,
27
28
  } from "../../../services/config/atomic-global-config.ts";
@@ -185,6 +186,16 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
185
186
  return 1;
186
187
  }
187
188
 
189
+ // ── Preflight: authentication ──
190
+ // Copilot and Claude expose SDK-level login checks; run them now so
191
+ // users get a short actionable error instead of being dropped into a
192
+ // native CLI that immediately redirects them to /login.
193
+ const auth = await checkAgentAuth(agentType);
194
+ if (!auth.loggedIn) {
195
+ printAuthError(agentType, auth);
196
+ return 1;
197
+ }
198
+
188
199
  // ── Preflight: global config sync ──
189
200
  const projectRoot = process.cwd();
190
201
  const configRoot = getConfigRoot();
@@ -9,10 +9,11 @@
9
9
  * and clean up in `afterEach` so test runs never collide with each other
10
10
  * or with real marker/queue/release files.
11
11
  *
12
- * The hook's default wait for a queued follow-up prompt is 15 minutes.
13
- * Every test here passes a short `waitTimeoutMs` so the hook exits quickly
14
- * when no queue entry is present we are testing the branching logic,
15
- * not the real-world wait budget.
12
+ * The hook's default wait for a queued follow-up prompt is effectively
13
+ * unbounded (~24 days) so the workflow can take as long as it needs between
14
+ * turns. Every test here passes a short `waitTimeoutMs` so the hook exits
15
+ * quickly when no queue entry is present — we are testing the branching
16
+ * logic, not the real-world wait budget.
16
17
  */
17
18
 
18
19
  import { describe, test, expect, afterEach, spyOn } from "bun:test";
@@ -20,7 +21,7 @@ import { access, rm, writeFile, mkdir } from "node:fs/promises";
20
21
  import { join } from "node:path";
21
22
  import { claudeStopHookCommand, claudeHookDirs } from "./claude-stop-hook.ts";
22
23
 
23
- const { marker: markerDir, queue: queueDir, release: releaseDir } = claudeHookDirs();
24
+ const { marker: markerDir, queue: queueDir, release: releaseDir, pid: pidDir } = claudeHookDirs();
24
25
 
25
26
  const SHORT_TIMEOUT_MS = 300;
26
27
 
@@ -52,6 +53,7 @@ afterEach(async () => {
52
53
  rm(join(markerDir, id), { force: true }),
53
54
  rm(join(queueDir, id), { force: true }),
54
55
  rm(join(releaseDir, id), { force: true }),
56
+ rm(join(pidDir, id), { force: true }),
55
57
  ]);
56
58
  }
57
59
  sessionIdsToClean.length = 0;
@@ -268,4 +270,48 @@ describe("claudeStopHookCommand", () => {
268
270
  // No block decision emitted.
269
271
  expect(stdoutChunks.join("")).toBe("");
270
272
  });
273
+
274
+ // 9. Dead atomic PID → hook exits without waiting out the full timeout.
275
+ //
276
+ // Simulates the case where the atomic workflow was SIGKILL'd between
277
+ // turns: the pid file on disk points at a process that no longer exists,
278
+ // so the liveness check should fire and let the hook bail. We pick a
279
+ // deliberately-bogus PID (2^22 - 1) that is almost certainly unused.
280
+ test("dead atomic pid triggers liveness exit before the wait timeout", async () => {
281
+ const sessionId = crypto.randomUUID();
282
+ sessionIdsToClean.push(sessionId);
283
+
284
+ // Find a PID that doesn't currently exist. `process.kill(pid, 0)` throws
285
+ // ESRCH for free PIDs; we scan from a high number downward to dodge
286
+ // system-reserved low PIDs.
287
+ let deadPid = 4_194_303;
288
+ while (deadPid > 1) {
289
+ try {
290
+ process.kill(deadPid, 0);
291
+ deadPid -= 1;
292
+ } catch (e: unknown) {
293
+ if (e instanceof Error && "code" in e && (e as NodeJS.ErrnoException).code === "ESRCH") break;
294
+ deadPid -= 1;
295
+ }
296
+ }
297
+
298
+ await mkdir(pidDir, { recursive: true });
299
+ await writeFile(join(pidDir, sessionId), String(deadPid), "utf-8");
300
+
301
+ mockStdin(JSON.stringify({ session_id: sessionId }));
302
+
303
+ // Use a long wait timeout so the test only passes if the liveness check
304
+ // short-circuits the wait. livenessIntervalMs is short so the test runs fast.
305
+ const started = Date.now();
306
+ const code = await claudeStopHookCommand({
307
+ waitTimeoutMs: 30_000,
308
+ pollIntervalMs: 10_000,
309
+ livenessIntervalMs: 50,
310
+ });
311
+ const elapsed = Date.now() - started;
312
+
313
+ expect(code).toBe(0);
314
+ expect(elapsed).toBeLessThan(5_000);
315
+ expect(await fileExists(join(markerDir, sessionId))).toBe(true);
316
+ });
271
317
  });
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  import fs from "node:fs/promises";
32
+ import { watch as watchDir } from "node:fs/promises";
32
33
  import { existsSync } from "node:fs";
33
34
  import path from "node:path";
34
35
  import os from "node:os";
@@ -60,13 +61,25 @@ function isClaudeStopHookPayload(value: unknown): value is ClaudeStopHookPayload
60
61
  *
61
62
  * Exported so tests and `src/sdk/providers/claude.ts` share one source of truth.
62
63
  */
63
- export function claudeHookDirs(): { marker: string; queue: string; release: string; hil: string } {
64
+ export function claudeHookDirs(): {
65
+ marker: string;
66
+ queue: string;
67
+ release: string;
68
+ hil: string;
69
+ pid: string;
70
+ } {
64
71
  const base = path.join(os.homedir(), ".atomic");
65
72
  return {
66
73
  marker: path.join(base, "claude-stop"),
67
74
  queue: path.join(base, "claude-queue"),
68
75
  release: path.join(base, "claude-release"),
69
76
  hil: path.join(base, "claude-hil"),
77
+ // Holds the PID of the atomic workflow process that owns each session.
78
+ // The Stop hook polls `process.kill(pid, 0)` against this value so that
79
+ // if atomic is SIGKILL'd (no chance to write a release marker), the hook
80
+ // can detect the orphaned session and self-exit instead of sitting in
81
+ // its wait loop for ~24 days.
82
+ pid: path.join(base, "claude-pid"),
70
83
  };
71
84
  }
72
85
 
@@ -74,12 +87,103 @@ export function claudeHookDirs(): { marker: string; queue: string; release: stri
74
87
  export interface ClaudeStopHookOptions {
75
88
  /** Maximum time the hook waits for a queued follow-up prompt before letting Claude stop. */
76
89
  waitTimeoutMs?: number;
77
- /** Polling interval for queue/release detection. */
90
+ /**
91
+ * Interval for the polling fallback that runs alongside the `fs.watch`
92
+ * watchers in case an inotify/FSEvent notification gets dropped. In the
93
+ * happy path, watcher events fire on create and the poll never matches.
94
+ */
78
95
  pollIntervalMs?: number;
96
+ /**
97
+ * Interval at which the hook checks whether the atomic workflow process
98
+ * that owns this session is still alive. Coarser than `pollIntervalMs`
99
+ * because atomic crashing is rare and `process.kill(pid, 0)` is a syscall.
100
+ */
101
+ livenessIntervalMs?: number;
79
102
  }
80
103
 
81
- const DEFAULT_WAIT_TIMEOUT_MS = 15 * 60 * 1000;
104
+ /**
105
+ * Effectively-unbounded default wait budget for the queue/release poll loop.
106
+ *
107
+ * The hook holds Claude Code in the Stop phase while the workflow runtime
108
+ * decides what to do next — either enqueueing a follow-up prompt (delivered
109
+ * back to Claude as `{decision:"block", reason:...}`) or writing a release
110
+ * marker on teardown. Any finite default here caps the time the workflow has
111
+ * between turns: when it expires, the hook exits 0, Claude stops, and the
112
+ * next `enqueuePrompt` writes to a file nobody's reading — the workflow
113
+ * hangs on `waitForIdle` for a turn that will never come.
114
+ *
115
+ * The Claude-side hook timeout (see `STOP_HOOK_TIMEOUT_SECONDS` in
116
+ * `src/sdk/providers/claude.ts`) is already set to ~24 days, so matching it
117
+ * here keeps the two bounds aligned — the hook either runs until the
118
+ * workflow releases it or until Claude Code itself gives up. Tests override
119
+ * `waitTimeoutMs` via options to keep runs fast.
120
+ *
121
+ * Expressed in ms: 2_147_483 s × 1000 = 2_147_483_000 ms, just under the
122
+ * max safe `setTimeout` value (2^31 - 1).
123
+ */
124
+ const DEFAULT_WAIT_TIMEOUT_MS = 2_147_483_000;
82
125
  const DEFAULT_POLL_INTERVAL_MS = 100;
126
+ const DEFAULT_LIVENESS_INTERVAL_MS = 5_000;
127
+
128
+ /**
129
+ * Read the atomic PID that owns this session from `~/.atomic/claude-pid/<id>`,
130
+ * or return null if the file is missing / malformed. Missing is fine: older
131
+ * runtimes didn't write one, and we just skip the liveness check in that case.
132
+ */
133
+ async function readAtomicPid(pidFilePath: string): Promise<number | null> {
134
+ let raw: string;
135
+ try {
136
+ raw = await fs.readFile(pidFilePath, "utf-8");
137
+ } catch {
138
+ return null;
139
+ }
140
+ const parsed = Number.parseInt(raw.trim(), 10);
141
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
142
+ }
143
+
144
+ /**
145
+ * Sleep that resolves early when `signal` is aborted. Used by the hook's
146
+ * wait loops so `ac.abort()` unblocks everything immediately instead of
147
+ * waiting for the next wake-up tick — otherwise a task that detects a hit
148
+ * (e.g. liveness check) can't meaningfully cancel its siblings.
149
+ */
150
+ function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
151
+ return new Promise<void>((resolve) => {
152
+ if (signal.aborted) {
153
+ resolve();
154
+ return;
155
+ }
156
+ const timer = setTimeout(() => {
157
+ signal.removeEventListener("abort", onAbort);
158
+ resolve();
159
+ }, ms);
160
+ const onAbort = (): void => {
161
+ clearTimeout(timer);
162
+ resolve();
163
+ };
164
+ signal.addEventListener("abort", onAbort, { once: true });
165
+ });
166
+ }
167
+
168
+ /**
169
+ * True when a process with `pid` exists. Uses signal `0`, which performs the
170
+ * permission/existence check without delivering a signal. ESRCH means gone,
171
+ * EPERM means alive-but-not-ours (still alive for our purposes).
172
+ */
173
+ function isProcessAlive(pid: number): boolean {
174
+ try {
175
+ process.kill(pid, 0);
176
+ return true;
177
+ } catch (e: unknown) {
178
+ if (e instanceof Error && "code" in e) {
179
+ const code = (e as NodeJS.ErrnoException).code;
180
+ if (code === "EPERM") return true;
181
+ if (code === "ESRCH") return false;
182
+ }
183
+ // Unknown error — assume alive to avoid false-positive teardown.
184
+ return true;
185
+ }
186
+ }
83
187
 
84
188
  /**
85
189
  * Handler for the hidden `_claude-stop-hook` subcommand.
@@ -95,6 +199,8 @@ export async function claudeStopHookCommand(
95
199
  ): Promise<number> {
96
200
  const waitTimeoutMs = options.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
97
201
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
202
+ const livenessIntervalMs =
203
+ options.livenessIntervalMs ?? DEFAULT_LIVENESS_INTERVAL_MS;
98
204
 
99
205
  // 1. Read stdin
100
206
  const raw = await Bun.stdin.text();
@@ -135,6 +241,7 @@ export async function claudeStopHookCommand(
135
241
  fs.mkdir(dirs.marker, { recursive: true }),
136
242
  fs.mkdir(dirs.queue, { recursive: true }),
137
243
  fs.mkdir(dirs.release, { recursive: true }),
244
+ fs.mkdir(dirs.pid, { recursive: true }),
138
245
  ]);
139
246
 
140
247
  // 4. Write the marker file directly.
@@ -148,7 +255,7 @@ export async function claudeStopHookCommand(
148
255
  const markerPath = path.join(dirs.marker, payload.session_id);
149
256
  await Bun.write(markerPath, raw);
150
257
 
151
- // 5. Block-poll for either a queued follow-up prompt or a release signal.
258
+ // 5. Wait for either a queued follow-up prompt or a release signal.
152
259
  //
153
260
  // The workflow's `waitForIdle` has already been unblocked by the marker
154
261
  // write above and is now returning control to the user's stage callback.
@@ -164,34 +271,130 @@ export async function claudeStopHookCommand(
164
271
  // `~/.atomic/claude-release/<session_id>`. We exit 0 with no stdout
165
272
  // payload and Claude stops as usual.
166
273
  //
167
- // c. Neither happens within `waitTimeoutMs`. We exit 0 on timeout as a
168
- // safety net Claude stops rather than hanging its Stop hook forever.
274
+ // c. Neither happens within `waitTimeoutMs`. We exit 0 so Claude Code
275
+ // doesn't hang past its own per-hook timeout. The production default
276
+ // for `waitTimeoutMs` is aligned with the Claude-side hook timeout
277
+ // (~24 days), so this path is effectively unreachable in real runs —
278
+ // it only fires in tests that pass a short override.
279
+ //
280
+ // Delivery uses `fs.watch` on the queue and release dirs for ~0-latency
281
+ // wake-up on create events, with a slower `existsSync` polling fallback
282
+ // in case a watcher notification gets dropped under fs load (same pattern
283
+ // as `watchHILMarker` in `src/sdk/providers/claude.ts`).
169
284
  const queuePath = path.join(dirs.queue, payload.session_id);
170
285
  const releasePath = path.join(dirs.release, payload.session_id);
171
286
 
172
- const deadline = Date.now() + waitTimeoutMs;
173
- while (Date.now() <= deadline) {
287
+ type Hit = { kind: "release" } | { kind: "queue"; prompt: string };
288
+
289
+ const check = async (): Promise<Hit | null> => {
174
290
  if (existsSync(releasePath)) {
175
291
  try { await fs.unlink(releasePath); } catch { /* ENOENT is fine */ }
176
- return 0;
292
+ return { kind: "release" };
177
293
  }
178
294
  if (existsSync(queuePath)) {
179
295
  let prompt: string;
180
296
  try {
181
297
  prompt = await fs.readFile(queuePath, "utf-8");
182
298
  } catch {
183
- return 0;
299
+ // Treat a failed read as a graceful release so the hook still exits.
300
+ return { kind: "release" };
184
301
  }
185
302
  try { await fs.unlink(queuePath); } catch { /* ENOENT is fine */ }
303
+ return { kind: "queue", prompt };
304
+ }
305
+ return null;
306
+ };
307
+
308
+ const emit = (hit: Hit): number => {
309
+ if (hit.kind === "queue") {
186
310
  process.stdout.write(JSON.stringify({
187
311
  decision: "block",
188
- reason: prompt,
312
+ reason: hit.prompt,
189
313
  }));
190
- return 0;
191
314
  }
192
- await Bun.sleep(pollIntervalMs);
315
+ return 0;
316
+ };
317
+
318
+ // Initial synchronous check — the runtime may have enqueued/released before
319
+ // we attached watchers, and without this the hook could hang until the
320
+ // polling fallback fires.
321
+ const early = await check();
322
+ if (early) return emit(early);
323
+
324
+ const ac = new AbortController();
325
+ const overallTimer = setTimeout(() => ac.abort(), waitTimeoutMs);
326
+ let hit: Hit | null = null;
327
+
328
+ // Read the atomic workflow's PID (if the runtime wrote one for this
329
+ // session). Used by the liveness task below to detect an atomic crash.
330
+ const atomicPid = await readAtomicPid(
331
+ path.join(dirs.pid, payload.session_id),
332
+ );
333
+
334
+ // Watch a single directory for change events and resolve `hit` on the
335
+ // first one that matches. `event.filename` is unreliable across OSes
336
+ // (see the comment in `watchHILMarker`), so disk state is authoritative.
337
+ const runWatcher = async (dir: string): Promise<void> => {
338
+ try {
339
+ for await (const _event of watchDir(dir, { signal: ac.signal })) {
340
+ const result = await check();
341
+ if (result) {
342
+ hit = result;
343
+ ac.abort();
344
+ return;
345
+ }
346
+ }
347
+ } catch (e: unknown) {
348
+ if (!(e instanceof Error && e.name === "AbortError")) throw e;
349
+ }
350
+ };
351
+
352
+ // Polling fallback — catches the rare dropped inotify/FSEvent event.
353
+ // Only runs while the watchers are live; `ac.abort()` shuts it down.
354
+ const runPollFallback = async (): Promise<void> => {
355
+ while (!ac.signal.aborted) {
356
+ await abortableSleep(pollIntervalMs, ac.signal);
357
+ if (ac.signal.aborted) return;
358
+ const result = await check();
359
+ if (result) {
360
+ hit = result;
361
+ ac.abort();
362
+ return;
363
+ }
364
+ }
365
+ };
366
+
367
+ // Liveness check — if the atomic workflow process died without writing a
368
+ // release marker (e.g. SIGKILL), this task abandons the wait and lets
369
+ // Claude stop. No-op when there's no pid file (older sessions or non-
370
+ // runtime spawns) so the hook still functions standalone.
371
+ const runLivenessCheck = async (): Promise<void> => {
372
+ if (atomicPid === null) return;
373
+ while (!ac.signal.aborted) {
374
+ await abortableSleep(livenessIntervalMs, ac.signal);
375
+ if (ac.signal.aborted) return;
376
+ if (!isProcessAlive(atomicPid)) {
377
+ // hit stays null → the hook exits 0 without emitting a block decision.
378
+ ac.abort();
379
+ return;
380
+ }
381
+ }
382
+ };
383
+
384
+ try {
385
+ await Promise.all([
386
+ runWatcher(dirs.queue),
387
+ runWatcher(dirs.release),
388
+ runPollFallback(),
389
+ runLivenessCheck(),
390
+ ]);
391
+ } finally {
392
+ clearTimeout(overallTimer);
393
+ ac.abort();
193
394
  }
194
395
 
396
+ if (hit) return emit(hit);
397
+
195
398
  // Timeout — no queued prompt arrived. Let Claude stop normally.
196
399
  return 0;
197
400
  }
@@ -34,7 +34,12 @@ export async function applyManagedOnboardingFiles(
34
34
  managedFile.destination,
35
35
  projectRoot,
36
36
  );
37
- await syncJsonFile(sourcePath, destinationPath, managedFile.merge);
37
+ await syncJsonFile(
38
+ sourcePath,
39
+ destinationPath,
40
+ managedFile.merge,
41
+ managedFile.excludeConfigKeys ?? [],
42
+ );
38
43
  }
39
44
  }
40
45