@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.
- package/.claude/settings.json +0 -1
- package/.opencode/opencode.json +7 -2
- package/dist/commands/cli/claude-stop-hook.d.ts +12 -1
- package/dist/commands/cli/claude-stop-hook.d.ts.map +1 -1
- package/dist/sdk/providers/claude.d.ts +20 -0
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/providers/copilot.d.ts +17 -0
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/services/config/definitions.d.ts +7 -0
- package/dist/services/config/definitions.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/commands/cli/chat/index.ts +11 -0
- package/src/commands/cli/claude-stop-hook.test.ts +51 -5
- package/src/commands/cli/claude-stop-hook.ts +216 -13
- package/src/commands/cli/init/onboarding.ts +6 -1
- package/src/commands/cli/workflow-command.test.ts +40 -0
- package/src/commands/cli/workflow.ts +10 -0
- package/src/lib/merge.ts +29 -4
- package/src/sdk/providers/claude.ts +80 -0
- package/src/sdk/providers/copilot.ts +20 -0
- package/src/sdk/providers/headless-hil-policy.test.ts +41 -1
- package/src/sdk/runtime/executor.ts +10 -1
- package/src/services/config/definitions.ts +10 -0
- package/src/services/system/auth.test.ts +194 -0
- package/src/services/system/auth.ts +137 -0
package/.claude/settings.json
CHANGED
package/.opencode/opencode.json
CHANGED
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
"headers": {
|
|
9
9
|
"Authorization": "Bearer ${env:GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
10
10
|
},
|
|
11
|
-
"enabled":
|
|
11
|
+
"enabled": true
|
|
12
12
|
},
|
|
13
13
|
"azure-devops": {
|
|
14
14
|
"type": "local",
|
|
15
|
-
"command": [
|
|
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
|
-
/**
|
|
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;
|
|
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":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,OAAO,IAAI,UAAU,EAC3B,MAAM,gCAAgC,CAAC;AAgCxC;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
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":"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"}
|
|
@@ -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;
|
|
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-
|
|
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.
|
|
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.
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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(): {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
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
|
|
168
|
-
//
|
|
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
|
-
|
|
173
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
37
|
+
await syncJsonFile(
|
|
38
|
+
sourcePath,
|
|
39
|
+
destinationPath,
|
|
40
|
+
managedFile.merge,
|
|
41
|
+
managedFile.excludeConfigKeys ?? [],
|
|
42
|
+
);
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
45
|
|