@os-eco/overstory-cli 0.8.0 → 0.8.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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Headless subprocess management for non-tmux agent runtimes.
3
+ *
4
+ * Used by `ov sling` when runtime.headless === true to bypass tmux entirely.
5
+ * Provides spawnHeadlessAgent() for direct Bun.spawn() invocation of
6
+ * headless agent processes (e.g., Sapling running with --json).
7
+ *
8
+ * Note: isProcessAlive() and killProcessTree() for headless process lifecycle
9
+ * management already exist in src/worktree/tmux.ts — not duplicated here.
10
+ */
11
+
12
+ import { AgentError } from "../errors.ts";
13
+
14
+ /**
15
+ * Handle to a spawned headless agent subprocess.
16
+ *
17
+ * Provides the PID for session tracking, stdin for sending input to the
18
+ * agent process, and stdout for consuming NDJSON event output.
19
+ *
20
+ * stdout is null when the process was spawned with a stdoutFile redirect
21
+ * (file-redirect mode). In that case, stdout is written directly to the
22
+ * log file and no pipe backpressure can occur.
23
+ */
24
+ export interface HeadlessProcess {
25
+ /** OS-level process ID. Stored in AgentSession.pid for watchdog monitoring. */
26
+ pid: number;
27
+ /** Writable sink for sending input to the process (e.g., RPC messages). */
28
+ stdin: { write(data: string | Uint8Array): number | Promise<number> };
29
+ /**
30
+ * Readable stream of the process stdout, or null when stdout was redirected
31
+ * to a file via stdoutFile. Consumed via runtime.parseEvents() when piped.
32
+ */
33
+ stdout: ReadableStream<Uint8Array> | null;
34
+ }
35
+
36
+ /**
37
+ * Options for spawning a headless agent subprocess.
38
+ *
39
+ * When stdoutFile or stderrFile are provided, the corresponding stream is
40
+ * redirected to the given file path instead of a pipe. This eliminates
41
+ * backpressure: the child process can write unlimited output without blocking.
42
+ *
43
+ * Log files are useful for post-mortem inspection and do not need to be
44
+ * consumed by the caller.
45
+ */
46
+ export interface SpawnHeadlessOptions {
47
+ /** Working directory for the subprocess. */
48
+ cwd: string;
49
+ /** Full environment for the subprocess (no implicit merging with process.env). */
50
+ env: Record<string, string>;
51
+ /**
52
+ * When set, redirect subprocess stdout to this file path instead of a pipe.
53
+ * HeadlessProcess.stdout will be null in this case.
54
+ */
55
+ stdoutFile?: string;
56
+ /**
57
+ * When set, redirect subprocess stderr to this file path instead of a pipe.
58
+ */
59
+ stderrFile?: string;
60
+ }
61
+
62
+ /**
63
+ * Spawn a headless agent subprocess directly via Bun.spawn().
64
+ *
65
+ * Used by `ov sling` when runtime.headless === true to bypass all tmux
66
+ * session management.
67
+ *
68
+ * **Backpressure prevention:** Pass stdoutFile (and stderrFile) to redirect
69
+ * output to log files instead of pipes. This is the recommended mode for
70
+ * `ov sling` — it prevents the OS pipe buffer (~64 KB) from filling up and
71
+ * blocking the child process when the caller does not actively consume stdout.
72
+ *
73
+ * When no file paths are provided (default/legacy mode), stdout is a pipe and
74
+ * the caller is responsible for consuming it to prevent backpressure.
75
+ *
76
+ * The provided env is used as the full subprocess environment (no implicit
77
+ * merging with process.env — callers should merge explicitly if needed).
78
+ *
79
+ * @param argv - Full argv array from runtime.buildDirectSpawn(); first element is the executable
80
+ * @param opts - Working directory, environment, and optional log file paths
81
+ * @returns HeadlessProcess with pid, stdin, and stdout (null if file-redirected)
82
+ * @throws AgentError if argv is empty
83
+ */
84
+ export async function spawnHeadlessAgent(
85
+ argv: string[],
86
+ opts: SpawnHeadlessOptions,
87
+ ): Promise<HeadlessProcess> {
88
+ const [cmd, ...args] = argv;
89
+ if (!cmd) {
90
+ throw new AgentError("buildDirectSpawn returned empty argv array", {
91
+ agentName: "headless",
92
+ });
93
+ }
94
+
95
+ const stdoutTarget = opts.stdoutFile ? Bun.file(opts.stdoutFile) : "pipe";
96
+ const stderrTarget = opts.stderrFile ? Bun.file(opts.stderrFile) : "pipe";
97
+
98
+ const proc = Bun.spawn([cmd, ...args], {
99
+ cwd: opts.cwd,
100
+ env: opts.env,
101
+ stdout: stdoutTarget,
102
+ stderr: stderrTarget,
103
+ stdin: "pipe",
104
+ });
105
+
106
+ return {
107
+ pid: proc.pid,
108
+ stdin: proc.stdin,
109
+ stdout: opts.stdoutFile ? null : (proc.stdout as ReadableStream<Uint8Array>),
110
+ };
111
+ }
@@ -98,6 +98,11 @@ export async function createSession(
98
98
  exports.push(`export PATH="${overstoryBinDir}:$PATH"`);
99
99
  }
100
100
 
101
+ // Clear Claude Code nesting guard so child agents can start.
102
+ // Claude Code >=2.1.66 sets CLAUDECODE=1 and refuses to launch when it's present.
103
+ // Overstory's agent spawning is intentional, not accidental nesting.
104
+ exports.push("unset CLAUDECODE CLAUDE_CODE_SSE_PORT CLAUDE_CODE_ENTRYPOINT");
105
+
101
106
  // Add any additional environment variables
102
107
  if (env) {
103
108
  for (const [key, value] of Object.entries(env)) {