@bastani/atomic 0.5.4 → 0.5.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 (173) hide show
  1. package/README.md +44 -1
  2. package/dist/lib/path-root-guard.d.ts +4 -0
  3. package/dist/lib/path-root-guard.d.ts.map +1 -0
  4. package/dist/sdk/components/color-utils.d.ts +1 -0
  5. package/dist/sdk/components/color-utils.d.ts.map +1 -0
  6. package/dist/sdk/components/connectors.d.ts +3 -2
  7. package/dist/sdk/components/connectors.d.ts.map +1 -0
  8. package/dist/sdk/components/connectors.test.d.ts +1 -0
  9. package/dist/sdk/components/connectors.test.d.ts.map +1 -0
  10. package/dist/sdk/components/edge.d.ts +2 -1
  11. package/dist/sdk/components/edge.d.ts.map +1 -0
  12. package/dist/sdk/components/error-boundary.d.ts +1 -0
  13. package/dist/sdk/components/error-boundary.d.ts.map +1 -0
  14. package/dist/sdk/components/graph-theme.d.ts +2 -1
  15. package/dist/sdk/components/graph-theme.d.ts.map +1 -0
  16. package/dist/sdk/components/header.d.ts +1 -0
  17. package/dist/sdk/components/header.d.ts.map +1 -0
  18. package/dist/sdk/components/hooks.d.ts +15 -0
  19. package/dist/sdk/components/hooks.d.ts.map +1 -0
  20. package/dist/sdk/components/layout.d.ts +2 -1
  21. package/dist/sdk/components/layout.d.ts.map +1 -0
  22. package/dist/sdk/components/layout.test.d.ts +1 -0
  23. package/dist/sdk/components/layout.test.d.ts.map +1 -0
  24. package/dist/sdk/components/node-card.d.ts +5 -3
  25. package/dist/sdk/components/node-card.d.ts.map +1 -0
  26. package/dist/sdk/components/orchestrator-panel-contexts.d.ts +3 -2
  27. package/dist/sdk/components/orchestrator-panel-contexts.d.ts.map +1 -0
  28. package/dist/sdk/components/orchestrator-panel-store.d.ts +2 -1
  29. package/dist/sdk/components/orchestrator-panel-store.d.ts.map +1 -0
  30. package/dist/sdk/components/orchestrator-panel-store.test.d.ts +1 -0
  31. package/dist/sdk/components/orchestrator-panel-store.test.d.ts.map +1 -0
  32. package/dist/sdk/components/orchestrator-panel-types.d.ts +1 -0
  33. package/dist/sdk/components/orchestrator-panel-types.d.ts.map +1 -0
  34. package/dist/sdk/components/orchestrator-panel.d.ts +2 -1
  35. package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -0
  36. package/dist/sdk/components/session-graph-panel.d.ts +1 -0
  37. package/dist/sdk/components/session-graph-panel.d.ts.map +1 -0
  38. package/dist/sdk/components/status-helpers.d.ts +2 -1
  39. package/dist/sdk/components/status-helpers.d.ts.map +1 -0
  40. package/dist/sdk/components/statusline.d.ts +2 -1
  41. package/dist/sdk/components/statusline.d.ts.map +1 -0
  42. package/dist/sdk/components/workflow-picker-panel.d.ts +11 -8
  43. package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -0
  44. package/dist/sdk/define-workflow.d.ts +2 -1
  45. package/dist/sdk/define-workflow.d.ts.map +1 -0
  46. package/dist/sdk/define-workflow.test.d.ts +1 -0
  47. package/dist/sdk/define-workflow.test.d.ts.map +1 -0
  48. package/dist/sdk/errors.d.ts +3 -0
  49. package/dist/sdk/errors.d.ts.map +1 -0
  50. package/dist/sdk/errors.test.d.ts +2 -0
  51. package/dist/sdk/errors.test.d.ts.map +1 -0
  52. package/dist/sdk/index.d.ts +7 -6
  53. package/dist/sdk/index.d.ts.map +1 -0
  54. package/dist/sdk/providers/claude.d.ts +17 -6
  55. package/dist/sdk/providers/claude.d.ts.map +1 -0
  56. package/dist/sdk/providers/copilot.d.ts +2 -5
  57. package/dist/sdk/providers/copilot.d.ts.map +1 -0
  58. package/dist/sdk/providers/opencode.d.ts +2 -5
  59. package/dist/sdk/providers/opencode.d.ts.map +1 -0
  60. package/dist/sdk/runtime/discovery.d.ts +2 -1
  61. package/dist/sdk/runtime/discovery.d.ts.map +1 -0
  62. package/dist/sdk/runtime/executor-entry.d.ts +1 -0
  63. package/dist/sdk/runtime/executor-entry.d.ts.map +1 -0
  64. package/dist/sdk/runtime/executor.d.ts +3 -6
  65. package/dist/sdk/runtime/executor.d.ts.map +1 -0
  66. package/dist/sdk/runtime/executor.test.d.ts +1 -0
  67. package/dist/sdk/runtime/executor.test.d.ts.map +1 -0
  68. package/dist/sdk/runtime/graph-inference.d.ts +1 -0
  69. package/dist/sdk/runtime/graph-inference.d.ts.map +1 -0
  70. package/dist/sdk/runtime/loader.d.ts +5 -7
  71. package/dist/sdk/runtime/loader.d.ts.map +1 -0
  72. package/dist/sdk/runtime/panel.d.ts +3 -2
  73. package/dist/sdk/runtime/panel.d.ts.map +1 -0
  74. package/dist/sdk/runtime/theme.d.ts +1 -0
  75. package/dist/sdk/runtime/theme.d.ts.map +1 -0
  76. package/dist/sdk/runtime/tmux.d.ts +26 -8
  77. package/dist/sdk/runtime/tmux.d.ts.map +1 -0
  78. package/dist/sdk/types.d.ts +23 -1
  79. package/dist/sdk/types.d.ts.map +1 -0
  80. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +1 -0
  81. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -0
  82. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +1 -0
  83. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -0
  84. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +1 -0
  85. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts.map +1 -0
  86. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +2 -1
  87. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -0
  88. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +1 -0
  89. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +1 -0
  90. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +1 -0
  91. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -0
  92. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts +1 -0
  93. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -0
  94. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts +1 -0
  95. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -0
  96. package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts +1 -0
  97. package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts.map +1 -0
  98. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +1 -0
  99. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -0
  100. package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts +2 -1
  101. package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts.map +1 -0
  102. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts +1 -0
  103. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -0
  104. package/dist/sdk/workflows/index.d.ts +14 -14
  105. package/dist/sdk/workflows/index.d.ts.map +1 -0
  106. package/dist/services/config/definitions.d.ts +85 -0
  107. package/dist/services/config/definitions.d.ts.map +1 -0
  108. package/dist/services/system/copy.d.ts +77 -0
  109. package/dist/services/system/copy.d.ts.map +1 -0
  110. package/dist/services/system/detect.d.ts +75 -0
  111. package/dist/services/system/detect.d.ts.map +1 -0
  112. package/package.json +13 -34
  113. package/src/cli.ts +11 -10
  114. package/src/commands/cli/chat/index.ts +11 -11
  115. package/src/commands/cli/chat.ts +1 -1
  116. package/src/commands/cli/config.ts +10 -9
  117. package/src/commands/cli/init/index.ts +11 -11
  118. package/src/commands/cli/init/onboarding.ts +4 -4
  119. package/src/commands/cli/init/scm.ts +5 -5
  120. package/src/commands/cli/init.ts +1 -1
  121. package/src/commands/cli/workflow-command.test.ts +19 -11
  122. package/src/commands/cli/workflow.test.ts +2 -2
  123. package/src/commands/cli/workflow.ts +6 -6
  124. package/src/lib/merge.ts +17 -31
  125. package/src/lib/path-root-guard.ts +2 -2
  126. package/src/lib/spawn.ts +13 -7
  127. package/src/scripts/bump-version.ts +1 -1
  128. package/src/scripts/constants.ts +2 -2
  129. package/src/sdk/components/header.tsx +21 -23
  130. package/src/sdk/components/hooks.ts +21 -0
  131. package/src/sdk/components/node-card.tsx +3 -2
  132. package/src/sdk/components/session-graph-panel.tsx +14 -18
  133. package/src/sdk/components/workflow-picker-panel.tsx +201 -216
  134. package/src/sdk/errors.test.ts +56 -0
  135. package/src/sdk/errors.ts +5 -0
  136. package/src/sdk/providers/claude.ts +279 -70
  137. package/src/sdk/providers/copilot.ts +17 -27
  138. package/src/sdk/providers/opencode.ts +17 -27
  139. package/src/sdk/runtime/discovery.ts +18 -18
  140. package/src/sdk/runtime/executor.test.ts +15 -48
  141. package/src/sdk/runtime/executor.ts +152 -121
  142. package/src/sdk/runtime/loader.ts +16 -21
  143. package/src/sdk/runtime/tmux.ts +95 -32
  144. package/src/sdk/types.ts +45 -0
  145. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +27 -0
  146. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +25 -16
  147. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +25 -24
  148. package/src/sdk/workflows/builtin/ralph/claude/index.ts +5 -0
  149. package/src/sdk/workflows/index.ts +3 -3
  150. package/src/services/config/atomic-config.ts +7 -8
  151. package/src/services/config/atomic-global-config.ts +9 -9
  152. package/src/services/config/config-path.ts +1 -1
  153. package/src/services/config/definitions.ts +3 -4
  154. package/src/services/config/index.ts +1 -1
  155. package/src/services/config/settings.ts +30 -36
  156. package/src/services/system/agents.ts +3 -3
  157. package/src/services/system/auto-sync.ts +9 -9
  158. package/src/services/system/copy.ts +9 -9
  159. package/src/services/system/file-lock.ts +2 -2
  160. package/src/services/system/install-ui.ts +2 -2
  161. package/src/services/system/skills.ts +1 -1
  162. package/src/theme/colors.ts +1 -1
  163. package/src/theme/logo.ts +1 -1
  164. package/tsconfig.json +3 -4
  165. package/dist/chunk-1gb5qxz9.js +0 -1
  166. package/dist/chunk-fdk7tact.js +0 -417
  167. package/dist/chunk-xkxndz5g.js +0 -1041
  168. package/dist/sdk/index.js +0 -52
  169. package/dist/sdk/workflows/builtin/ralph/claude/index.js +0 -96
  170. package/dist/sdk/workflows/builtin/ralph/copilot/index.js +0 -119
  171. package/dist/sdk/workflows/builtin/ralph/opencode/index.js +0 -148
  172. package/dist/sdk/workflows/index.js +0 -100
  173. package/src/commands/cli/chat/client.ts +0 -18
@@ -0,0 +1,56 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import {
3
+ MissingDependencyError,
4
+ WorkflowNotCompiledError,
5
+ InvalidWorkflowError,
6
+ errorMessage,
7
+ } from "./errors";
8
+
9
+ describe("MissingDependencyError", () => {
10
+ test("sets name, dependency, and message", () => {
11
+ const err = new MissingDependencyError("tmux");
12
+ expect(err).toBeInstanceOf(Error);
13
+ expect(err.name).toBe("MissingDependencyError");
14
+ expect(err.dependency).toBe("tmux");
15
+ expect(err.message).toBe("Required dependency not found: tmux");
16
+ });
17
+
18
+ test.each(["tmux", "psmux", "bun"] as const)("accepts %s", (dep) => {
19
+ const err = new MissingDependencyError(dep);
20
+ expect(err.message).toContain(dep);
21
+ });
22
+ });
23
+
24
+ describe("WorkflowNotCompiledError", () => {
25
+ test("sets name, path, and message", () => {
26
+ const err = new WorkflowNotCompiledError("/tmp/wf.ts");
27
+ expect(err).toBeInstanceOf(Error);
28
+ expect(err.name).toBe("WorkflowNotCompiledError");
29
+ expect(err.path).toBe("/tmp/wf.ts");
30
+ expect(err.message).toContain("Workflow at /tmp/wf.ts was defined but not compiled");
31
+ expect(err.message).toContain(".compile()");
32
+ });
33
+ });
34
+
35
+ describe("InvalidWorkflowError", () => {
36
+ test("sets name, path, and message", () => {
37
+ const err = new InvalidWorkflowError("/tmp/bad.ts");
38
+ expect(err).toBeInstanceOf(Error);
39
+ expect(err.name).toBe("InvalidWorkflowError");
40
+ expect(err.path).toBe("/tmp/bad.ts");
41
+ expect(err.message).toContain("/tmp/bad.ts does not export a valid WorkflowDefinition");
42
+ });
43
+ });
44
+
45
+ describe("errorMessage", () => {
46
+ test("extracts message from Error", () => {
47
+ expect(errorMessage(new Error("boom"))).toBe("boom");
48
+ });
49
+
50
+ test("stringifies non-Error values", () => {
51
+ expect(errorMessage("oops")).toBe("oops");
52
+ expect(errorMessage(42)).toBe("42");
53
+ expect(errorMessage(null)).toBe("null");
54
+ expect(errorMessage(undefined)).toBe("undefined");
55
+ });
56
+ });
package/src/sdk/errors.ts CHANGED
@@ -37,3 +37,8 @@ export class InvalidWorkflowError extends Error {
37
37
  this.name = "InvalidWorkflowError";
38
38
  }
39
39
  }
40
+
41
+ /** Extract a human-readable message from an unknown thrown value. */
42
+ export function errorMessage(error: unknown): string {
43
+ return error instanceof Error ? error.message : String(error);
44
+ }
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import {
21
- sendLiteralText,
21
+ sendViaPasteBuffer,
22
22
  sendSpecialKey,
23
23
  sendKeysAndSubmit,
24
24
  capturePaneVisible,
@@ -35,10 +35,18 @@ import {
35
35
  // Session tracking — ensures createClaudeSession is called before claudeQuery
36
36
  // ---------------------------------------------------------------------------
37
37
 
38
- const initializedPanes = new Set<string>();
38
+ /** Per-pane state for Claude sessions, used by transcript-based idle detection. */
39
+ interface PaneState {
40
+ /** Claude Code's own session ID (from the Agent SDK). Resolved lazily. */
41
+ claudeSessionId: string | undefined;
42
+ /** Session IDs that existed before this pane's Claude instance started. */
43
+ knownSessionIds: Set<string>;
44
+ }
45
+
46
+ const initializedPanes = new Map<string, PaneState>();
39
47
 
40
48
  /**
41
- * Remove a pane from the initialized set, freeing memory.
49
+ * Remove a pane from the initialized map, freeing memory.
42
50
  * Call when a Claude session is killed or no longer needed.
43
51
  */
44
52
  export function clearClaudeSession(paneId: string): void {
@@ -95,8 +103,19 @@ export async function createClaudeSession(options: ClaudeSessionOptions): Promis
95
103
  readyTimeoutMs = 30_000,
96
104
  } = options;
97
105
 
106
+ // Snapshot existing Claude sessions BEFORE starting, so we can identify the
107
+ // new session later for transcript-based idle detection.
108
+ let knownSessionIds = new Set<string>();
109
+ try {
110
+ const { listSessions } = await import("@anthropic-ai/claude-agent-sdk");
111
+ const existing = await listSessions({ dir: process.cwd() });
112
+ knownSessionIds = new Set(existing.map((s) => s.sessionId));
113
+ } catch {
114
+ // SDK unavailable — transcript-based detection will gracefully degrade
115
+ }
116
+
98
117
  const cmd = ["claude", ...chatFlags].join(" ");
99
- sendKeysAndSubmit(paneId, cmd);
118
+ await sendKeysAndSubmit(paneId, cmd);
100
119
 
101
120
  // Give the shell time to exec before polling for TUI readiness
102
121
  await Bun.sleep(1_000);
@@ -112,7 +131,166 @@ export async function createClaudeSession(options: ClaudeSessionOptions): Promis
112
131
  );
113
132
  }
114
133
 
115
- initializedPanes.add(paneId);
134
+ // Try to resolve the Claude session ID eagerly. It may not exist yet if
135
+ // Claude hasn't written its session file; we'll retry lazily in claudeQuery.
136
+ let claudeSessionId: string | undefined;
137
+ try {
138
+ const { listSessions } = await import("@anthropic-ai/claude-agent-sdk");
139
+ const current = await listSessions({ dir: process.cwd() });
140
+ const newSession = current.find((s) => !knownSessionIds.has(s.sessionId));
141
+ claudeSessionId = newSession?.sessionId;
142
+ } catch {}
143
+
144
+ initializedPanes.set(paneId, { claudeSessionId, knownSessionIds });
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Transcript-based idle detection
149
+ // ---------------------------------------------------------------------------
150
+
151
+ /**
152
+ * Check whether a SessionMessage represents a session_state_changed event
153
+ * with state 'idle'. The `message` payload is `unknown` in the SDK type, so
154
+ * we do runtime narrowing to handle both possible JSONL serialization shapes
155
+ * (extra fields only, or full raw SDKMessage).
156
+ */
157
+ function isIdleStateInTranscript(msg: { type: string; message: unknown }): boolean {
158
+ if (msg.type !== "system") return false;
159
+ const m = msg.message;
160
+ if (!m || typeof m !== "object") return false;
161
+ const obj = m as Record<string, unknown>;
162
+ return obj.subtype === "session_state_changed" && obj.state === "idle";
163
+ }
164
+
165
+ /**
166
+ * Wait for the Claude session to become idle by polling its transcript.
167
+ *
168
+ * Reads session messages (with `includeSystemMessages: true`) and looks for
169
+ * an `SDKSessionStateChangedMessage` with `state: 'idle'` that appears after
170
+ * `transcriptBeforeCount` messages — i.e., a NEW idle event that fired after
171
+ * our prompt was submitted.
172
+ *
173
+ * This is the **authoritative** turn-over signal from Claude Code's runtime,
174
+ * far more reliable than pane-capture heuristics which can false-positive on
175
+ * transient prompt indicators between sub-agent dispatches.
176
+ *
177
+ * Returns `null` if the SDK is unavailable, signalling the caller to fall
178
+ * back to pane-capture polling.
179
+ */
180
+ async function waitForIdleViaTranscript(
181
+ paneId: string,
182
+ claudeSessionId: string,
183
+ transcriptBeforeCount: number,
184
+ deadline: number,
185
+ pollIntervalMs: number,
186
+ delivered: boolean,
187
+ ): Promise<ClaudeQueryResult | null> {
188
+ const sdk = await import("@anthropic-ai/claude-agent-sdk").catch(() => null);
189
+ if (!sdk) return null;
190
+
191
+ const dir = process.cwd();
192
+
193
+ // Give Claude time to start processing before first poll
194
+ await Bun.sleep(3_000);
195
+
196
+ while (Date.now() < deadline) {
197
+ try {
198
+ const msgs = await sdk.getSessionMessages(claudeSessionId, {
199
+ dir,
200
+ includeSystemMessages: true,
201
+ });
202
+
203
+ // No new messages yet — prompt may not have been received
204
+ if (msgs.length <= transcriptBeforeCount) {
205
+ await Bun.sleep(pollIntervalMs);
206
+ continue;
207
+ }
208
+
209
+ // New messages exist. Scan backwards from the tail for an idle event
210
+ // that appeared after our prompt was sent.
211
+ for (let i = msgs.length - 1; i >= transcriptBeforeCount; i--) {
212
+ const msg = msgs[i];
213
+ if (msg && isIdleStateInTranscript(msg)) {
214
+ const output = normalizeTmuxLines(capturePaneScrollback(paneId));
215
+ return { output, delivered: true };
216
+ }
217
+ }
218
+ } catch {
219
+ // SDK read error — signal caller to fall back to pane capture
220
+ return null;
221
+ }
222
+
223
+ await Bun.sleep(pollIntervalMs);
224
+ }
225
+
226
+ // Timeout — return whatever the pane currently shows
227
+ const output = capturePaneScrollback(paneId);
228
+ return { output: normalizeTmuxLines(output || ""), delivered };
229
+ }
230
+
231
+ /**
232
+ * Wait for the Claude session to become idle by polling pane capture.
233
+ *
234
+ * Legacy fallback used when transcript-based detection is unavailable
235
+ * (SDK error, session ID unknown). Uses the same hysteresis logic as before:
236
+ * require `idleConfirmCount` consecutive idle detections to avoid
237
+ * false-idle returns between sub-agent dispatches.
238
+ */
239
+ async function waitForIdleViaCapture(
240
+ paneId: string,
241
+ beforeContent: string,
242
+ deadline: number,
243
+ pollIntervalMs: number,
244
+ idleConfirmCount: number,
245
+ delivered: boolean,
246
+ ): Promise<ClaudeQueryResult> {
247
+ let lastContent = "";
248
+ let stableCount = 0;
249
+ let consecutiveIdleCount = 0;
250
+ const idleThreshold = Math.max(1, idleConfirmCount);
251
+
252
+ // Give Claude time to start processing
253
+ await Bun.sleep(3_000);
254
+
255
+ while (Date.now() < deadline) {
256
+ const currentContent = normalizeTmuxLines(capturePaneScrollback(paneId));
257
+
258
+ // Must have new content compared to before we sent
259
+ if (currentContent === beforeContent) {
260
+ consecutiveIdleCount = 0;
261
+ await Bun.sleep(pollIntervalMs);
262
+ continue;
263
+ }
264
+
265
+ // Use visible capture for state detection to avoid stale scrollback matches
266
+ const visible = capturePaneVisible(paneId);
267
+ if (paneLooksReady(visible) && !paneHasActiveTask(visible)) {
268
+ consecutiveIdleCount++;
269
+ if (consecutiveIdleCount >= idleThreshold) {
270
+ return { output: currentContent, delivered };
271
+ }
272
+ // Not yet confirmed idle — wait and recheck
273
+ await Bun.sleep(pollIntervalMs);
274
+ continue;
275
+ } else {
276
+ consecutiveIdleCount = 0;
277
+ }
278
+
279
+ if (currentContent === lastContent) {
280
+ stableCount++;
281
+ if (stableCount >= 3) {
282
+ return { output: currentContent, delivered };
283
+ }
284
+ } else {
285
+ stableCount = 0;
286
+ }
287
+
288
+ lastContent = currentContent;
289
+ await Bun.sleep(pollIntervalMs);
290
+ }
291
+
292
+ // Timeout — return whatever we have
293
+ return { output: lastContent || capturePaneScrollback(paneId), delivered };
116
294
  }
117
295
 
118
296
  // ---------------------------------------------------------------------------
@@ -134,6 +312,13 @@ export interface ClaudeQueryOptions {
134
312
  maxSubmitRounds?: number;
135
313
  /** Timeout in ms waiting for pane to be ready before sending (default: 30s) */
136
314
  readyTimeoutMs?: number;
315
+ /**
316
+ * Number of consecutive idle detections required before considering the
317
+ * response complete (default: 2). Prevents false-idle returns between
318
+ * sub-agent dispatches where the pane briefly shows the prompt indicator
319
+ * without an active task.
320
+ */
321
+ idleConfirmCount?: number;
137
322
  }
138
323
 
139
324
  export interface ClaudeQueryResult {
@@ -175,9 +360,11 @@ export async function claudeQuery(options: ClaudeQueryOptions): Promise<ClaudeQu
175
360
  submitPresses = 1,
176
361
  maxSubmitRounds = 6,
177
362
  readyTimeoutMs = 30_000,
363
+ idleConfirmCount = 2,
178
364
  } = options;
179
365
 
180
- if (!initializedPanes.has(paneId)) {
366
+ const paneState = initializedPanes.get(paneId);
367
+ if (!paneState) {
181
368
  throw new Error(
182
369
  "claudeQuery() called without a prior createClaudeSession() for this pane. " +
183
370
  "Call createClaudeSession({ paneId }) first to start the Claude CLI.",
@@ -199,8 +386,42 @@ export async function claudeQuery(options: ClaudeQueryOptions): Promise<ClaudeQu
199
386
 
200
387
  const beforeContent = normalizeTmuxLines(capturePaneScrollback(paneId));
201
388
 
202
- // Step 2: Send literal text
203
- sendLiteralText(paneId, prompt);
389
+ // ── Transcript snapshot (before sending) ──
390
+ // Lazily resolve the Claude session ID if not yet known, then snapshot the
391
+ // current transcript length. This lets us detect NEW idle events that fire
392
+ // after our prompt is submitted.
393
+ let claudeSessionId = paneState.claudeSessionId;
394
+ let transcriptBeforeCount = -1;
395
+
396
+ if (!claudeSessionId) {
397
+ try {
398
+ const { listSessions } = await import("@anthropic-ai/claude-agent-sdk");
399
+ const sessions = await listSessions({ dir: process.cwd() });
400
+ const newSession = sessions.find(
401
+ (s) => !paneState.knownSessionIds.has(s.sessionId),
402
+ );
403
+ if (newSession) {
404
+ claudeSessionId = newSession.sessionId;
405
+ paneState.claudeSessionId = claudeSessionId;
406
+ }
407
+ } catch {}
408
+ }
409
+
410
+ if (claudeSessionId) {
411
+ try {
412
+ const { getSessionMessages } = await import(
413
+ "@anthropic-ai/claude-agent-sdk"
414
+ );
415
+ const msgs = await getSessionMessages(claudeSessionId, {
416
+ dir: process.cwd(),
417
+ includeSystemMessages: true,
418
+ });
419
+ transcriptBeforeCount = msgs.length;
420
+ } catch {}
421
+ }
422
+
423
+ // Step 2: Send text via paste buffer (atomic, avoids ARG_MAX)
424
+ sendViaPasteBuffer(paneId, prompt);
204
425
  await Bun.sleep(150);
205
426
 
206
427
  // Step 3: Submit with per-round capture verification
@@ -215,7 +436,7 @@ export async function claudeQuery(options: ClaudeQueryOptions): Promise<ClaudeQu
215
436
  if (visibleNorm.includes(normalizedPrompt) && !paneHasActiveTask(visibleCapture) && paneLooksReady(visibleCapture)) {
216
437
  sendSpecialKey(paneId, "C-u");
217
438
  await Bun.sleep(80);
218
- sendLiteralText(paneId, prompt);
439
+ sendViaPasteBuffer(paneId, prompt);
219
440
  await Bun.sleep(120);
220
441
  delivered = await attemptSubmitRounds(paneId, normalizedPrompt, 4, submitPresses);
221
442
  }
@@ -243,44 +464,35 @@ export async function claudeQuery(options: ClaudeQueryOptions): Promise<ClaudeQu
243
464
  }
244
465
  }
245
466
 
246
- // Step 6: Wait for response by detecting output stabilization or prompt return
467
+ // Step 6: Wait for response completion
247
468
  const deadline = Date.now() + responseTimeoutMs;
248
- let lastContent = "";
249
- let stableCount = 0;
250
-
251
- // Give Claude time to start processing
252
- await Bun.sleep(3_000);
253
-
254
- while (Date.now() < deadline) {
255
- const currentContent = normalizeTmuxLines(capturePaneScrollback(paneId));
256
-
257
- // Must have new content compared to before we sent
258
- if (currentContent === beforeContent) {
259
- await Bun.sleep(pollIntervalMs);
260
- continue;
261
- }
262
-
263
- // Use visible capture for state detection to avoid stale scrollback matches
264
- const visible = capturePaneVisible(paneId);
265
- if (paneLooksReady(visible) && !paneHasActiveTask(visible)) {
266
- return { output: currentContent, delivered };
267
- }
268
-
269
- if (currentContent === lastContent) {
270
- stableCount++;
271
- if (stableCount >= 3) {
272
- return { output: currentContent, delivered };
273
- }
274
- } else {
275
- stableCount = 0;
276
- }
277
469
 
278
- lastContent = currentContent;
279
- await Bun.sleep(pollIntervalMs);
470
+ // ── Transcript-based idle detection (preferred) ──
471
+ // Uses the Claude Agent SDK's session_state_changed message as the
472
+ // authoritative turn-over signal. Falls back to pane capture if the
473
+ // SDK is unavailable or the session ID couldn't be resolved.
474
+ if (claudeSessionId && transcriptBeforeCount >= 0) {
475
+ const transcriptResult = await waitForIdleViaTranscript(
476
+ paneId,
477
+ claudeSessionId,
478
+ transcriptBeforeCount,
479
+ deadline,
480
+ pollIntervalMs,
481
+ delivered,
482
+ );
483
+ if (transcriptResult) return transcriptResult;
484
+ // null → SDK error; fall through to pane-capture
280
485
  }
281
486
 
282
- // Timeout return whatever we have
283
- return { output: lastContent || capturePaneScrollback(paneId), delivered };
487
+ // ── Pane-capture fallback ──
488
+ return waitForIdleViaCapture(
489
+ paneId,
490
+ beforeContent,
491
+ deadline,
492
+ pollIntervalMs,
493
+ idleConfirmCount,
494
+ delivered,
495
+ );
284
496
  }
285
497
 
286
498
  // ---------------------------------------------------------------------------
@@ -302,6 +514,13 @@ export interface ClaudeQueryDefaults {
302
514
  maxSubmitRounds?: number;
303
515
  /** Timeout in ms waiting for pane to be ready before sending (default: 30s) */
304
516
  readyTimeoutMs?: number;
517
+ /**
518
+ * Number of consecutive idle detections required before considering the
519
+ * response complete (default: 2). Increase for long-running multi-step
520
+ * tasks (e.g., explorer stages with sub-agent dispatches) to avoid
521
+ * false-idle returns between steps.
522
+ */
523
+ idleConfirmCount?: number;
305
524
  }
306
525
 
307
526
  /**
@@ -373,10 +592,7 @@ export class ClaudeSessionWrapper {
373
592
  // Static source validation
374
593
  // ---------------------------------------------------------------------------
375
594
 
376
- export interface ClaudeValidationWarning {
377
- rule: string;
378
- message: string;
379
- }
595
+ import { createProviderValidator } from "../types.ts";
380
596
 
381
597
  /**
382
598
  * Validate a Claude workflow source file for common mistakes.
@@ -384,26 +600,19 @@ export interface ClaudeValidationWarning {
384
600
  * Warns on direct usage of createClaudeSession/claudeQuery — the runtime
385
601
  * now handles init/cleanup automatically via s.client and s.session.
386
602
  */
387
- export function validateClaudeWorkflow(source: string): ClaudeValidationWarning[] {
388
- const warnings: ClaudeValidationWarning[] = [];
389
-
390
- if (/\bcreateClaudeSession\b/.test(source)) {
391
- warnings.push({
392
- rule: "claude/manual-session",
393
- message:
394
- "Manual createClaudeSession() call detected. The runtime auto-starts the Claude CLI — " +
395
- "use s.session.query() instead of claudeQuery(). Pass chatFlags via the second arg to ctx.stage().",
396
- });
397
- }
398
-
399
- if (/\bclaudeQuery\b/.test(source)) {
400
- warnings.push({
401
- rule: "claude/manual-query",
402
- message:
403
- "Direct claudeQuery() call detected. Use s.session.query(prompt) instead — " +
404
- "it wraps claudeQuery with the correct paneId.",
405
- });
406
- }
407
-
408
- return warnings;
409
- }
603
+ export const validateClaudeWorkflow = createProviderValidator([
604
+ {
605
+ pattern: /\bcreateClaudeSession\b/,
606
+ rule: "claude/manual-session",
607
+ message:
608
+ "Manual createClaudeSession() call detected. The runtime auto-starts the Claude CLI — " +
609
+ "use s.session.query() instead of claudeQuery(). Pass chatFlags via the second arg to ctx.stage().",
610
+ },
611
+ {
612
+ pattern: /\bclaudeQuery\b/,
613
+ rule: "claude/manual-query",
614
+ message:
615
+ "Direct claudeQuery() call detected. Use s.session.query(prompt) instead — " +
616
+ "it wraps claudeQuery with the correct paneId.",
617
+ },
618
+ ]);
@@ -5,34 +5,24 @@
5
5
  * `s.client` and `s.session` instead of manual SDK client creation.
6
6
  */
7
7
 
8
- export interface CopilotValidationWarning {
9
- rule: string;
10
- message: string;
11
- }
8
+ import { createProviderValidator } from "../types.ts";
12
9
 
13
10
  /**
14
11
  * Validate a Copilot workflow source file for common mistakes.
15
12
  */
16
- export function validateCopilotWorkflow(source: string): CopilotValidationWarning[] {
17
- const warnings: CopilotValidationWarning[] = [];
18
-
19
- if (/\bnew\s+CopilotClient\b/.test(source)) {
20
- warnings.push({
21
- rule: "copilot/manual-client",
22
- message:
23
- "Manual CopilotClient creation detected. Use s.client instead — " +
24
- "the runtime auto-creates and cleans up the client.",
25
- });
26
- }
27
-
28
- if (/\bclient\.createSession\b/.test(source)) {
29
- warnings.push({
30
- rule: "copilot/manual-session",
31
- message:
32
- "Manual createSession() call detected. Use s.session instead — " +
33
- "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage().",
34
- });
35
- }
36
-
37
- return warnings;
38
- }
13
+ export const validateCopilotWorkflow = createProviderValidator([
14
+ {
15
+ pattern: /\bnew\s+CopilotClient\b/,
16
+ rule: "copilot/manual-client",
17
+ message:
18
+ "Manual CopilotClient creation detected. Use s.client instead — " +
19
+ "the runtime auto-creates and cleans up the client.",
20
+ },
21
+ {
22
+ pattern: /\bclient\.createSession\b/,
23
+ rule: "copilot/manual-session",
24
+ message:
25
+ "Manual createSession() call detected. Use s.session instead — " +
26
+ "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage().",
27
+ },
28
+ ]);
@@ -5,34 +5,24 @@
5
5
  * `s.client` and `s.session` instead of manual SDK client creation.
6
6
  */
7
7
 
8
- export interface OpenCodeValidationWarning {
9
- rule: string;
10
- message: string;
11
- }
8
+ import { createProviderValidator } from "../types.ts";
12
9
 
13
10
  /**
14
11
  * Validate an OpenCode workflow source file for common mistakes.
15
12
  */
16
- export function validateOpenCodeWorkflow(source: string): OpenCodeValidationWarning[] {
17
- const warnings: OpenCodeValidationWarning[] = [];
18
-
19
- if (/\bcreateOpencodeClient\b/.test(source)) {
20
- warnings.push({
21
- rule: "opencode/manual-client",
22
- message:
23
- "Manual createOpencodeClient() call detected. Use s.client instead — " +
24
- "the runtime auto-creates the client. Pass client config as the second arg to ctx.stage().",
25
- });
26
- }
27
-
28
- if (/\bclient\.session\.create\b/.test(source)) {
29
- warnings.push({
30
- rule: "opencode/manual-session",
31
- message:
32
- "Manual client.session.create() call detected. Use s.session instead — " +
33
- "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage().",
34
- });
35
- }
36
-
37
- return warnings;
38
- }
13
+ export const validateOpenCodeWorkflow = createProviderValidator([
14
+ {
15
+ pattern: /\bcreateOpencodeClient\b/,
16
+ rule: "opencode/manual-client",
17
+ message:
18
+ "Manual createOpencodeClient() call detected. Use s.client instead — " +
19
+ "the runtime auto-creates the client. Pass client config as the second arg to ctx.stage().",
20
+ },
21
+ {
22
+ pattern: /\bclient\.session\.create\b/,
23
+ rule: "opencode/manual-session",
24
+ message:
25
+ "Manual client.session.create() call detected. Use s.session instead — " +
26
+ "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage().",
27
+ },
28
+ ]);