@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.
- package/README.md +44 -1
- package/dist/lib/path-root-guard.d.ts +4 -0
- package/dist/lib/path-root-guard.d.ts.map +1 -0
- package/dist/sdk/components/color-utils.d.ts +1 -0
- package/dist/sdk/components/color-utils.d.ts.map +1 -0
- package/dist/sdk/components/connectors.d.ts +3 -2
- package/dist/sdk/components/connectors.d.ts.map +1 -0
- package/dist/sdk/components/connectors.test.d.ts +1 -0
- package/dist/sdk/components/connectors.test.d.ts.map +1 -0
- package/dist/sdk/components/edge.d.ts +2 -1
- package/dist/sdk/components/edge.d.ts.map +1 -0
- package/dist/sdk/components/error-boundary.d.ts +1 -0
- package/dist/sdk/components/error-boundary.d.ts.map +1 -0
- package/dist/sdk/components/graph-theme.d.ts +2 -1
- package/dist/sdk/components/graph-theme.d.ts.map +1 -0
- package/dist/sdk/components/header.d.ts +1 -0
- package/dist/sdk/components/header.d.ts.map +1 -0
- package/dist/sdk/components/hooks.d.ts +15 -0
- package/dist/sdk/components/hooks.d.ts.map +1 -0
- package/dist/sdk/components/layout.d.ts +2 -1
- package/dist/sdk/components/layout.d.ts.map +1 -0
- package/dist/sdk/components/layout.test.d.ts +1 -0
- package/dist/sdk/components/layout.test.d.ts.map +1 -0
- package/dist/sdk/components/node-card.d.ts +5 -3
- package/dist/sdk/components/node-card.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts +3 -2
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.d.ts +2 -1
- package/dist/sdk/components/orchestrator-panel-store.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel.d.ts +2 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts.map +1 -0
- package/dist/sdk/components/status-helpers.d.ts +2 -1
- package/dist/sdk/components/status-helpers.d.ts.map +1 -0
- package/dist/sdk/components/statusline.d.ts +2 -1
- package/dist/sdk/components/statusline.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +11 -8
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -0
- package/dist/sdk/define-workflow.d.ts +2 -1
- package/dist/sdk/define-workflow.d.ts.map +1 -0
- package/dist/sdk/define-workflow.test.d.ts +1 -0
- package/dist/sdk/define-workflow.test.d.ts.map +1 -0
- package/dist/sdk/errors.d.ts +3 -0
- package/dist/sdk/errors.d.ts.map +1 -0
- package/dist/sdk/errors.test.d.ts +2 -0
- package/dist/sdk/errors.test.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +7 -6
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/providers/claude.d.ts +17 -6
- package/dist/sdk/providers/claude.d.ts.map +1 -0
- package/dist/sdk/providers/copilot.d.ts +2 -5
- package/dist/sdk/providers/copilot.d.ts.map +1 -0
- package/dist/sdk/providers/opencode.d.ts +2 -5
- package/dist/sdk/providers/opencode.d.ts.map +1 -0
- package/dist/sdk/runtime/discovery.d.ts +2 -1
- package/dist/sdk/runtime/discovery.d.ts.map +1 -0
- package/dist/sdk/runtime/executor-entry.d.ts +1 -0
- package/dist/sdk/runtime/executor-entry.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +3 -6
- package/dist/sdk/runtime/executor.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.test.d.ts +1 -0
- package/dist/sdk/runtime/executor.test.d.ts.map +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts.map +1 -0
- package/dist/sdk/runtime/loader.d.ts +5 -7
- package/dist/sdk/runtime/loader.d.ts.map +1 -0
- package/dist/sdk/runtime/panel.d.ts +3 -2
- package/dist/sdk/runtime/panel.d.ts.map +1 -0
- package/dist/sdk/runtime/theme.d.ts +1 -0
- package/dist/sdk/runtime/theme.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +26 -8
- package/dist/sdk/runtime/tmux.d.ts.map +1 -0
- package/dist/sdk/types.d.ts +23 -1
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +2 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts +2 -1
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +14 -14
- package/dist/sdk/workflows/index.d.ts.map +1 -0
- package/dist/services/config/definitions.d.ts +85 -0
- package/dist/services/config/definitions.d.ts.map +1 -0
- package/dist/services/system/copy.d.ts +77 -0
- package/dist/services/system/copy.d.ts.map +1 -0
- package/dist/services/system/detect.d.ts +75 -0
- package/dist/services/system/detect.d.ts.map +1 -0
- package/package.json +13 -34
- package/src/cli.ts +11 -10
- package/src/commands/cli/chat/index.ts +11 -11
- package/src/commands/cli/chat.ts +1 -1
- package/src/commands/cli/config.ts +10 -9
- package/src/commands/cli/init/index.ts +11 -11
- package/src/commands/cli/init/onboarding.ts +4 -4
- package/src/commands/cli/init/scm.ts +5 -5
- package/src/commands/cli/init.ts +1 -1
- package/src/commands/cli/workflow-command.test.ts +19 -11
- package/src/commands/cli/workflow.test.ts +2 -2
- package/src/commands/cli/workflow.ts +6 -6
- package/src/lib/merge.ts +17 -31
- package/src/lib/path-root-guard.ts +2 -2
- package/src/lib/spawn.ts +13 -7
- package/src/scripts/bump-version.ts +1 -1
- package/src/scripts/constants.ts +2 -2
- package/src/sdk/components/header.tsx +21 -23
- package/src/sdk/components/hooks.ts +21 -0
- package/src/sdk/components/node-card.tsx +3 -2
- package/src/sdk/components/session-graph-panel.tsx +14 -18
- package/src/sdk/components/workflow-picker-panel.tsx +201 -216
- package/src/sdk/errors.test.ts +56 -0
- package/src/sdk/errors.ts +5 -0
- package/src/sdk/providers/claude.ts +279 -70
- package/src/sdk/providers/copilot.ts +17 -27
- package/src/sdk/providers/opencode.ts +17 -27
- package/src/sdk/runtime/discovery.ts +18 -18
- package/src/sdk/runtime/executor.test.ts +15 -48
- package/src/sdk/runtime/executor.ts +152 -121
- package/src/sdk/runtime/loader.ts +16 -21
- package/src/sdk/runtime/tmux.ts +95 -32
- package/src/sdk/types.ts +45 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +27 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +25 -16
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +25 -24
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +5 -0
- package/src/sdk/workflows/index.ts +3 -3
- package/src/services/config/atomic-config.ts +7 -8
- package/src/services/config/atomic-global-config.ts +9 -9
- package/src/services/config/config-path.ts +1 -1
- package/src/services/config/definitions.ts +3 -4
- package/src/services/config/index.ts +1 -1
- package/src/services/config/settings.ts +30 -36
- package/src/services/system/agents.ts +3 -3
- package/src/services/system/auto-sync.ts +9 -9
- package/src/services/system/copy.ts +9 -9
- package/src/services/system/file-lock.ts +2 -2
- package/src/services/system/install-ui.ts +2 -2
- package/src/services/system/skills.ts +1 -1
- package/src/theme/colors.ts +1 -1
- package/src/theme/logo.ts +1 -1
- package/tsconfig.json +3 -4
- package/dist/chunk-1gb5qxz9.js +0 -1
- package/dist/chunk-fdk7tact.js +0 -417
- package/dist/chunk-xkxndz5g.js +0 -1041
- package/dist/sdk/index.js +0 -52
- package/dist/sdk/workflows/builtin/ralph/claude/index.js +0 -96
- package/dist/sdk/workflows/builtin/ralph/copilot/index.js +0 -119
- package/dist/sdk/workflows/builtin/ralph/opencode/index.js +0 -148
- package/dist/sdk/workflows/index.js +0 -100
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
203
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
//
|
|
283
|
-
return
|
|
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
|
-
|
|
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
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
]);
|