arcane-agents 1.2.0 → 1.2.3
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 +64 -48
- package/config.example.yaml +1 -0
- package/dist/client/assets/index-CT0NFttM.css +32 -0
- package/dist/client/assets/index-DHyA1AST.js +83 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/bootstrap/serverContext.js +8 -3
- package/dist/server/server/cli.js +120 -2
- package/dist/server/server/config/schema.js +5 -1
- package/dist/server/server/http/routes/registerApiRoutes.js +10 -0
- package/dist/server/server/orchestrator/orchestratorService.js +29 -0
- package/dist/server/server/orchestrator/orchestratorService.test.js +59 -0
- package/dist/server/server/orchestrator/spawn/resolveSpawnPlan.test.js +1 -0
- package/dist/server/server/setup/prerequisites.js +74 -0
- package/dist/server/server/setup/prerequisites.test.js +50 -0
- package/dist/server/server/status/activityParser.js +1 -1
- package/dist/server/server/status/engine/signalContext.js +20 -7
- package/dist/server/server/status/engine/stateMachine/constants.js +6 -2
- package/dist/server/server/status/engine/stateMachine/decision.js +17 -0
- package/dist/server/server/status/engine/stateMachine/decision.test.js +64 -0
- package/dist/server/server/status/engine/stateMachine/helpers.js +7 -1
- package/dist/server/server/status/engine/stateMachine/helpers.test.js +10 -1
- package/dist/server/server/status/engine/stateMachine/idleBlockers.js +13 -0
- package/dist/server/server/status/engine/stateMachine/workingEvidence.js +38 -0
- package/dist/server/server/status/runtime/activityTextExtractors.js +40 -0
- package/dist/server/server/status/runtime/claudeSignals.js +114 -33
- package/dist/server/server/status/runtime/claudeSignals.test.js +23 -0
- package/dist/server/server/status/runtime/codexSignals.js +132 -0
- package/dist/server/server/status/runtime/codexSignals.test.js +47 -0
- package/dist/server/server/status/runtime/runtimeProcess.js +86 -0
- package/dist/server/server/status/runtime/sessionDetection.js +15 -0
- package/dist/server/server/status/runtimeSignals.js +8 -1
- package/dist/server/server/status/statusEvaluator.js +4 -2
- package/dist/server/server/status/statusMonitor.js +22 -4
- package/dist/server/server/status/statusMonitor.test.js +9 -2
- package/dist/server/server/status/statusPipeline.js +14 -4
- package/dist/server/server/tmux/tmuxAdapter.js +30 -51
- package/dist/server/server/tmux/tmuxClient.js +35 -0
- package/dist/server/server/tmux/tmuxClient.test.js +58 -0
- package/dist/server/server/ws/terminalBridge.js +16 -2
- package/dist/server/shared/mapConstants.js +4 -0
- package/package.json +4 -3
- package/dist/client/assets/index-CWU29xaz.css +0 -32
- package/dist/client/assets/index-DNXJVqF0.js +0 -83
|
@@ -50,15 +50,21 @@ function createContext(overrides = {}) {
|
|
|
50
50
|
},
|
|
51
51
|
runtimeActivityText: undefined,
|
|
52
52
|
activeClaudeTask: undefined,
|
|
53
|
+
activeRuntimeProcess: undefined,
|
|
54
|
+
hasClaudePromptSignal: false,
|
|
53
55
|
hasClaudeProgressSignal: false,
|
|
54
56
|
hasOpenCodePromptSignal: false,
|
|
55
57
|
hasOpenCodeActiveSignal: false,
|
|
58
|
+
hasCodexPromptSignal: false,
|
|
59
|
+
hasCodexActiveSignal: false,
|
|
56
60
|
isClaudeSession: false,
|
|
57
61
|
isOpenCodeSession: false,
|
|
62
|
+
isCodexSession: false,
|
|
58
63
|
outputQuietForMs: 1_000,
|
|
59
64
|
commandQuietForMs: 5_000,
|
|
60
65
|
workerAgeMs: 30_000,
|
|
61
66
|
interactiveCommands: new Set(),
|
|
67
|
+
runtimeFreshnessWindowMs: undefined,
|
|
62
68
|
...overrides
|
|
63
69
|
};
|
|
64
70
|
}
|
|
@@ -150,4 +156,62 @@ function createContext(overrides = {}) {
|
|
|
150
156
|
(0, vitest_1.expect)(decision.status).toBe("idle");
|
|
151
157
|
(0, vitest_1.expect)(decision.reasons.some((r) => r.code === "opencode-spawn-grace-idle")).toBe(true);
|
|
152
158
|
});
|
|
159
|
+
(0, vitest_1.it)("returns attention for Codex approval prompts", () => {
|
|
160
|
+
const decision = (0, decision_1.deriveWorkerStatusDecision)(createContext({
|
|
161
|
+
worker: {
|
|
162
|
+
...createWorker(),
|
|
163
|
+
runtimeId: "codex",
|
|
164
|
+
runtimeLabel: "Codex",
|
|
165
|
+
command: ["codex"]
|
|
166
|
+
},
|
|
167
|
+
currentCommand: "bash",
|
|
168
|
+
commandLower: "bash",
|
|
169
|
+
runtimeActivityText: "Waiting for approval",
|
|
170
|
+
hasCodexPromptSignal: true,
|
|
171
|
+
hasCodexActiveSignal: false,
|
|
172
|
+
isCodexSession: true
|
|
173
|
+
}));
|
|
174
|
+
(0, vitest_1.expect)(decision.status).toBe("attention");
|
|
175
|
+
(0, vitest_1.expect)(decision.activityText).toBe("Waiting for approval");
|
|
176
|
+
(0, vitest_1.expect)(decision.reasons[0]?.code).toBe("codex-approval-prompt");
|
|
177
|
+
});
|
|
178
|
+
(0, vitest_1.it)("returns working when a wrapped agent runtime process is active", () => {
|
|
179
|
+
const decision = (0, decision_1.deriveWorkerStatusDecision)(createContext({
|
|
180
|
+
currentCommand: "bash",
|
|
181
|
+
commandLower: "bash",
|
|
182
|
+
activeRuntimeProcess: {
|
|
183
|
+
pid: 42,
|
|
184
|
+
runtime: "codex",
|
|
185
|
+
command: "codex",
|
|
186
|
+
args: "codex exec"
|
|
187
|
+
},
|
|
188
|
+
isCodexSession: true,
|
|
189
|
+
outputQuietForMs: 45_000
|
|
190
|
+
}));
|
|
191
|
+
(0, vitest_1.expect)(decision.status).toBe("working");
|
|
192
|
+
(0, vitest_1.expect)(decision.reasons.some((reason) => reason.code === "agent-runtime-child-process")).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
(0, vitest_1.it)("returns idle when the Claude prompt is visible under a shell wrapper", () => {
|
|
195
|
+
const decision = (0, decision_1.deriveWorkerStatusDecision)(createContext({
|
|
196
|
+
worker: {
|
|
197
|
+
...createWorker(),
|
|
198
|
+
runtimeId: "claude",
|
|
199
|
+
runtimeLabel: "Claude",
|
|
200
|
+
command: ["claude"]
|
|
201
|
+
},
|
|
202
|
+
currentCommand: "bash",
|
|
203
|
+
commandLower: "bash",
|
|
204
|
+
isClaudeSession: true,
|
|
205
|
+
hasClaudePromptSignal: true,
|
|
206
|
+
activeRuntimeProcess: {
|
|
207
|
+
pid: 42,
|
|
208
|
+
runtime: "claude",
|
|
209
|
+
command: "claude",
|
|
210
|
+
args: "claude"
|
|
211
|
+
},
|
|
212
|
+
outputQuietForMs: 45_000
|
|
213
|
+
}));
|
|
214
|
+
(0, vitest_1.expect)(decision.status).toBe("idle");
|
|
215
|
+
(0, vitest_1.expect)(decision.reasons[0]?.code).toBe("shell-command-idle");
|
|
216
|
+
});
|
|
153
217
|
});
|
|
@@ -22,7 +22,7 @@ function recentNormalizedLines(output, limit) {
|
|
|
22
22
|
.map((line) => line.toLowerCase());
|
|
23
23
|
}
|
|
24
24
|
function isAgentRuntime(context) {
|
|
25
|
-
return context.isOpenCodeSession || context.isClaudeSession;
|
|
25
|
+
return context.isOpenCodeSession || context.isClaudeSession || context.isCodexSession;
|
|
26
26
|
}
|
|
27
27
|
function shouldSuppressShellHistorySignals(context) {
|
|
28
28
|
if (!isShellCommand(context.commandLower) && !isInteractiveCommand(context)) {
|
|
@@ -53,12 +53,18 @@ function looksLikeActiveRuntimeText(activityText) {
|
|
|
53
53
|
return normalized.startsWith("thinking");
|
|
54
54
|
}
|
|
55
55
|
function statusFreshnessWindowMs(context) {
|
|
56
|
+
if (context.runtimeFreshnessWindowMs !== undefined) {
|
|
57
|
+
return context.runtimeFreshnessWindowMs;
|
|
58
|
+
}
|
|
56
59
|
if (context.isClaudeSession) {
|
|
57
60
|
return constants_1.claudeWorkingFreshWindowMs;
|
|
58
61
|
}
|
|
59
62
|
if (context.isOpenCodeSession) {
|
|
60
63
|
return constants_1.openCodeWorkingFreshWindowMs;
|
|
61
64
|
}
|
|
65
|
+
if (context.isCodexSession) {
|
|
66
|
+
return constants_1.codexWorkingFreshWindowMs;
|
|
67
|
+
}
|
|
62
68
|
return constants_1.genericWorkingFreshWindowMs;
|
|
63
69
|
}
|
|
64
70
|
function isShellCommand(commandLower) {
|
|
@@ -50,15 +50,21 @@ function createContext(overrides = {}) {
|
|
|
50
50
|
},
|
|
51
51
|
runtimeActivityText: undefined,
|
|
52
52
|
activeClaudeTask: undefined,
|
|
53
|
+
activeRuntimeProcess: undefined,
|
|
54
|
+
hasClaudePromptSignal: false,
|
|
53
55
|
hasClaudeProgressSignal: false,
|
|
54
56
|
hasOpenCodePromptSignal: false,
|
|
55
57
|
hasOpenCodeActiveSignal: false,
|
|
58
|
+
hasCodexPromptSignal: false,
|
|
59
|
+
hasCodexActiveSignal: false,
|
|
56
60
|
isClaudeSession: false,
|
|
57
61
|
isOpenCodeSession: false,
|
|
62
|
+
isCodexSession: false,
|
|
58
63
|
outputQuietForMs: 200,
|
|
59
64
|
commandQuietForMs: 300,
|
|
60
65
|
workerAgeMs: 10_000,
|
|
61
66
|
interactiveCommands: new Set(),
|
|
67
|
+
runtimeFreshnessWindowMs: undefined,
|
|
62
68
|
...overrides
|
|
63
69
|
};
|
|
64
70
|
}
|
|
@@ -88,7 +94,10 @@ function createContext(overrides = {}) {
|
|
|
88
94
|
(0, vitest_1.it)("returns freshness windows by runtime session", () => {
|
|
89
95
|
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isClaudeSession: true }))).toBe(10_000);
|
|
90
96
|
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isOpenCodeSession: true }))).toBe(12_000);
|
|
91
|
-
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext())).toBe(
|
|
97
|
+
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isCodexSession: true }))).toBe(10_000);
|
|
98
|
+
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext())).toBe(20_000);
|
|
99
|
+
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ runtimeFreshnessWindowMs: 45_000 }))).toBe(45_000);
|
|
100
|
+
(0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isClaudeSession: true, runtimeFreshnessWindowMs: 30_000 }))).toBe(30_000);
|
|
92
101
|
});
|
|
93
102
|
(0, vitest_1.it)("handles helper utility behavior", () => {
|
|
94
103
|
const values = [];
|
|
@@ -54,6 +54,19 @@ function detectIdleBlocker(context, evidence) {
|
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
if (context.isCodexSession &&
|
|
58
|
+
context.workerAgeMs <= constants_1.codexSpawnGraceMs &&
|
|
59
|
+
!context.hasCodexActiveSignal &&
|
|
60
|
+
!context.activeRuntimeProcess &&
|
|
61
|
+
!evidence.parsedStrongSignal) {
|
|
62
|
+
return {
|
|
63
|
+
reason: {
|
|
64
|
+
code: "codex-spawn-grace-idle",
|
|
65
|
+
message: "During early Codex spawn grace window without active signals.",
|
|
66
|
+
detail: `${Math.round(context.workerAgeMs)}ms since worker creation`
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
57
70
|
const activeWindowMs = (0, helpers_1.statusFreshnessWindowMs)(context);
|
|
58
71
|
if (context.outputQuietForMs > activeWindowMs && context.transcriptSnapshot?.status !== "working") {
|
|
59
72
|
return {
|
|
@@ -37,6 +37,19 @@ function collectWorkingEvidence(context, hasRecoverableParserError) {
|
|
|
37
37
|
activityTextCandidates.push(context.runtimeActivityText ?? "Responding");
|
|
38
38
|
activityToolCandidates.push("terminal");
|
|
39
39
|
}
|
|
40
|
+
if (context.hasCodexActiveSignal) {
|
|
41
|
+
strongReasons.push({ code: "codex-active-signal", message: "Codex active execution signal detected." });
|
|
42
|
+
activityTextCandidates.push(context.runtimeActivityText ?? "Responding");
|
|
43
|
+
activityToolCandidates.push("terminal");
|
|
44
|
+
}
|
|
45
|
+
if (context.activeRuntimeProcess && !(context.activeRuntimeProcess.runtime === "claude" && context.hasClaudePromptSignal)) {
|
|
46
|
+
strongReasons.push({
|
|
47
|
+
code: "agent-runtime-child-process",
|
|
48
|
+
message: `${labelRuntime(context.activeRuntimeProcess.runtime)} is still running under the pane shell.`
|
|
49
|
+
});
|
|
50
|
+
activityTextCandidates.push(context.runtimeActivityText ?? `${labelRuntime(context.activeRuntimeProcess.runtime)} running`);
|
|
51
|
+
activityToolCandidates.push("terminal");
|
|
52
|
+
}
|
|
40
53
|
if ((0, helpers_1.looksLikeActiveRuntimeText)(context.runtimeActivityText)) {
|
|
41
54
|
strongReasons.push({ code: "runtime-activity-text", message: "Runtime activity text indicates active work." });
|
|
42
55
|
(0, helpers_1.pushMaybe)(activityTextCandidates, context.runtimeActivityText);
|
|
@@ -80,6 +93,21 @@ function collectWorkingEvidence(context, hasRecoverableParserError) {
|
|
|
80
93
|
activityToolCandidates.push(context.parsed.activity.tool);
|
|
81
94
|
(0, helpers_1.pushMaybe)(activityPathCandidates, context.parsed.activity.filePath);
|
|
82
95
|
}
|
|
96
|
+
if (context.worker.status === "working" &&
|
|
97
|
+
!(0, helpers_1.isShellCommand)(context.commandLower) &&
|
|
98
|
+
!(0, helpers_1.isInteractiveCommand)(context) &&
|
|
99
|
+
context.outputQuietForMs <= (0, helpers_1.statusFreshnessWindowMs)(context) &&
|
|
100
|
+
strongReasons.length === 0 &&
|
|
101
|
+
weakReasons.length === 0) {
|
|
102
|
+
weakReasons.push({
|
|
103
|
+
code: "output-still-fresh",
|
|
104
|
+
message: "Output is still within the freshness window; maintaining working status.",
|
|
105
|
+
detail: `${Math.round(context.outputQuietForMs)}ms quiet (window: ${(0, helpers_1.statusFreshnessWindowMs)(context)}ms)`
|
|
106
|
+
});
|
|
107
|
+
(0, helpers_1.pushMaybe)(activityTextCandidates, context.runtimeActivityText ?? context.worker.activityText);
|
|
108
|
+
activityToolCandidates.push(context.worker.activityTool ?? "terminal");
|
|
109
|
+
(0, helpers_1.pushMaybe)(activityPathCandidates, context.worker.activityPath);
|
|
110
|
+
}
|
|
83
111
|
return {
|
|
84
112
|
strongReasons,
|
|
85
113
|
weakReasons,
|
|
@@ -98,3 +126,13 @@ function shouldKeepStickyWorking(context, evidence) {
|
|
|
98
126
|
}
|
|
99
127
|
return (0, helpers_1.hasAnyWorkingEvidence)(evidence);
|
|
100
128
|
}
|
|
129
|
+
function labelRuntime(runtime) {
|
|
130
|
+
switch (runtime) {
|
|
131
|
+
case "claude":
|
|
132
|
+
return "Claude";
|
|
133
|
+
case "opencode":
|
|
134
|
+
return "OpenCode";
|
|
135
|
+
case "codex":
|
|
136
|
+
return "Codex";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.extractRuntimeActivityText = extractRuntimeActivityText;
|
|
4
|
+
const codexSignals_1 = require("./codexSignals");
|
|
4
5
|
const openCodeCapturePaneLines = 420;
|
|
5
6
|
const openCodeThinkingContinuationMaxLines = 3;
|
|
7
|
+
const codexCapturePaneLines = 240;
|
|
6
8
|
function extractRuntimeActivityText(output, session) {
|
|
7
9
|
if (session.isClaude) {
|
|
8
10
|
return extractClaudeRuntimeActivityText(output);
|
|
@@ -10,6 +12,9 @@ function extractRuntimeActivityText(output, session) {
|
|
|
10
12
|
if (session.isOpenCode) {
|
|
11
13
|
return extractOpenCodeRuntimeActivityText(output);
|
|
12
14
|
}
|
|
15
|
+
if (session.isCodex) {
|
|
16
|
+
return extractCodexRuntimeActivityText(output);
|
|
17
|
+
}
|
|
13
18
|
return undefined;
|
|
14
19
|
}
|
|
15
20
|
function extractClaudeRuntimeActivityText(output) {
|
|
@@ -163,6 +168,41 @@ function normalizeOpenCodeRuntimeLine(line) {
|
|
|
163
168
|
}
|
|
164
169
|
return withoutFrame;
|
|
165
170
|
}
|
|
171
|
+
function extractCodexRuntimeActivityText(output) {
|
|
172
|
+
const signals = (0, codexSignals_1.detectCodexSignals)(output);
|
|
173
|
+
const linesNewestFirst = output
|
|
174
|
+
.split("\n")
|
|
175
|
+
.slice(-codexCapturePaneLines)
|
|
176
|
+
.map((line) => (0, codexSignals_1.normalizeCodexRuntimeLine)(line))
|
|
177
|
+
.filter((line) => line.length > 0)
|
|
178
|
+
.reverse();
|
|
179
|
+
for (const line of linesNewestFirst) {
|
|
180
|
+
const statusText = (0, codexSignals_1.extractCodexStatusText)(line);
|
|
181
|
+
if (!statusText) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const normalizedStatus = statusText.toLowerCase();
|
|
185
|
+
if (normalizedStatus.includes("waiting on approval") || normalizedStatus.includes("approval requested")) {
|
|
186
|
+
return "Waiting for approval";
|
|
187
|
+
}
|
|
188
|
+
if (normalizedStatus.includes("waiting on user input") ||
|
|
189
|
+
normalizedStatus.includes("question requested") ||
|
|
190
|
+
normalizedStatus.includes("user input requested")) {
|
|
191
|
+
return "Waiting for input";
|
|
192
|
+
}
|
|
193
|
+
if (normalizedStatus === "finished" || normalizedStatus.includes("agent turn complete")) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return truncateActivityText(statusText, 72);
|
|
197
|
+
}
|
|
198
|
+
if (signals.prompt) {
|
|
199
|
+
return "Waiting for approval";
|
|
200
|
+
}
|
|
201
|
+
if (signals.active) {
|
|
202
|
+
return "Responding";
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
166
206
|
function recentLinesNewestFirst(output, limit) {
|
|
167
207
|
return output
|
|
168
208
|
.split("\n")
|
|
@@ -2,55 +2,136 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.extractClaudeActiveTask = extractClaudeActiveTask;
|
|
4
4
|
exports.hasClaudeLiveProgressSignal = hasClaudeLiveProgressSignal;
|
|
5
|
+
exports.hasClaudePromptSignal = hasClaudePromptSignal;
|
|
6
|
+
exports.detectClaudeSignals = detectClaudeSignals;
|
|
5
7
|
exports.isGenericClaudeProgressLabel = isGenericClaudeProgressLabel;
|
|
8
|
+
const claudeSignalWindowLines = 120;
|
|
9
|
+
const claudePromptFreshLineWindow = 8;
|
|
10
|
+
const claudeActiveFreshLineWindow = 12;
|
|
11
|
+
const escapeChar = String.fromCharCode(0x1b);
|
|
12
|
+
const bellChar = String.fromCharCode(0x07);
|
|
6
13
|
function extractClaudeActiveTask(output) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.reverse();
|
|
13
|
-
for (const line of linesNewestFirst) {
|
|
14
|
-
const parentheticalMatch = line.match(/^(?:\*|•|·|✶|✻|✢|✽)\s+(.+?)\s+\((?:[^)]*(?:thinking|thought\s+for)[^)]*)\)\s*$/i);
|
|
15
|
-
const plainProgressMatch = line.match(/^(?:\*|•|·|✶|✻|✢|✽)\s+(.+?)\s*$/);
|
|
16
|
-
const task = parentheticalMatch?.[1]?.trim() ?? plainProgressMatch?.[1]?.trim();
|
|
14
|
+
const lines = recentClaudeLines(output);
|
|
15
|
+
const latestPromptIndex = findLastMatchingIndex(lines, isClaudePromptLine);
|
|
16
|
+
const lowerBound = latestPromptIndex >= 0 ? latestPromptIndex : 0;
|
|
17
|
+
for (let index = lines.length - 1; index >= lowerBound; index -= 1) {
|
|
18
|
+
const task = extractClaudeTaskText(lines[index] ?? "");
|
|
17
19
|
if (task) {
|
|
18
|
-
if (isGenericClaudeProgressLabel(task)) {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
20
|
return task;
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
return undefined;
|
|
25
24
|
}
|
|
26
25
|
function hasClaudeLiveProgressSignal(output) {
|
|
27
|
-
|
|
26
|
+
return detectClaudeSignals(output).active;
|
|
27
|
+
}
|
|
28
|
+
function hasClaudePromptSignal(output) {
|
|
29
|
+
return detectClaudeSignals(output).prompt;
|
|
30
|
+
}
|
|
31
|
+
function detectClaudeSignals(output) {
|
|
32
|
+
const lines = recentClaudeLines(output);
|
|
33
|
+
const newestIndex = lines.length - 1;
|
|
34
|
+
const latestPromptIndex = findLastMatchingIndex(lines, isClaudePromptLine);
|
|
35
|
+
const latestProgressIndex = findLastMatchingIndex(lines, isClaudeProgressLine);
|
|
36
|
+
return {
|
|
37
|
+
prompt: latestPromptIndex >= 0 &&
|
|
38
|
+
newestIndex >= 0 &&
|
|
39
|
+
newestIndex - latestPromptIndex <= claudePromptFreshLineWindow,
|
|
40
|
+
active: latestProgressIndex >= 0 &&
|
|
41
|
+
newestIndex >= 0 &&
|
|
42
|
+
newestIndex - latestProgressIndex <= claudeActiveFreshLineWindow &&
|
|
43
|
+
(latestPromptIndex < 0 || latestProgressIndex >= latestPromptIndex)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isGenericClaudeProgressLabel(text) {
|
|
47
|
+
const normalized = text
|
|
48
|
+
.trim()
|
|
49
|
+
.replace(/[.…]+$/, "")
|
|
50
|
+
.toLowerCase();
|
|
51
|
+
return /^(?:whirring|thinking|saut[eé]ed|churned|baked|accomplishing|conversation compacted)\b/.test(normalized);
|
|
52
|
+
}
|
|
53
|
+
function extractClaudeTaskText(line) {
|
|
54
|
+
const task = extractClaudeBulletText(line);
|
|
55
|
+
if (!task || isGenericClaudeProgressLabel(task)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return task;
|
|
59
|
+
}
|
|
60
|
+
function isClaudeProgressLine(line) {
|
|
61
|
+
const progressText = extractClaudeBulletText(line);
|
|
62
|
+
if (!progressText) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (!isGenericClaudeProgressLabel(progressText)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return /\bfor\s+\d+[ms]\b/i.test(progressText) || /\((?:[^)]*(?:thinking|thought\s+for)[^)]*)\)/i.test(line);
|
|
69
|
+
}
|
|
70
|
+
function extractClaudeBulletText(line) {
|
|
71
|
+
const parentheticalMatch = line.match(/^(?:\*|•|·|✶|✻|✢|✽)\s+(.+?)\s+\((?:[^)]*(?:thinking|thought\s+for)[^)]*)\)\s*$/i);
|
|
72
|
+
const plainProgressMatch = line.match(/^(?:\*|•|·|✶|✻|✢|✽)\s+(.+?)\s*$/);
|
|
73
|
+
return parentheticalMatch?.[1]?.trim() ?? plainProgressMatch?.[1]?.trim();
|
|
74
|
+
}
|
|
75
|
+
function isClaudePromptLine(line) {
|
|
76
|
+
return /^\u276f$/u.test(line) || /^--\s*insert\s*--.*\bbypass permissions\b/i.test(line) || /\bbypass permissions\b/i.test(line);
|
|
77
|
+
}
|
|
78
|
+
function recentClaudeLines(output) {
|
|
79
|
+
return output
|
|
28
80
|
.split("\n")
|
|
29
|
-
.map((line) => line
|
|
81
|
+
.map((line) => normalizeClaudeRuntimeLine(line))
|
|
30
82
|
.filter((line) => line.length > 0)
|
|
31
|
-
.slice(-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
83
|
+
.slice(-claudeSignalWindowLines);
|
|
84
|
+
}
|
|
85
|
+
function normalizeClaudeRuntimeLine(line) {
|
|
86
|
+
return stripTerminalControlSequences(line)
|
|
87
|
+
.replace(/\s+/g, " ")
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
function stripTerminalControlSequences(line) {
|
|
91
|
+
let normalized = "";
|
|
92
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
93
|
+
const current = line[index] ?? "";
|
|
94
|
+
const next = line[index + 1] ?? "";
|
|
95
|
+
if (current === escapeChar && next === "]") {
|
|
96
|
+
index += 2;
|
|
97
|
+
while (index < line.length) {
|
|
98
|
+
const cursor = line[index] ?? "";
|
|
99
|
+
const following = line[index + 1] ?? "";
|
|
100
|
+
if (cursor === bellChar) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (cursor === escapeChar && following === "\\") {
|
|
104
|
+
index += 1;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
index += 1;
|
|
108
|
+
}
|
|
35
109
|
continue;
|
|
36
110
|
}
|
|
37
|
-
|
|
38
|
-
|
|
111
|
+
if (current === escapeChar && next === "[") {
|
|
112
|
+
index += 2;
|
|
113
|
+
while (index < line.length) {
|
|
114
|
+
const code = line.charCodeAt(index);
|
|
115
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
index += 1;
|
|
119
|
+
}
|
|
39
120
|
continue;
|
|
40
121
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (/\bfor\s+\d+[ms]\b/i.test(progressText) || /\((?:[^)]*(?:thinking|thought\s+for)[^)]*)\)/i.test(line)) {
|
|
45
|
-
return true;
|
|
122
|
+
const code = current.charCodeAt(0);
|
|
123
|
+
if ((code >= 0x00 && code <= 0x08) || (code >= 0x0b && code <= 0x1a) || (code >= 0x1c && code <= 0x1f) || code === 0x7f) {
|
|
124
|
+
continue;
|
|
46
125
|
}
|
|
126
|
+
normalized += current;
|
|
47
127
|
}
|
|
48
|
-
return
|
|
128
|
+
return normalized;
|
|
49
129
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
130
|
+
function findLastMatchingIndex(lines, predicate) {
|
|
131
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
132
|
+
if (predicate(lines[index] ?? "")) {
|
|
133
|
+
return index;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return -1;
|
|
56
137
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const claudeSignals_1 = require("./claudeSignals");
|
|
5
|
+
(0, vitest_1.describe)("claudeSignals", () => {
|
|
6
|
+
(0, vitest_1.it)("stops treating progress lines as active once the Claude prompt returns", () => {
|
|
7
|
+
const output = [
|
|
8
|
+
"• Reviewing the final patch",
|
|
9
|
+
"✻ Churned for 1m 42s",
|
|
10
|
+
"",
|
|
11
|
+
"❯",
|
|
12
|
+
" -- INSERT -- ⏵⏵ bypass permissions on (shift+tab to cycle)"
|
|
13
|
+
].join("\n");
|
|
14
|
+
(0, vitest_1.expect)((0, claudeSignals_1.hasClaudePromptSignal)(output)).toBe(true);
|
|
15
|
+
(0, vitest_1.expect)((0, claudeSignals_1.hasClaudeLiveProgressSignal)(output)).toBe(false);
|
|
16
|
+
(0, vitest_1.expect)((0, claudeSignals_1.extractClaudeActiveTask)(output)).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.it)("keeps reporting live progress when Claude is still actively working", () => {
|
|
19
|
+
const output = ["✻ Churned for 12s", "", "Thinking through the next change"].join("\n");
|
|
20
|
+
(0, vitest_1.expect)((0, claudeSignals_1.hasClaudePromptSignal)(output)).toBe(false);
|
|
21
|
+
(0, vitest_1.expect)((0, claudeSignals_1.hasClaudeLiveProgressSignal)(output)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectCodexSignals = detectCodexSignals;
|
|
4
|
+
exports.hasCodexPromptSignal = hasCodexPromptSignal;
|
|
5
|
+
exports.hasCodexActiveSignal = hasCodexActiveSignal;
|
|
6
|
+
exports.extractCodexStatusText = extractCodexStatusText;
|
|
7
|
+
exports.normalizeCodexRuntimeLine = normalizeCodexRuntimeLine;
|
|
8
|
+
const codexSignalWindowLines = 240;
|
|
9
|
+
const codexPromptFreshLineWindow = 24;
|
|
10
|
+
const codexActiveFreshLineWindow = 12;
|
|
11
|
+
const escapeChar = String.fromCharCode(0x1b);
|
|
12
|
+
const bellChar = String.fromCharCode(0x07);
|
|
13
|
+
const codexPromptMatchers = [
|
|
14
|
+
/needs your approval\./i,
|
|
15
|
+
/^would you like to run the following command\?/i,
|
|
16
|
+
/^would you like to make the following edits\?/i,
|
|
17
|
+
/^do you want to approve network access to /i,
|
|
18
|
+
/^permission rule:/i,
|
|
19
|
+
/^yes, just this once$/i,
|
|
20
|
+
/^yes, and don't ask again/i,
|
|
21
|
+
/^yes, and allow these permissions for this session$/i,
|
|
22
|
+
/^yes, and allow this host /i,
|
|
23
|
+
/^yes, provide the requested info$/i,
|
|
24
|
+
/^no, but continue without it$/i,
|
|
25
|
+
/^no, continue without running it$/i,
|
|
26
|
+
/^no, and tell codex what to do differently$/i,
|
|
27
|
+
/^cancel this request$/i
|
|
28
|
+
];
|
|
29
|
+
const codexPromptStatusMatchers = [
|
|
30
|
+
/\bwaiting on approval\b/i,
|
|
31
|
+
/\bwaiting on user input\b/i,
|
|
32
|
+
/\bapproval[-\s]requested\b/i,
|
|
33
|
+
/\buser[-\s]input[-\s]requested\b/i,
|
|
34
|
+
/\bquestion requested\b/i
|
|
35
|
+
];
|
|
36
|
+
const codexActiveMatchers = [/\besc to interrupt\b/i];
|
|
37
|
+
function detectCodexSignals(output) {
|
|
38
|
+
const lines = output
|
|
39
|
+
.split("\n")
|
|
40
|
+
.slice(-codexSignalWindowLines)
|
|
41
|
+
.map((line) => normalizeCodexRuntimeLine(line))
|
|
42
|
+
.filter((line) => line.length > 0);
|
|
43
|
+
const newestIndex = lines.length - 1;
|
|
44
|
+
const latestPromptIndex = findLastMatchingIndex(lines, (line) => isCodexPromptLine(line) || hasCodexPromptStatus(line));
|
|
45
|
+
const latestActiveIndex = findLastMatchingIndex(lines, (line) => codexActiveMatchers.some((matcher) => matcher.test(line)));
|
|
46
|
+
return {
|
|
47
|
+
prompt: latestPromptIndex >= 0 &&
|
|
48
|
+
newestIndex >= 0 &&
|
|
49
|
+
newestIndex - latestPromptIndex <= codexPromptFreshLineWindow,
|
|
50
|
+
active: latestActiveIndex >= 0 &&
|
|
51
|
+
newestIndex >= 0 &&
|
|
52
|
+
newestIndex - latestActiveIndex <= codexActiveFreshLineWindow
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function hasCodexPromptSignal(output) {
|
|
56
|
+
return detectCodexSignals(output).prompt;
|
|
57
|
+
}
|
|
58
|
+
function hasCodexActiveSignal(output) {
|
|
59
|
+
return detectCodexSignals(output).active;
|
|
60
|
+
}
|
|
61
|
+
function extractCodexStatusText(line) {
|
|
62
|
+
const match = normalizeCodexRuntimeLine(line).match(/^status:\s+(.+)$/i);
|
|
63
|
+
return match?.[1]?.trim() || undefined;
|
|
64
|
+
}
|
|
65
|
+
function normalizeCodexRuntimeLine(line) {
|
|
66
|
+
const normalized = stripTerminalControlSequences(line)
|
|
67
|
+
.replace(/\s+/g, " ")
|
|
68
|
+
.trim();
|
|
69
|
+
if (!normalized) {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
const withoutFrame = normalized.replace(/^[│┃╹▀▣⬝■•·]+/, "").trim();
|
|
73
|
+
if (!withoutFrame || /^╹?▀+$/.test(withoutFrame)) {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
return withoutFrame;
|
|
77
|
+
}
|
|
78
|
+
function stripTerminalControlSequences(line) {
|
|
79
|
+
let normalized = "";
|
|
80
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
81
|
+
const current = line[index] ?? "";
|
|
82
|
+
const next = line[index + 1] ?? "";
|
|
83
|
+
if (current === escapeChar && next === "]") {
|
|
84
|
+
index += 2;
|
|
85
|
+
while (index < line.length) {
|
|
86
|
+
const cursor = line[index] ?? "";
|
|
87
|
+
const following = line[index + 1] ?? "";
|
|
88
|
+
if (cursor === bellChar) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (cursor === escapeChar && following === "\\") {
|
|
92
|
+
index += 1;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
index += 1;
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (current === escapeChar && next === "[") {
|
|
100
|
+
index += 2;
|
|
101
|
+
while (index < line.length) {
|
|
102
|
+
const code = line.charCodeAt(index);
|
|
103
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
index += 1;
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const code = current.charCodeAt(0);
|
|
111
|
+
if ((code >= 0x00 && code <= 0x08) || (code >= 0x0b && code <= 0x1a) || (code >= 0x1c && code <= 0x1f) || code === 0x7f) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
normalized += current;
|
|
115
|
+
}
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
function hasCodexPromptStatus(line) {
|
|
119
|
+
const statusText = extractCodexStatusText(line);
|
|
120
|
+
return statusText ? codexPromptStatusMatchers.some((matcher) => matcher.test(statusText)) : false;
|
|
121
|
+
}
|
|
122
|
+
function isCodexPromptLine(line) {
|
|
123
|
+
return codexPromptMatchers.some((matcher) => matcher.test(line));
|
|
124
|
+
}
|
|
125
|
+
function findLastMatchingIndex(lines, predicate) {
|
|
126
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
127
|
+
if (predicate(lines[index] ?? "")) {
|
|
128
|
+
return index;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return -1;
|
|
132
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const activityTextExtractors_1 = require("./activityTextExtractors");
|
|
5
|
+
const codexSignals_1 = require("./codexSignals");
|
|
6
|
+
(0, vitest_1.describe)("detectCodexSignals", () => {
|
|
7
|
+
(0, vitest_1.it)("detects approval prompts from Codex terminal output", () => {
|
|
8
|
+
const output = `
|
|
9
|
+
Would you like to run the following command?
|
|
10
|
+
Permission rule: Yes, just this once
|
|
11
|
+
Yes, and don't ask again for this command in this session
|
|
12
|
+
No, continue without running it
|
|
13
|
+
`;
|
|
14
|
+
(0, vitest_1.expect)((0, codexSignals_1.detectCodexSignals)(output)).toEqual({
|
|
15
|
+
prompt: true,
|
|
16
|
+
active: false
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
(0, vitest_1.it)("detects active Codex turns from interrupt hints", () => {
|
|
20
|
+
const output = `
|
|
21
|
+
Searching repository
|
|
22
|
+
esc to interrupt
|
|
23
|
+
`;
|
|
24
|
+
(0, vitest_1.expect)((0, codexSignals_1.detectCodexSignals)(output)).toEqual({
|
|
25
|
+
prompt: false,
|
|
26
|
+
active: true
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.describe)("extractRuntimeActivityText", () => {
|
|
31
|
+
(0, vitest_1.it)("maps Codex approval status lines to waiting text", () => {
|
|
32
|
+
const output = "Status: Waiting on approval";
|
|
33
|
+
(0, vitest_1.expect)((0, activityTextExtractors_1.extractRuntimeActivityText)(output, {
|
|
34
|
+
isClaude: false,
|
|
35
|
+
isOpenCode: false,
|
|
36
|
+
isCodex: true
|
|
37
|
+
})).toBe("Waiting for approval");
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("maps Codex interrupt hints to responding text", () => {
|
|
40
|
+
const output = "Scanning files\nesc to interrupt";
|
|
41
|
+
(0, vitest_1.expect)((0, activityTextExtractors_1.extractRuntimeActivityText)(output, {
|
|
42
|
+
isClaude: false,
|
|
43
|
+
isOpenCode: false,
|
|
44
|
+
isCodex: true
|
|
45
|
+
})).toBe("Responding");
|
|
46
|
+
});
|
|
47
|
+
});
|