@os-eco/overstory-cli 0.8.6 → 0.8.7

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.
@@ -220,14 +220,16 @@ describe("ClaudeRuntime", () => {
220
220
  expect(state).toEqual({ phase: "ready" });
221
221
  });
222
222
 
223
- test("returns ready for prompt indicator + shift+tab", () => {
223
+ test("returns loading for prompt indicator + shift+tab (no bypass permissions)", () => {
224
+ // shift+tab appears in ALL Claude Code sessions — it must NOT trigger ready
224
225
  const state = runtime.detectReady("Claude Code\n\u276f\nshift+tab to chat");
225
- expect(state).toEqual({ phase: "ready" });
226
+ expect(state).toEqual({ phase: "loading" });
226
227
  });
227
228
 
228
- test('returns ready for Try " + shift+tab', () => {
229
+ test('returns loading for Try " + shift+tab (no bypass permissions)', () => {
230
+ // False-positive scenario: shift+tab alone is not a reliable readiness signal
229
231
  const state = runtime.detectReady('Try "help"\nshift+tab');
230
- expect(state).toEqual({ phase: "ready" });
232
+ expect(state).toEqual({ phase: "loading" });
231
233
  });
232
234
 
233
235
  test("returns dialog for trust dialog", () => {
@@ -235,6 +237,20 @@ describe("ClaudeRuntime", () => {
235
237
  expect(state).toEqual({ phase: "dialog", action: "Enter" });
236
238
  });
237
239
 
240
+ test("returns dialog for bypass permissions confirmation", () => {
241
+ const state = runtime.detectReady(
242
+ "WARNING: Claude Code running in Bypass Permissions mode\n❯ 1. No, exit\n2. Yes, I accept",
243
+ );
244
+ expect(state).toEqual({ phase: "dialog", action: "type:2" });
245
+ });
246
+
247
+ test("bypass permissions confirmation takes precedence over ready indicators", () => {
248
+ const state = runtime.detectReady(
249
+ "WARNING: Claude Code running in Bypass Permissions mode\n❯ 1. No, exit\n2. Yes, I accept\nbypass permissions",
250
+ );
251
+ expect(state).toEqual({ phase: "dialog", action: "type:2" });
252
+ });
253
+
238
254
  test("trust dialog takes precedence over ready indicators", () => {
239
255
  const state = runtime.detectReady("trust this folder\n\u276f\nbypass permissions");
240
256
  expect(state).toEqual({ phase: "dialog", action: "Enter" });
@@ -577,9 +593,10 @@ describe("ClaudeRuntime integration: detectReady matches pre-refactor tmux behav
577
593
  expect(state.phase).toBe("ready");
578
594
  });
579
595
 
580
- test("ready: 'Try \"help\"' + 'shift+tab'", () => {
596
+ test("loading: 'Try \"help\"' + 'shift+tab' (no bypass permissions — false-positive fix)", () => {
597
+ // shift+tab appears in all Claude Code sessions, must not trigger ready without bypass permissions
581
598
  const state = runtime.detectReady('Try "help"\nshift+tab');
582
- expect(state.phase).toBe("ready");
599
+ expect(state.phase).toBe("loading");
583
600
  });
584
601
 
585
602
  test("not ready: only prompt (no status bar)", () => {
@@ -597,6 +614,14 @@ describe("ClaudeRuntime integration: detectReady matches pre-refactor tmux behav
597
614
  expect(state.phase).toBe("dialog");
598
615
  expect((state as { phase: "dialog"; action: string }).action).toBe("Enter");
599
616
  });
617
+
618
+ test("dialog: bypass permissions confirmation", () => {
619
+ const state = runtime.detectReady(
620
+ "WARNING: Claude Code running in Bypass Permissions mode\n❯ 1. No, exit\n2. Yes, I accept",
621
+ );
622
+ expect(state.phase).toBe("dialog");
623
+ expect((state as { phase: "dialog"; action: string }).action).toBe("type:2");
624
+ });
600
625
  });
601
626
 
602
627
  describe("ClaudeRuntime integration: buildEnv matches pre-refactor env injection", () => {
@@ -652,6 +677,6 @@ describe("ClaudeRuntime integration: registry resolves 'claude' as default", ()
652
677
  test("getRuntime rejects unknown runtimes", async () => {
653
678
  const { getRuntime } = await import("./registry.ts");
654
679
  expect(() => getRuntime("aider")).toThrow('Unknown runtime: "aider"');
655
- expect(() => getRuntime("cursor")).toThrow('Unknown runtime: "cursor"');
680
+ expect(() => getRuntime("nonexistent")).toThrow('Unknown runtime: "nonexistent"');
656
681
  });
657
682
  });
@@ -31,6 +31,9 @@ export class ClaudeRuntime implements AgentRuntime {
31
31
  /** Unique identifier for this runtime. */
32
32
  readonly id = "claude";
33
33
 
34
+ /** Stability level. Claude Code is the primary runtime. */
35
+ readonly stability = "stable" as const;
36
+
34
37
  /** Relative path to the instruction file within a worktree. */
35
38
  readonly instructionPath = ".claude/CLAUDE.md";
36
39
 
@@ -136,14 +139,25 @@ export class ClaudeRuntime implements AgentRuntime {
136
139
  *
137
140
  * Detection phases:
138
141
  * - Trust dialog: "trust this folder" detected → `{ phase: "dialog", action: "Enter" }`
139
- * - Ready: prompt indicator (❯ or 'Try "') AND status bar ("bypass permissions"
140
- * or "shift+tab") both present → `{ phase: "ready" }`
142
+ * - Ready: prompt indicator (❯ or 'Try "') AND status bar ("bypass permissions")
143
+ * both present → `{ phase: "ready" }`
141
144
  * - Otherwise → `{ phase: "loading" }`
142
145
  *
143
146
  * @param paneContent - Captured tmux pane content to analyze
144
147
  * @returns Current readiness phase
145
148
  */
146
149
  detectReady(paneContent: string): ReadyState {
150
+ // Claude Code v2.1.71+ shows a dedicated bypass confirmation screen.
151
+ // It already contains both a prompt marker and the phrase "bypass permissions",
152
+ // so it must be detected before the normal ready heuristics.
153
+ if (
154
+ paneContent.includes("WARNING: Claude Code running in Bypass Permissions mode") &&
155
+ paneContent.includes("1. No, exit") &&
156
+ paneContent.includes("2. Yes, I accept")
157
+ ) {
158
+ return { phase: "dialog", action: "type:2" };
159
+ }
160
+
147
161
  // Trust dialog takes precedence — it replaces the normal TUI temporarily.
148
162
  // The caller should send the action key to dismiss it.
149
163
  if (paneContent.includes("trust this folder")) {
@@ -155,8 +169,9 @@ export class ClaudeRuntime implements AgentRuntime {
155
169
  const hasPrompt = paneContent.includes("\u276f") || paneContent.includes('Try "');
156
170
 
157
171
  // Phase 2: status bar text confirms full TUI render.
158
- const hasStatusBar =
159
- paneContent.includes("bypass permissions") || paneContent.includes("shift+tab");
172
+ // Only match 'bypass permissions' — 'shift+tab' appears in ALL Claude Code sessions
173
+ // regardless of permission mode and would cause false-positive ready detection.
174
+ const hasStatusBar = paneContent.includes("bypass permissions");
160
175
 
161
176
  if (hasPrompt && hasStatusBar) {
162
177
  return { phase: "ready" };
@@ -151,6 +151,19 @@ describe("CodexRuntime", () => {
151
151
  expect(cmd).not.toContain("This inline content should be ignored");
152
152
  });
153
153
 
154
+ test("sharedWritableDirs are exposed with --add-dir", () => {
155
+ const opts: SpawnOpts = {
156
+ model: "gpt-5-codex",
157
+ permissionMode: "bypass",
158
+ cwd: "/tmp/worktree",
159
+ sharedWritableDirs: ["/project/.overstory", "/project/.git"],
160
+ env: {},
161
+ };
162
+ const cmd = runtime.buildSpawnCommand(opts);
163
+ expect(cmd).toContain("--add-dir '/project/.overstory'");
164
+ expect(cmd).toContain("--add-dir '/project/.git'");
165
+ });
166
+
154
167
  test("without appendSystemPrompt uses default AGENTS.md prompt", () => {
155
168
  const opts: SpawnOpts = {
156
169
  model: "gpt-5-codex",
@@ -37,6 +37,9 @@ export class CodexRuntime implements AgentRuntime {
37
37
  /** Unique identifier for this runtime. */
38
38
  readonly id = "codex";
39
39
 
40
+ /** Stability level. Codex adapter is experimental — not fully validated. */
41
+ readonly stability = "experimental" as const;
42
+
40
43
  /** Relative path to the instruction file within a worktree. */
41
44
  readonly instructionPath = "AGENTS.md";
42
45
 
@@ -46,6 +49,16 @@ export class CodexRuntime implements AgentRuntime {
46
49
  */
47
50
  private static readonly MANIFEST_ALIASES = new Set(["sonnet", "opus", "haiku"]);
48
51
 
52
+ /**
53
+ * Escape a directory path for use in a single-quoted shell argument.
54
+ *
55
+ * @param path - Absolute directory path
56
+ * @returns POSIX shell-safe path string
57
+ */
58
+ private static shellEscape(path: string): string {
59
+ return path.replace(/'/g, "'\\''");
60
+ }
61
+
49
62
  /**
50
63
  * Build the shell command string to spawn a Codex agent in a tmux pane.
51
64
  *
@@ -68,16 +81,19 @@ export class CodexRuntime implements AgentRuntime {
68
81
  if (!CodexRuntime.MANIFEST_ALIASES.has(opts.model)) {
69
82
  cmd += ` --model ${opts.model}`;
70
83
  }
84
+ for (const dir of opts.sharedWritableDirs ?? []) {
85
+ cmd += ` --add-dir '${CodexRuntime.shellEscape(dir)}'`;
86
+ }
71
87
 
72
88
  if (opts.appendSystemPromptFile) {
73
89
  // Read role definition from file at shell expansion time — avoids tmux
74
90
  // IPC message size limits. Append the "read AGENTS.md" instruction.
75
- const escaped = opts.appendSystemPromptFile.replace(/'/g, "'\\''");
91
+ const escaped = CodexRuntime.shellEscape(opts.appendSystemPromptFile);
76
92
  cmd += ` "$(cat '${escaped}')"' Read AGENTS.md for your task assignment and begin immediately.'`;
77
93
  } else if (opts.appendSystemPrompt) {
78
94
  // Inline role definition + instruction to read AGENTS.md.
79
95
  const prompt = `${opts.appendSystemPrompt}\n\nRead AGENTS.md for your task assignment and begin immediately.`;
80
- const escaped = prompt.replace(/'/g, "'\\''");
96
+ const escaped = CodexRuntime.shellEscape(prompt);
81
97
  cmd += ` '${escaped}'`;
82
98
  } else {
83
99
  cmd += ` 'Read AGENTS.md for your task assignment and begin immediately.'`;
@@ -28,6 +28,9 @@ export class CopilotRuntime implements AgentRuntime {
28
28
  /** Unique identifier for this runtime. */
29
29
  readonly id = "copilot";
30
30
 
31
+ /** Stability level. Copilot adapter is experimental — not fully validated. */
32
+ readonly stability = "experimental" as const;
33
+
31
34
  /** Relative path to the instruction file within a worktree. */
32
35
  readonly instructionPath = ".github/copilot-instructions.md";
33
36