ax-agents 0.1.1 → 0.1.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/ax.js +336 -55
- package/package.json +1 -1
package/ax.js
CHANGED
|
@@ -144,6 +144,15 @@ function debugError(context, err) {
|
|
|
144
144
|
if (DEBUG) console.error(`[debug:${context}]`, err instanceof Error ? err.message : err);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Log debug message when AX_DEBUG=1
|
|
149
|
+
* @param {string} tag - Short tag for the debug message (e.g., "poll", "tmux")
|
|
150
|
+
* @param {string} message - The debug message
|
|
151
|
+
*/
|
|
152
|
+
function debug(tag, message) {
|
|
153
|
+
if (DEBUG) console.error(`[${tag}] ${message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
// =============================================================================
|
|
148
157
|
// Project root detection (walk up to find .ai/ directory)
|
|
149
158
|
// =============================================================================
|
|
@@ -214,6 +223,7 @@ function tmuxCapture(session, scrollback = 0) {
|
|
|
214
223
|
* @param {string} keys
|
|
215
224
|
*/
|
|
216
225
|
function tmuxSend(session, keys) {
|
|
226
|
+
debug("tmux", `send session=${session}, keys=${keys}`);
|
|
217
227
|
tmux(["send-keys", "-t", session, keys]);
|
|
218
228
|
}
|
|
219
229
|
|
|
@@ -222,6 +232,7 @@ function tmuxSend(session, keys) {
|
|
|
222
232
|
* @param {string} text
|
|
223
233
|
*/
|
|
224
234
|
function tmuxSendLiteral(session, text) {
|
|
235
|
+
debug("tmux", `sendLiteral session=${session}, text=${text.slice(0, 50)}...`);
|
|
225
236
|
tmux(["send-keys", "-t", session, "-l", text]);
|
|
226
237
|
}
|
|
227
238
|
|
|
@@ -241,11 +252,15 @@ function tmuxKill(session) {
|
|
|
241
252
|
* @param {string} command
|
|
242
253
|
*/
|
|
243
254
|
function tmuxNewSession(session, command) {
|
|
255
|
+
debug("tmux", `newSession: ${session}, command: ${command.slice(0, 80)}...`);
|
|
244
256
|
// Use spawnSync to avoid command injection via session/command
|
|
245
257
|
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
246
258
|
encoding: "utf-8",
|
|
247
259
|
});
|
|
248
|
-
if (result.status !== 0)
|
|
260
|
+
if (result.status !== 0) {
|
|
261
|
+
debug("tmux", `newSession failed: ${result.stderr}`);
|
|
262
|
+
throw new Error(result.stderr || "tmux new-session failed");
|
|
263
|
+
}
|
|
249
264
|
}
|
|
250
265
|
|
|
251
266
|
/**
|
|
@@ -404,11 +419,16 @@ class TimeoutError extends Error {
|
|
|
404
419
|
*/
|
|
405
420
|
async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
406
421
|
const start = Date.now();
|
|
422
|
+
debug("waitFor", `waiting (timeout=${timeoutMs}ms)`);
|
|
407
423
|
while (Date.now() - start < timeoutMs) {
|
|
408
424
|
const screen = tmuxCapture(session);
|
|
409
|
-
if (predicate(screen))
|
|
425
|
+
if (predicate(screen)) {
|
|
426
|
+
debug("waitFor", `matched after ${Date.now() - start}ms`);
|
|
427
|
+
return screen;
|
|
428
|
+
}
|
|
410
429
|
await sleep(POLL_MS);
|
|
411
430
|
}
|
|
431
|
+
debug("waitFor", `timeout after ${timeoutMs}ms`);
|
|
412
432
|
throw new TimeoutError(session);
|
|
413
433
|
}
|
|
414
434
|
|
|
@@ -711,6 +731,7 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
711
731
|
const cwd = (sessionName && getTmuxSessionCwd(sessionName)) || process.cwd();
|
|
712
732
|
const projectPath = getClaudeProjectPath(cwd);
|
|
713
733
|
const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
|
|
734
|
+
debug("log", `findClaudeLogPath: sessionId=${sessionId}, projectDir=${claudeProjectDir}`);
|
|
714
735
|
|
|
715
736
|
// Check sessions-index.json first
|
|
716
737
|
const indexPath = path.join(claudeProjectDir, "sessions-index.json");
|
|
@@ -720,7 +741,10 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
720
741
|
const entry = index.entries?.find(
|
|
721
742
|
/** @param {{sessionId: string, fullPath?: string}} e */ (e) => e.sessionId === sessionId,
|
|
722
743
|
);
|
|
723
|
-
if (entry?.fullPath)
|
|
744
|
+
if (entry?.fullPath) {
|
|
745
|
+
debug("log", `findClaudeLogPath: found via index -> ${entry.fullPath}`);
|
|
746
|
+
return entry.fullPath;
|
|
747
|
+
}
|
|
724
748
|
} catch (err) {
|
|
725
749
|
debugError("findClaudeLogPath", err);
|
|
726
750
|
}
|
|
@@ -728,8 +752,12 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
728
752
|
|
|
729
753
|
// Fallback: direct path
|
|
730
754
|
const directPath = path.join(claudeProjectDir, `${sessionId}.jsonl`);
|
|
731
|
-
if (existsSync(directPath))
|
|
755
|
+
if (existsSync(directPath)) {
|
|
756
|
+
debug("log", `findClaudeLogPath: found via direct path -> ${directPath}`);
|
|
757
|
+
return directPath;
|
|
758
|
+
}
|
|
732
759
|
|
|
760
|
+
debug("log", `findClaudeLogPath: not found`);
|
|
733
761
|
return null;
|
|
734
762
|
}
|
|
735
763
|
|
|
@@ -738,6 +766,7 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
738
766
|
* @returns {string | null}
|
|
739
767
|
*/
|
|
740
768
|
function findCodexLogPath(sessionName) {
|
|
769
|
+
debug("log", `findCodexLogPath: sessionName=${sessionName}`);
|
|
741
770
|
// For Codex, we need to match by timing since we can't control the session ID
|
|
742
771
|
// Get tmux session creation time
|
|
743
772
|
try {
|
|
@@ -748,13 +777,22 @@ function findCodexLogPath(sessionName) {
|
|
|
748
777
|
encoding: "utf-8",
|
|
749
778
|
},
|
|
750
779
|
);
|
|
751
|
-
if (result.status !== 0)
|
|
780
|
+
if (result.status !== 0) {
|
|
781
|
+
debug("log", `findCodexLogPath: tmux display-message failed`);
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
752
784
|
const createdTs = parseInt(result.stdout.trim(), 10) * 1000; // tmux gives seconds, we need ms
|
|
753
|
-
if (isNaN(createdTs))
|
|
785
|
+
if (isNaN(createdTs)) {
|
|
786
|
+
debug("log", `findCodexLogPath: invalid timestamp`);
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
754
789
|
|
|
755
790
|
// Codex stores sessions in ~/.codex/sessions/YYYY/MM/DD/rollout-TIMESTAMP-UUID.jsonl
|
|
756
791
|
const sessionsDir = path.join(CODEX_CONFIG_DIR, "sessions");
|
|
757
|
-
if (!existsSync(sessionsDir))
|
|
792
|
+
if (!existsSync(sessionsDir)) {
|
|
793
|
+
debug("log", `findCodexLogPath: sessions dir not found`);
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
758
796
|
|
|
759
797
|
const startDate = new Date(createdTs);
|
|
760
798
|
const year = startDate.getFullYear().toString();
|
|
@@ -762,11 +800,15 @@ function findCodexLogPath(sessionName) {
|
|
|
762
800
|
const day = String(startDate.getDate()).padStart(2, "0");
|
|
763
801
|
|
|
764
802
|
const dayDir = path.join(sessionsDir, year, month, day);
|
|
765
|
-
if (!existsSync(dayDir))
|
|
803
|
+
if (!existsSync(dayDir)) {
|
|
804
|
+
debug("log", `findCodexLogPath: day dir not found: ${dayDir}`);
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
766
807
|
|
|
767
808
|
// Find the closest log file created after the tmux session started
|
|
768
809
|
// Use 60-second window to handle slow startups (model download, first run, heavy load)
|
|
769
810
|
const files = readdirSync(dayDir).filter((f) => f.endsWith(".jsonl"));
|
|
811
|
+
debug("log", `findCodexLogPath: ${files.length} jsonl files in ${dayDir}`);
|
|
770
812
|
const candidates = [];
|
|
771
813
|
|
|
772
814
|
for (const file of files) {
|
|
@@ -789,11 +831,16 @@ function findCodexLogPath(sessionName) {
|
|
|
789
831
|
}
|
|
790
832
|
}
|
|
791
833
|
|
|
792
|
-
if (candidates.length === 0)
|
|
834
|
+
if (candidates.length === 0) {
|
|
835
|
+
debug("log", `findCodexLogPath: no candidates within time window`);
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
793
838
|
// Return the closest match
|
|
794
839
|
candidates.sort((a, b) => a.diff - b.diff);
|
|
840
|
+
debug("log", `findCodexLogPath: found ${candidates.length} candidates, best: ${candidates[0].path}`);
|
|
795
841
|
return candidates[0].path;
|
|
796
842
|
} catch {
|
|
843
|
+
debug("log", `findCodexLogPath: exception caught`);
|
|
797
844
|
return null;
|
|
798
845
|
}
|
|
799
846
|
}
|
|
@@ -977,11 +1024,12 @@ function tailJsonl(logPath, fromOffset) {
|
|
|
977
1024
|
*/
|
|
978
1025
|
|
|
979
1026
|
/**
|
|
980
|
-
* Format a JSONL entry for streaming display.
|
|
1027
|
+
* Format a Claude Code JSONL log entry for streaming display.
|
|
1028
|
+
* Claude format: {type: "assistant", message: {content: [...]}}
|
|
981
1029
|
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
|
|
982
1030
|
* @returns {string | null}
|
|
983
1031
|
*/
|
|
984
|
-
function
|
|
1032
|
+
function formatClaudeLogEntry(entry) {
|
|
985
1033
|
// Skip tool_result entries (they can be very verbose)
|
|
986
1034
|
if (entry.type === "tool_result") return null;
|
|
987
1035
|
|
|
@@ -994,6 +1042,9 @@ function formatEntry(entry) {
|
|
|
994
1042
|
for (const part of parts) {
|
|
995
1043
|
if (part.type === "text" && part.text) {
|
|
996
1044
|
output.push(part.text);
|
|
1045
|
+
} else if (part.type === "thinking" && part.thinking) {
|
|
1046
|
+
// Include thinking blocks - extended thinking models put responses here
|
|
1047
|
+
output.push(part.thinking);
|
|
997
1048
|
} else if (part.type === "tool_use" || part.type === "tool_call") {
|
|
998
1049
|
const name = part.name || part.tool || "tool";
|
|
999
1050
|
const input = part.input || part.arguments || {};
|
|
@@ -1006,12 +1057,69 @@ function formatEntry(entry) {
|
|
|
1006
1057
|
}
|
|
1007
1058
|
output.push(`> ${name}(${summary})`);
|
|
1008
1059
|
}
|
|
1009
|
-
// Skip thinking blocks - internal reasoning
|
|
1010
1060
|
}
|
|
1011
1061
|
|
|
1012
1062
|
return output.length > 0 ? output.join("\n") : null;
|
|
1013
1063
|
}
|
|
1014
1064
|
|
|
1065
|
+
/**
|
|
1066
|
+
* Format a Codex JSONL log entry for streaming display.
|
|
1067
|
+
* Codex format:
|
|
1068
|
+
* - {type: "response_item", payload: {type: "message", role: "assistant", content: [{type: "output_text", text: "..."}]}}
|
|
1069
|
+
* - {type: "response_item", payload: {type: "function_call", name: "...", arguments: "{...}"}}
|
|
1070
|
+
* - {type: "event_msg", payload: {type: "agent_message", message: "..."}}
|
|
1071
|
+
* @param {{type?: string, payload?: {type?: string, role?: string, name?: string, arguments?: string, message?: string, content?: Array<{type?: string, text?: string}>}}} entry
|
|
1072
|
+
* @returns {string | null}
|
|
1073
|
+
*/
|
|
1074
|
+
function formatCodexLogEntry(entry) {
|
|
1075
|
+
// Skip function_call_output entries (equivalent to tool_result - can be verbose)
|
|
1076
|
+
if (entry.type === "response_item" && entry.payload?.type === "function_call_output") {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Handle function calls
|
|
1081
|
+
if (entry.type === "response_item" && entry.payload?.type === "function_call") {
|
|
1082
|
+
const name = entry.payload.name || "tool";
|
|
1083
|
+
let summary = "";
|
|
1084
|
+
try {
|
|
1085
|
+
const args = JSON.parse(entry.payload.arguments || "{}");
|
|
1086
|
+
if (name === "shell_command" && args.command) {
|
|
1087
|
+
summary = args.command.slice(0, 50);
|
|
1088
|
+
} else {
|
|
1089
|
+
const target = args.file_path || args.path || args.pattern || "";
|
|
1090
|
+
summary = target.split("/").pop() || target.slice(0, 30);
|
|
1091
|
+
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
summary = "...";
|
|
1094
|
+
}
|
|
1095
|
+
return `> ${name}(${summary})`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Handle assistant messages (final response)
|
|
1099
|
+
if (entry.type === "response_item" && entry.payload?.role === "assistant") {
|
|
1100
|
+
const parts = entry.payload.content || [];
|
|
1101
|
+
const output = [];
|
|
1102
|
+
for (const part of parts) {
|
|
1103
|
+
if ((part.type === "output_text" || part.type === "text") && part.text) {
|
|
1104
|
+
output.push(part.text);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return output.length > 0 ? output.join("\n") : null;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Handle streaming agent messages
|
|
1111
|
+
if (entry.type === "event_msg" && entry.payload?.type === "agent_message") {
|
|
1112
|
+
return entry.payload.message || null;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Handle agent reasoning (thinking during review)
|
|
1116
|
+
if (entry.type === "event_msg" && entry.payload?.type === "agent_reasoning") {
|
|
1117
|
+
return entry.payload.text || null;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1015
1123
|
/**
|
|
1016
1124
|
* Extract pending tool from confirmation screen.
|
|
1017
1125
|
* @param {string} screen
|
|
@@ -1075,16 +1183,23 @@ function resolveSessionName(partial) {
|
|
|
1075
1183
|
|
|
1076
1184
|
const sessions = tmuxListSessions();
|
|
1077
1185
|
const agentSessions = sessions.filter((s) => parseSessionName(s));
|
|
1186
|
+
debug("session", `resolving "${partial}" from ${agentSessions.length} agent sessions`);
|
|
1078
1187
|
|
|
1079
1188
|
// Exact match
|
|
1080
|
-
if (agentSessions.includes(partial))
|
|
1189
|
+
if (agentSessions.includes(partial)) {
|
|
1190
|
+
debug("session", `exact match: ${partial}`);
|
|
1191
|
+
return partial;
|
|
1192
|
+
}
|
|
1081
1193
|
|
|
1082
1194
|
// Archangel name match (e.g., "reviewer" matches "claude-archangel-reviewer-uuid")
|
|
1083
1195
|
const archangelMatches = agentSessions.filter((s) => {
|
|
1084
1196
|
const parsed = parseSessionName(s);
|
|
1085
1197
|
return parsed?.archangelName === partial;
|
|
1086
1198
|
});
|
|
1087
|
-
if (archangelMatches.length === 1)
|
|
1199
|
+
if (archangelMatches.length === 1) {
|
|
1200
|
+
debug("session", `archangel match: ${archangelMatches[0]}`);
|
|
1201
|
+
return archangelMatches[0];
|
|
1202
|
+
}
|
|
1088
1203
|
if (archangelMatches.length > 1) {
|
|
1089
1204
|
console.log("ERROR: ambiguous archangel name. Matches:");
|
|
1090
1205
|
for (const m of archangelMatches) console.log(` ${m}`);
|
|
@@ -1093,7 +1208,10 @@ function resolveSessionName(partial) {
|
|
|
1093
1208
|
|
|
1094
1209
|
// Prefix match
|
|
1095
1210
|
const matches = agentSessions.filter((s) => s.startsWith(partial));
|
|
1096
|
-
if (matches.length === 1)
|
|
1211
|
+
if (matches.length === 1) {
|
|
1212
|
+
debug("session", `prefix match: ${matches[0]}`);
|
|
1213
|
+
return matches[0];
|
|
1214
|
+
}
|
|
1097
1215
|
if (matches.length > 1) {
|
|
1098
1216
|
console.log("ERROR: ambiguous session prefix. Matches:");
|
|
1099
1217
|
for (const m of matches) console.log(` ${m}`);
|
|
@@ -1105,13 +1223,17 @@ function resolveSessionName(partial) {
|
|
|
1105
1223
|
const parsed = parseSessionName(s);
|
|
1106
1224
|
return parsed?.uuid?.startsWith(partial);
|
|
1107
1225
|
});
|
|
1108
|
-
if (uuidMatches.length === 1)
|
|
1226
|
+
if (uuidMatches.length === 1) {
|
|
1227
|
+
debug("session", `UUID match: ${uuidMatches[0]}`);
|
|
1228
|
+
return uuidMatches[0];
|
|
1229
|
+
}
|
|
1109
1230
|
if (uuidMatches.length > 1) {
|
|
1110
1231
|
console.log("ERROR: ambiguous UUID prefix. Matches:");
|
|
1111
1232
|
for (const m of uuidMatches) console.log(` ${m}`);
|
|
1112
1233
|
process.exit(1);
|
|
1113
1234
|
}
|
|
1114
1235
|
|
|
1236
|
+
debug("session", `no match found, returning as-is: ${partial}`);
|
|
1115
1237
|
return partial; // Return as-is, let caller handle not found
|
|
1116
1238
|
}
|
|
1117
1239
|
|
|
@@ -1991,7 +2113,10 @@ const State = {
|
|
|
1991
2113
|
* @returns {string} The detected state
|
|
1992
2114
|
*/
|
|
1993
2115
|
function detectState(screen, config) {
|
|
1994
|
-
if (!screen)
|
|
2116
|
+
if (!screen) {
|
|
2117
|
+
debug("state", "no screen -> STARTING");
|
|
2118
|
+
return State.STARTING;
|
|
2119
|
+
}
|
|
1995
2120
|
|
|
1996
2121
|
const lines = screen.trim().split("\n");
|
|
1997
2122
|
const lastLines = lines.slice(-8).join("\n");
|
|
@@ -2000,6 +2125,7 @@ function detectState(screen, config) {
|
|
|
2000
2125
|
|
|
2001
2126
|
// Rate limited - check recent lines (not full screen to avoid matching historical output)
|
|
2002
2127
|
if (config.rateLimitPattern && config.rateLimitPattern.test(recentLines)) {
|
|
2128
|
+
debug("state", "rateLimitPattern matched -> RATE_LIMITED");
|
|
2003
2129
|
return State.RATE_LIMITED;
|
|
2004
2130
|
}
|
|
2005
2131
|
|
|
@@ -2011,6 +2137,7 @@ function detectState(screen, config) {
|
|
|
2011
2137
|
/3:\s*Good/i.test(recentLines) &&
|
|
2012
2138
|
/0:\s*Dismiss/i.test(recentLines)
|
|
2013
2139
|
) {
|
|
2140
|
+
debug("state", "feedback modal detected -> FEEDBACK_MODAL");
|
|
2014
2141
|
return State.FEEDBACK_MODAL;
|
|
2015
2142
|
}
|
|
2016
2143
|
|
|
@@ -2019,54 +2146,71 @@ function detectState(screen, config) {
|
|
|
2019
2146
|
for (const pattern of confirmPatterns) {
|
|
2020
2147
|
if (typeof pattern === "function") {
|
|
2021
2148
|
// Functions check lastLines first (most specific), then recentLines
|
|
2022
|
-
if (pattern(lastLines))
|
|
2023
|
-
|
|
2149
|
+
if (pattern(lastLines)) {
|
|
2150
|
+
debug("state", "confirmPattern function matched lastLines -> CONFIRMING");
|
|
2151
|
+
return State.CONFIRMING;
|
|
2152
|
+
}
|
|
2153
|
+
if (pattern(recentLines)) {
|
|
2154
|
+
debug("state", "confirmPattern function matched recentLines -> CONFIRMING");
|
|
2155
|
+
return State.CONFIRMING;
|
|
2156
|
+
}
|
|
2024
2157
|
} else {
|
|
2025
2158
|
// String patterns check recentLines (bounded range)
|
|
2026
|
-
if (recentLines.includes(pattern))
|
|
2159
|
+
if (recentLines.includes(pattern)) {
|
|
2160
|
+
debug("state", `confirmPattern "${pattern}" matched -> CONFIRMING`);
|
|
2161
|
+
return State.CONFIRMING;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// Check for active work patterns first (agent shows prompt even while working)
|
|
2167
|
+
const activeWorkPatterns = config.activeWorkPatterns || [];
|
|
2168
|
+
for (const p of activeWorkPatterns) {
|
|
2169
|
+
const matched = p instanceof RegExp ? p.test(lastLines) : lastLines.includes(p);
|
|
2170
|
+
if (matched) {
|
|
2171
|
+
debug("state", `activeWorkPattern "${p}" matched -> THINKING`);
|
|
2172
|
+
return State.THINKING;
|
|
2027
2173
|
}
|
|
2028
2174
|
}
|
|
2029
2175
|
|
|
2030
2176
|
// Ready - check BEFORE thinking to avoid false positives from timing messages like "✻ Worked for 45s"
|
|
2031
2177
|
// If the prompt symbol is visible, the agent is ready regardless of spinner characters in timing messages
|
|
2032
2178
|
if (lastLines.includes(config.promptSymbol)) {
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
const linesArray = lastLines.split("\n");
|
|
2036
|
-
const promptWithPaste = linesArray.some(
|
|
2037
|
-
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text"),
|
|
2038
|
-
);
|
|
2039
|
-
if (!promptWithPaste) {
|
|
2040
|
-
return State.READY;
|
|
2041
|
-
}
|
|
2042
|
-
// If prompt has pasted content, Claude is still processing - not ready yet
|
|
2179
|
+
debug("state", `promptSymbol "${config.promptSymbol}" found -> READY`);
|
|
2180
|
+
return State.READY;
|
|
2043
2181
|
}
|
|
2044
2182
|
|
|
2045
2183
|
// Thinking - spinners (check last lines only)
|
|
2046
2184
|
const spinners = config.spinners || [];
|
|
2047
|
-
|
|
2048
|
-
|
|
2185
|
+
for (const s of spinners) {
|
|
2186
|
+
if (lastLines.includes(s)) {
|
|
2187
|
+
debug("state", `spinner "${s}" matched -> THINKING`);
|
|
2188
|
+
return State.THINKING;
|
|
2189
|
+
}
|
|
2049
2190
|
}
|
|
2050
2191
|
// Thinking - text patterns (last lines) - supports strings, regexes, and functions
|
|
2051
2192
|
const thinkingPatterns = config.thinkingPatterns || [];
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2193
|
+
for (const p of thinkingPatterns) {
|
|
2194
|
+
let matched = false;
|
|
2195
|
+
if (typeof p === "function") matched = p(lastLines);
|
|
2196
|
+
else if (p instanceof RegExp) matched = p.test(lastLines);
|
|
2197
|
+
else matched = lastLines.includes(p);
|
|
2198
|
+
if (matched) {
|
|
2199
|
+
debug("state", `thinkingPattern "${p}" matched -> THINKING`);
|
|
2200
|
+
return State.THINKING;
|
|
2201
|
+
}
|
|
2060
2202
|
}
|
|
2061
2203
|
|
|
2062
2204
|
// Update prompt
|
|
2063
2205
|
if (config.updatePromptPatterns) {
|
|
2064
2206
|
const { screen: sp, lastLines: lp } = config.updatePromptPatterns;
|
|
2065
2207
|
if (sp && sp.some((p) => screen.includes(p)) && lp && lp.some((p) => lastLines.includes(p))) {
|
|
2208
|
+
debug("state", "updatePromptPatterns matched -> UPDATE_PROMPT");
|
|
2066
2209
|
return State.UPDATE_PROMPT;
|
|
2067
2210
|
}
|
|
2068
2211
|
}
|
|
2069
2212
|
|
|
2213
|
+
debug("state", "no patterns matched -> STARTING");
|
|
2070
2214
|
return State.STARTING;
|
|
2071
2215
|
}
|
|
2072
2216
|
|
|
@@ -2094,6 +2238,7 @@ function detectState(screen, config) {
|
|
|
2094
2238
|
* @property {string[]} [spinners]
|
|
2095
2239
|
* @property {RegExp} [rateLimitPattern]
|
|
2096
2240
|
* @property {(string | RegExp | ((lines: string) => boolean))[]} [thinkingPatterns]
|
|
2241
|
+
* @property {(string | RegExp)[]} [activeWorkPatterns]
|
|
2097
2242
|
* @property {ConfirmPattern[]} [confirmPatterns]
|
|
2098
2243
|
* @property {UpdatePromptPatterns | null} [updatePromptPatterns]
|
|
2099
2244
|
* @property {string[]} [responseMarkers]
|
|
@@ -2105,6 +2250,7 @@ function detectState(screen, config) {
|
|
|
2105
2250
|
* @property {string} [safeAllowedTools]
|
|
2106
2251
|
* @property {string | null} [sessionIdFlag]
|
|
2107
2252
|
* @property {((sessionName: string) => string | null) | null} [logPathFinder]
|
|
2253
|
+
* @property {((entry: object) => string | null) | null} [logEntryFormatter]
|
|
2108
2254
|
*/
|
|
2109
2255
|
|
|
2110
2256
|
class Agent {
|
|
@@ -2128,6 +2274,8 @@ class Agent {
|
|
|
2128
2274
|
this.rateLimitPattern = config.rateLimitPattern;
|
|
2129
2275
|
/** @type {(string | RegExp | ((lines: string) => boolean))[]} */
|
|
2130
2276
|
this.thinkingPatterns = config.thinkingPatterns || [];
|
|
2277
|
+
/** @type {(string | RegExp)[]} */
|
|
2278
|
+
this.activeWorkPatterns = config.activeWorkPatterns || [];
|
|
2131
2279
|
/** @type {ConfirmPattern[]} */
|
|
2132
2280
|
this.confirmPatterns = config.confirmPatterns || [];
|
|
2133
2281
|
/** @type {UpdatePromptPatterns | null} */
|
|
@@ -2150,6 +2298,8 @@ class Agent {
|
|
|
2150
2298
|
this.sessionIdFlag = config.sessionIdFlag || null;
|
|
2151
2299
|
/** @type {((sessionName: string) => string | null) | null} */
|
|
2152
2300
|
this.logPathFinder = config.logPathFinder || null;
|
|
2301
|
+
/** @type {((entry: object) => string | null) | null} */
|
|
2302
|
+
this.logEntryFormatter = config.logEntryFormatter || null;
|
|
2153
2303
|
}
|
|
2154
2304
|
|
|
2155
2305
|
/**
|
|
@@ -2162,24 +2312,31 @@ class Agent {
|
|
|
2162
2312
|
let base;
|
|
2163
2313
|
if (yolo) {
|
|
2164
2314
|
base = this.yoloCommand;
|
|
2315
|
+
debug("command", `mode=yolo`);
|
|
2165
2316
|
} else if (customAllowedTools) {
|
|
2166
2317
|
// Custom permissions from --auto-approve flag
|
|
2167
2318
|
// Escape for shell: backslashes first, then double quotes
|
|
2168
2319
|
const escaped = customAllowedTools.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2169
2320
|
base = `${this.startCommand} --allowedTools "${escaped}"`;
|
|
2321
|
+
debug("command", `mode=custom, allowedTools=${customAllowedTools}`);
|
|
2170
2322
|
} else if (this.safeAllowedTools) {
|
|
2171
2323
|
// Default: auto-approve safe read-only operations
|
|
2172
2324
|
base = `${this.startCommand} --allowedTools "${this.safeAllowedTools}"`;
|
|
2325
|
+
debug("command", `mode=safe, allowedTools=${this.safeAllowedTools}`);
|
|
2173
2326
|
} else {
|
|
2174
2327
|
base = this.startCommand;
|
|
2328
|
+
debug("command", `mode=default`);
|
|
2175
2329
|
}
|
|
2176
2330
|
// Some agents support session ID flags for deterministic session tracking
|
|
2177
2331
|
if (this.sessionIdFlag && sessionName) {
|
|
2178
2332
|
const parsed = parseSessionName(sessionName);
|
|
2179
2333
|
if (parsed?.uuid) {
|
|
2180
|
-
|
|
2334
|
+
const cmd = `${base} ${this.sessionIdFlag} ${parsed.uuid}`;
|
|
2335
|
+
debug("command", `full: ${cmd}`);
|
|
2336
|
+
return cmd;
|
|
2181
2337
|
}
|
|
2182
2338
|
}
|
|
2339
|
+
debug("command", `full: ${base}`);
|
|
2183
2340
|
return base;
|
|
2184
2341
|
}
|
|
2185
2342
|
|
|
@@ -2289,6 +2446,18 @@ class Agent {
|
|
|
2289
2446
|
return null;
|
|
2290
2447
|
}
|
|
2291
2448
|
|
|
2449
|
+
/**
|
|
2450
|
+
* Format a log entry for streaming display.
|
|
2451
|
+
* @param {object} entry
|
|
2452
|
+
* @returns {string | null}
|
|
2453
|
+
*/
|
|
2454
|
+
formatLogEntry(entry) {
|
|
2455
|
+
if (this.logEntryFormatter) {
|
|
2456
|
+
return this.logEntryFormatter(entry);
|
|
2457
|
+
}
|
|
2458
|
+
return null;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2292
2461
|
/**
|
|
2293
2462
|
* @param {string} screen
|
|
2294
2463
|
* @returns {string}
|
|
@@ -2299,6 +2468,7 @@ class Agent {
|
|
|
2299
2468
|
spinners: this.spinners,
|
|
2300
2469
|
rateLimitPattern: this.rateLimitPattern,
|
|
2301
2470
|
thinkingPatterns: this.thinkingPatterns,
|
|
2471
|
+
activeWorkPatterns: this.activeWorkPatterns,
|
|
2302
2472
|
confirmPatterns: this.confirmPatterns,
|
|
2303
2473
|
updatePromptPatterns: this.updatePromptPatterns,
|
|
2304
2474
|
});
|
|
@@ -2528,11 +2698,13 @@ const CodexAgent = new Agent({
|
|
|
2528
2698
|
screen: ["Update available"],
|
|
2529
2699
|
lastLines: ["Skip"],
|
|
2530
2700
|
},
|
|
2701
|
+
activeWorkPatterns: ["esc to interrupt"],
|
|
2531
2702
|
responseMarkers: ["•", "- ", "**"],
|
|
2532
2703
|
chromePatterns: ["context left", "for shortcuts"],
|
|
2533
|
-
reviewOptions: {
|
|
2704
|
+
reviewOptions: { branch: "1", uncommitted: "2", commit: "3", custom: "4" },
|
|
2534
2705
|
envVar: "AX_SESSION",
|
|
2535
2706
|
logPathFinder: findCodexLogPath,
|
|
2707
|
+
logEntryFormatter: formatCodexLogEntry,
|
|
2536
2708
|
});
|
|
2537
2709
|
|
|
2538
2710
|
// =============================================================================
|
|
@@ -2550,6 +2722,7 @@ const ClaudeAgent = new Agent({
|
|
|
2550
2722
|
rateLimitPattern: /rate.?limit/i,
|
|
2551
2723
|
// Claude uses whimsical verbs like "Wibbling…", "Dancing…", etc. Match any capitalized -ing word + ellipsis (… or ...)
|
|
2552
2724
|
thinkingPatterns: ["Thinking", /[A-Z][a-z]+ing(…|\.\.\.)/],
|
|
2725
|
+
activeWorkPatterns: ["[Pasted text", "esc to interrupt"],
|
|
2553
2726
|
confirmPatterns: [
|
|
2554
2727
|
"Do you want to make this edit",
|
|
2555
2728
|
"Do you want to run this command",
|
|
@@ -2581,6 +2754,7 @@ const ClaudeAgent = new Agent({
|
|
|
2581
2754
|
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
2582
2755
|
return null;
|
|
2583
2756
|
},
|
|
2757
|
+
logEntryFormatter: formatClaudeLogEntry,
|
|
2584
2758
|
});
|
|
2585
2759
|
|
|
2586
2760
|
// =============================================================================
|
|
@@ -2599,9 +2773,11 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2599
2773
|
const start = Date.now();
|
|
2600
2774
|
const initialScreen = tmuxCapture(session);
|
|
2601
2775
|
const initialState = agent.getState(initialScreen);
|
|
2776
|
+
debug("waitUntilReady", `start: initialState=${initialState}, timeout=${timeoutMs}ms`);
|
|
2602
2777
|
|
|
2603
2778
|
// Dismiss feedback modal if present
|
|
2604
2779
|
if (initialState === State.FEEDBACK_MODAL) {
|
|
2780
|
+
debug("waitUntilReady", `dismissing feedback modal`);
|
|
2605
2781
|
tmuxSend(session, "0");
|
|
2606
2782
|
await sleep(200);
|
|
2607
2783
|
} else if (
|
|
@@ -2610,6 +2786,7 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2610
2786
|
initialState === State.CONFIRMING ||
|
|
2611
2787
|
initialState === State.READY
|
|
2612
2788
|
) {
|
|
2789
|
+
debug("waitUntilReady", `already in terminal state: ${initialState}`);
|
|
2613
2790
|
return { state: initialState, screen: initialScreen };
|
|
2614
2791
|
}
|
|
2615
2792
|
|
|
@@ -2620,15 +2797,18 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2620
2797
|
|
|
2621
2798
|
// Dismiss feedback modal if it appears
|
|
2622
2799
|
if (state === State.FEEDBACK_MODAL) {
|
|
2800
|
+
debug("waitUntilReady", `dismissing feedback modal`);
|
|
2623
2801
|
tmuxSend(session, "0");
|
|
2624
2802
|
await sleep(200);
|
|
2625
2803
|
continue;
|
|
2626
2804
|
}
|
|
2627
2805
|
|
|
2628
2806
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING || state === State.READY) {
|
|
2807
|
+
debug("waitUntilReady", `reached state=${state} after ${Date.now() - start}ms`);
|
|
2629
2808
|
return { state, screen };
|
|
2630
2809
|
}
|
|
2631
2810
|
}
|
|
2811
|
+
debug("waitUntilReady", `timeout after ${timeoutMs}ms`);
|
|
2632
2812
|
throw new TimeoutError(session);
|
|
2633
2813
|
}
|
|
2634
2814
|
|
|
@@ -2644,11 +2824,19 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
|
2644
2824
|
const { onPoll, onStateChange, onReady } = hooks;
|
|
2645
2825
|
const start = Date.now();
|
|
2646
2826
|
const initialScreen = tmuxCapture(session);
|
|
2827
|
+
const initialState = agent.getState(initialScreen);
|
|
2828
|
+
debug("poll", `start: initialState=${initialState}, timeoutMs=${timeoutMs}`);
|
|
2647
2829
|
|
|
2648
2830
|
let lastScreen = initialScreen;
|
|
2649
2831
|
let lastState = null;
|
|
2650
2832
|
let stableAt = null;
|
|
2651
2833
|
let sawActivity = false;
|
|
2834
|
+
let sawThinking = false;
|
|
2835
|
+
|
|
2836
|
+
// Fallback timeout: accept READY without sawThinking after this many ms
|
|
2837
|
+
// This handles fast responses where we might miss the THINKING state
|
|
2838
|
+
// Clamp to timeoutMs so short timeouts don't always fail
|
|
2839
|
+
const THINKING_FALLBACK_MS = Math.min(10000, timeoutMs);
|
|
2652
2840
|
|
|
2653
2841
|
while (Date.now() - start < timeoutMs) {
|
|
2654
2842
|
const screen = tmuxCapture(session);
|
|
@@ -2676,19 +2864,28 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
|
2676
2864
|
lastScreen = screen;
|
|
2677
2865
|
stableAt = Date.now();
|
|
2678
2866
|
if (screen !== initialScreen) {
|
|
2867
|
+
if (!sawActivity) debug("poll", "sawActivity=true (screen changed from initial)");
|
|
2679
2868
|
sawActivity = true;
|
|
2680
2869
|
}
|
|
2681
2870
|
}
|
|
2682
2871
|
|
|
2872
|
+
// Check if we can return READY
|
|
2683
2873
|
if (sawActivity && stableAt && Date.now() - stableAt >= STABLE_MS) {
|
|
2684
2874
|
if (state === State.READY) {
|
|
2685
|
-
|
|
2686
|
-
|
|
2875
|
+
// Require sawThinking OR enough time has passed (fallback for fast responses)
|
|
2876
|
+
const elapsed = Date.now() - start;
|
|
2877
|
+
if (sawThinking || elapsed >= THINKING_FALLBACK_MS) {
|
|
2878
|
+
debug("poll", `returning READY after ${elapsed}ms (sawThinking=${sawThinking})`);
|
|
2879
|
+
if (onReady) onReady(screen);
|
|
2880
|
+
return { state, screen };
|
|
2881
|
+
}
|
|
2687
2882
|
}
|
|
2688
2883
|
}
|
|
2689
2884
|
|
|
2690
2885
|
if (state === State.THINKING) {
|
|
2691
2886
|
sawActivity = true;
|
|
2887
|
+
if (!sawThinking) debug("poll", "sawThinking=true");
|
|
2888
|
+
sawThinking = true;
|
|
2692
2889
|
}
|
|
2693
2890
|
|
|
2694
2891
|
await sleep(POLL_MS);
|
|
@@ -2718,20 +2915,43 @@ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2718
2915
|
let logPath = agent.findLogPath(session);
|
|
2719
2916
|
let logOffset = logPath && existsSync(logPath) ? statSync(logPath).size : 0;
|
|
2720
2917
|
let printedThinking = false;
|
|
2918
|
+
debug("stream", `start: logPath=${logPath || "null"}, logOffset=${logOffset}`);
|
|
2919
|
+
// Sliding window for deduplication - only dedupe recent messages
|
|
2920
|
+
// This catches Codex's duplicate log entries (A,B,A,B pattern) while
|
|
2921
|
+
// allowing legitimate repeated messages across turns
|
|
2922
|
+
/** @type {string[]} */
|
|
2923
|
+
const recentMessages = [];
|
|
2924
|
+
const DEDUPE_WINDOW = 10;
|
|
2721
2925
|
|
|
2722
2926
|
const streamNewEntries = () => {
|
|
2723
2927
|
if (!logPath) {
|
|
2724
2928
|
logPath = agent.findLogPath(session);
|
|
2725
2929
|
if (logPath && existsSync(logPath)) {
|
|
2726
|
-
|
|
2930
|
+
// Read from beginning when file is first discovered
|
|
2931
|
+
// (Claude creates log file when first message is sent)
|
|
2932
|
+
debug("stream", `log file discovered: ${logPath}`);
|
|
2933
|
+
logOffset = 0;
|
|
2727
2934
|
}
|
|
2728
2935
|
}
|
|
2729
2936
|
if (logPath) {
|
|
2730
2937
|
const { entries, newOffset } = tailJsonl(logPath, logOffset);
|
|
2938
|
+
if (entries.length > 0) {
|
|
2939
|
+
debug("stream", `read ${entries.length} entries, offset ${logOffset} -> ${newOffset}`);
|
|
2940
|
+
}
|
|
2731
2941
|
logOffset = newOffset;
|
|
2732
2942
|
for (const entry of entries) {
|
|
2733
|
-
const formatted =
|
|
2734
|
-
if (formatted)
|
|
2943
|
+
const formatted = agent.formatLogEntry(entry);
|
|
2944
|
+
if (!formatted) continue;
|
|
2945
|
+
|
|
2946
|
+
// Dedupe messages within sliding window (Codex logs can contain duplicates)
|
|
2947
|
+
// Tool calls (starting with ">") are always printed
|
|
2948
|
+
if (!formatted.startsWith(">")) {
|
|
2949
|
+
if (recentMessages.includes(formatted)) continue;
|
|
2950
|
+
recentMessages.push(formatted);
|
|
2951
|
+
if (recentMessages.length > DEDUPE_WINDOW) recentMessages.shift();
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
console.log(formatted);
|
|
2735
2955
|
}
|
|
2736
2956
|
}
|
|
2737
2957
|
};
|
|
@@ -2764,6 +2984,7 @@ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2764
2984
|
*/
|
|
2765
2985
|
async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
2766
2986
|
const deadline = Date.now() + timeoutMs;
|
|
2987
|
+
debug("autoApprove", `starting loop, timeout=${timeoutMs}ms`);
|
|
2767
2988
|
|
|
2768
2989
|
while (Date.now() < deadline) {
|
|
2769
2990
|
const remaining = deadline - Date.now();
|
|
@@ -2772,10 +2993,12 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2772
2993
|
const { state, screen } = await waitFn(agent, session, remaining);
|
|
2773
2994
|
|
|
2774
2995
|
if (state === State.RATE_LIMITED || state === State.READY) {
|
|
2996
|
+
debug("autoApprove", `finished with state=${state}`);
|
|
2775
2997
|
return { state, screen };
|
|
2776
2998
|
}
|
|
2777
2999
|
|
|
2778
3000
|
if (state === State.CONFIRMING) {
|
|
3001
|
+
debug("autoApprove", `auto-approving confirmation`);
|
|
2779
3002
|
tmuxSend(session, agent.approveKey);
|
|
2780
3003
|
await sleep(APPROVE_DELAY_MS);
|
|
2781
3004
|
continue;
|
|
@@ -2785,6 +3008,7 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2785
3008
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
2786
3009
|
}
|
|
2787
3010
|
|
|
3011
|
+
debug("autoApprove", `timeout`);
|
|
2788
3012
|
throw new TimeoutError(session);
|
|
2789
3013
|
}
|
|
2790
3014
|
|
|
@@ -2800,9 +3024,13 @@ async function cmdStart(agent, session, { yolo = false, allowedTools = null } =
|
|
|
2800
3024
|
// Generate session name if not provided
|
|
2801
3025
|
if (!session) {
|
|
2802
3026
|
session = agent.generateSession({ allowedTools, yolo });
|
|
3027
|
+
debug("session", `generated new session: ${session}`);
|
|
2803
3028
|
}
|
|
2804
3029
|
|
|
2805
|
-
if (tmuxHasSession(session))
|
|
3030
|
+
if (tmuxHasSession(session)) {
|
|
3031
|
+
debug("session", `reusing existing session: ${session}`);
|
|
3032
|
+
return session;
|
|
3033
|
+
}
|
|
2806
3034
|
|
|
2807
3035
|
// Check agent CLI is installed before trying to start
|
|
2808
3036
|
const cliCheck = spawnSync("which", [agent.name], { encoding: "utf-8" });
|
|
@@ -2812,6 +3040,7 @@ async function cmdStart(agent, session, { yolo = false, allowedTools = null } =
|
|
|
2812
3040
|
}
|
|
2813
3041
|
|
|
2814
3042
|
const command = agent.getCommand(yolo, session, allowedTools);
|
|
3043
|
+
debug("session", `creating tmux session: ${session}`);
|
|
2815
3044
|
tmuxNewSession(session, command);
|
|
2816
3045
|
|
|
2817
3046
|
const start = Date.now();
|
|
@@ -4291,7 +4520,7 @@ async function cmdAsk(
|
|
|
4291
4520
|
: await cmdStart(agent, session, { yolo, allowedTools });
|
|
4292
4521
|
|
|
4293
4522
|
tmuxSendLiteral(activeSession, message);
|
|
4294
|
-
await sleep(
|
|
4523
|
+
await sleep(200);
|
|
4295
4524
|
tmuxSend(activeSession, "Enter");
|
|
4296
4525
|
|
|
4297
4526
|
if (noWait) {
|
|
@@ -4422,6 +4651,13 @@ async function cmdReview(
|
|
|
4422
4651
|
customInstructions,
|
|
4423
4652
|
{ wait = true, yolo = false, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
|
|
4424
4653
|
) {
|
|
4654
|
+
const validOptions = ["uncommitted", "custom", "branch", "commit"];
|
|
4655
|
+
if (option && !validOptions.includes(option)) {
|
|
4656
|
+
console.error(`Unknown review option: ${option}`);
|
|
4657
|
+
console.error(`Valid options: ${validOptions.join(", ")}`);
|
|
4658
|
+
process.exit(1);
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4425
4661
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
4426
4662
|
|
|
4427
4663
|
// Reset conversation if --fresh and session exists
|
|
@@ -4436,12 +4672,17 @@ async function cmdReview(
|
|
|
4436
4672
|
if (!agent.reviewOptions) {
|
|
4437
4673
|
/** @type {Record<string, string>} */
|
|
4438
4674
|
const reviewPrompts = {
|
|
4439
|
-
pr: "Review the current PR.",
|
|
4440
4675
|
uncommitted: "Review uncommitted changes.",
|
|
4441
|
-
|
|
4676
|
+
branch: customInstructions
|
|
4677
|
+
? `Review changes on the current branch compared to ${customInstructions}.`
|
|
4678
|
+
: "Review changes on the current branch compared to main.",
|
|
4679
|
+
commit: customInstructions
|
|
4680
|
+
? `Review commit ${customInstructions}.`
|
|
4681
|
+
: "Review the most recent commit.",
|
|
4442
4682
|
custom: customInstructions || "Review the code.",
|
|
4443
4683
|
};
|
|
4444
|
-
const prompt = (option && reviewPrompts[option]) || reviewPrompts.
|
|
4684
|
+
const prompt = (option && reviewPrompts[option]) || reviewPrompts.uncommitted;
|
|
4685
|
+
debug("review", `Claude path: noWait=${!wait}, timeoutMs=${timeoutMs}`);
|
|
4445
4686
|
return cmdAsk(agent, session, prompt, { noWait: !wait, yolo, timeoutMs });
|
|
4446
4687
|
}
|
|
4447
4688
|
|
|
@@ -4467,21 +4708,55 @@ async function cmdReview(
|
|
|
4467
4708
|
? /** @type {string} */ (session)
|
|
4468
4709
|
: await cmdStart(agent, session, { yolo });
|
|
4469
4710
|
|
|
4711
|
+
debug("review", `Codex path: sending /review command`);
|
|
4470
4712
|
tmuxSendLiteral(activeSession, "/review");
|
|
4471
4713
|
await sleep(50);
|
|
4472
4714
|
tmuxSend(activeSession, "Enter");
|
|
4473
4715
|
|
|
4716
|
+
debug("review", `waiting for review menu`);
|
|
4474
4717
|
await waitFor(activeSession, (s) => s.includes("Select a review preset") || s.includes("review"));
|
|
4475
4718
|
|
|
4476
4719
|
if (option) {
|
|
4477
4720
|
const key = agent.reviewOptions[option] || option;
|
|
4721
|
+
debug("review", `selecting option=${option} (key=${key})`);
|
|
4478
4722
|
tmuxSend(activeSession, key);
|
|
4479
4723
|
|
|
4480
4724
|
if (option === "custom" && customInstructions) {
|
|
4725
|
+
debug("review", `waiting for custom instructions prompt`);
|
|
4481
4726
|
await waitFor(activeSession, (s) => s.includes("custom") || s.includes("instructions"));
|
|
4482
4727
|
tmuxSendLiteral(activeSession, customInstructions);
|
|
4483
4728
|
await sleep(50);
|
|
4484
4729
|
tmuxSend(activeSession, "Enter");
|
|
4730
|
+
} else if (option === "branch") {
|
|
4731
|
+
debug("review", `waiting for branch picker`);
|
|
4732
|
+
await waitFor(activeSession, (s) => !s.includes("Select a review preset"));
|
|
4733
|
+
await sleep(200);
|
|
4734
|
+
if (customInstructions) {
|
|
4735
|
+
debug("review", `typing branch filter: ${customInstructions}`);
|
|
4736
|
+
tmuxSendLiteral(activeSession, customInstructions);
|
|
4737
|
+
await sleep(100);
|
|
4738
|
+
}
|
|
4739
|
+
tmuxSend(activeSession, "Enter");
|
|
4740
|
+
} else if (option === "commit") {
|
|
4741
|
+
debug("review", `waiting for commit picker`);
|
|
4742
|
+
await waitFor(activeSession, (s) => !s.includes("Select a review preset"));
|
|
4743
|
+
await sleep(200);
|
|
4744
|
+
if (customInstructions) {
|
|
4745
|
+
// Codex commit picker shows messages, not hashes - resolve ref to message
|
|
4746
|
+
let searchTerm = customInstructions;
|
|
4747
|
+
const gitResult = spawnSync("git", ["log", "--format=%s", "-n", "1", customInstructions], {
|
|
4748
|
+
encoding: "utf-8",
|
|
4749
|
+
});
|
|
4750
|
+
if (gitResult.status === 0 && gitResult.stdout.trim()) {
|
|
4751
|
+
// Use first few words of commit message for search
|
|
4752
|
+
searchTerm = gitResult.stdout.trim().slice(0, 40);
|
|
4753
|
+
debug("review", `resolved commit ${customInstructions} -> "${searchTerm}"`);
|
|
4754
|
+
}
|
|
4755
|
+
debug("review", `typing commit filter: ${searchTerm}`);
|
|
4756
|
+
tmuxSendLiteral(activeSession, searchTerm);
|
|
4757
|
+
await sleep(100);
|
|
4758
|
+
}
|
|
4759
|
+
tmuxSend(activeSession, "Enter");
|
|
4485
4760
|
}
|
|
4486
4761
|
}
|
|
4487
4762
|
|
|
@@ -4772,7 +5047,7 @@ Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
|
4772
5047
|
|
|
4773
5048
|
Messaging:
|
|
4774
5049
|
<message> Send message to ${name}
|
|
4775
|
-
review [TYPE]
|
|
5050
|
+
review [TYPE] [TARGET] Review code: uncommitted, branch [base], commit [ref], custom
|
|
4776
5051
|
|
|
4777
5052
|
Sessions:
|
|
4778
5053
|
compact Summarise session to shrink context size
|
|
@@ -4815,6 +5090,8 @@ Examples:
|
|
|
4815
5090
|
${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
|
|
4816
5091
|
${name} --auto-approve='Bash("cargo *")' "run tests" # Session with specific permissions
|
|
4817
5092
|
${name} review uncommitted --wait
|
|
5093
|
+
${name} review branch main # Review changes vs main branch
|
|
5094
|
+
${name} review commit HEAD~1 # Review specific commit
|
|
4818
5095
|
${name} kill # Kill agents in current project
|
|
4819
5096
|
${name} kill --all # Kill all agents across all projects
|
|
4820
5097
|
${name} kill --session=NAME # Kill specific session
|
|
@@ -4952,9 +5229,9 @@ async function main() {
|
|
|
4952
5229
|
if (cmd === "review") {
|
|
4953
5230
|
const customInstructions = await readStdinIfNeeded(positionals[2]);
|
|
4954
5231
|
return cmdReview(agent, session, positionals[1], customInstructions ?? undefined, {
|
|
4955
|
-
wait,
|
|
5232
|
+
wait: !noWait,
|
|
4956
5233
|
fresh,
|
|
4957
|
-
timeoutMs,
|
|
5234
|
+
timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
|
|
4958
5235
|
});
|
|
4959
5236
|
}
|
|
4960
5237
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
@@ -5039,4 +5316,8 @@ export {
|
|
|
5039
5316
|
State,
|
|
5040
5317
|
normalizeAllowedTools,
|
|
5041
5318
|
computePermissionHash,
|
|
5319
|
+
formatClaudeLogEntry,
|
|
5320
|
+
formatCodexLogEntry,
|
|
5321
|
+
CodexAgent,
|
|
5322
|
+
ClaudeAgent,
|
|
5042
5323
|
};
|