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
package/dist/client/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Arcane Agents</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DHyA1AST.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CT0NFttM.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -21,7 +21,9 @@ async function createServerContext(sessionName) {
|
|
|
21
21
|
const baseConfig = (0, loadConfig_1.loadResolvedConfig)(paths);
|
|
22
22
|
if ((0, loadConfig_1.isNonDefaultSession)(sessionName)) {
|
|
23
23
|
const baseTmuxName = baseConfig.backend.tmux.sessionName;
|
|
24
|
+
const baseSocketName = baseConfig.backend.tmux.socketName;
|
|
24
25
|
baseConfig.backend.tmux.sessionName = `${baseTmuxName}-${sessionName}`;
|
|
26
|
+
baseConfig.backend.tmux.socketName = `${baseSocketName}-${sessionName}`;
|
|
25
27
|
}
|
|
26
28
|
const discoveryService = new discovery_1.DiscoveryService();
|
|
27
29
|
const initialDiscovery = await discoveryService.discover(baseConfig);
|
|
@@ -29,8 +31,8 @@ async function createServerContext(sessionName) {
|
|
|
29
31
|
console.warn(`[arcane-agents] ${warning}`);
|
|
30
32
|
}
|
|
31
33
|
const workers = new workerRepository_1.WorkerRepository(paths.dbPath);
|
|
32
|
-
const tmux = new tmuxAdapter_1.TmuxAdapter(baseConfig.backend.tmux
|
|
33
|
-
await tmux.
|
|
34
|
+
const tmux = new tmuxAdapter_1.TmuxAdapter(baseConfig.backend.tmux);
|
|
35
|
+
await tmux.ensureManagedDefaults();
|
|
34
36
|
const orchestrator = new orchestratorService_1.OrchestratorService(baseConfig, workers, tmux);
|
|
35
37
|
orchestrator.setDiscoveredProjects(initialDiscovery.projects);
|
|
36
38
|
const hub = new realtimeHub_1.RealtimeHub();
|
|
@@ -46,9 +48,12 @@ async function createServerContext(sessionName) {
|
|
|
46
48
|
workerId
|
|
47
49
|
});
|
|
48
50
|
}, baseConfig);
|
|
49
|
-
const terminalBridge = new terminalBridge_1.TerminalBridge(workers, {
|
|
51
|
+
const terminalBridge = new terminalBridge_1.TerminalBridge(workers, baseConfig.backend.tmux, {
|
|
50
52
|
onSubmittedInput: () => {
|
|
51
53
|
statusMonitor.requestPollSoon();
|
|
54
|
+
},
|
|
55
|
+
onTerminalOutput: () => {
|
|
56
|
+
statusMonitor.requestPollSoon(20);
|
|
52
57
|
}
|
|
53
58
|
});
|
|
54
59
|
return {
|
|
@@ -10,6 +10,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
10
10
|
const node_readline_1 = __importDefault(require("node:readline"));
|
|
11
11
|
const bootstrapApp_1 = require("./bootstrapApp");
|
|
12
12
|
const loadConfig_1 = require("./config/loadConfig");
|
|
13
|
+
const prerequisites_1 = require("./setup/prerequisites");
|
|
13
14
|
const appRoot_1 = require("./utils/appRoot");
|
|
14
15
|
const path_1 = require("./utils/path");
|
|
15
16
|
function extractSessionFlag(args) {
|
|
@@ -57,6 +58,8 @@ async function runCli() {
|
|
|
57
58
|
return runStart(sessionName);
|
|
58
59
|
case "init":
|
|
59
60
|
return runInit(commandArgs);
|
|
61
|
+
case "setup":
|
|
62
|
+
return runSetup(commandArgs);
|
|
60
63
|
case "config":
|
|
61
64
|
return runConfig(commandArgs);
|
|
62
65
|
case "doctor":
|
|
@@ -126,6 +129,83 @@ function runInit(args) {
|
|
|
126
129
|
console.log("[arcane-agents] next: edit it with 'arcane-agents config edit'.");
|
|
127
130
|
return 0;
|
|
128
131
|
}
|
|
132
|
+
async function runSetup(args) {
|
|
133
|
+
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
134
|
+
printSetupHelp();
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (args.length > 0) {
|
|
138
|
+
console.error(`[arcane-agents] unknown setup options: ${args.join(", ")}`);
|
|
139
|
+
printSetupHelp();
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
console.log("[arcane-agents] setup");
|
|
143
|
+
const tmuxPath = findExecutable("tmux");
|
|
144
|
+
if (tmuxPath) {
|
|
145
|
+
console.log(`[arcane-agents] tmux: ${tmuxPath}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const installRecommendation = (0, prerequisites_1.recommendTmuxInstall)({
|
|
149
|
+
platform: process.platform,
|
|
150
|
+
lookupCommand: findExecutable,
|
|
151
|
+
isRootUser: process.getuid?.() === 0
|
|
152
|
+
});
|
|
153
|
+
console.log("[arcane-agents] tmux is required but was not found on PATH.");
|
|
154
|
+
if (installRecommendation) {
|
|
155
|
+
console.log(`[arcane-agents] suggested install (${installRecommendation.packageManager}): ${installRecommendation.command}`);
|
|
156
|
+
if (installRecommendation.note) {
|
|
157
|
+
console.log(`[arcane-agents] note: ${installRecommendation.note}`);
|
|
158
|
+
}
|
|
159
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
160
|
+
const approved = await promptConfirm(`[arcane-agents] run that command now? [y/N] `);
|
|
161
|
+
if (approved) {
|
|
162
|
+
const exitCode = runShellCommand(installRecommendation.command);
|
|
163
|
+
if (exitCode !== 0) {
|
|
164
|
+
console.error(`[arcane-agents] install command failed with exit code ${exitCode}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log("[arcane-agents] skipped tmux install.");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log("[arcane-agents] non-interactive terminal detected; not running install command automatically.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log("[arcane-agents] could not determine a package-manager command for tmux on this system.");
|
|
177
|
+
if (process.platform === "win32") {
|
|
178
|
+
console.log("[arcane-agents] run Arcane Agents inside WSL2 or another Unix-like environment, then install tmux there.");
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log("[arcane-agents] install tmux manually, then rerun 'arcane-agents setup' or 'arcane-agents doctor'.");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const paths = (0, loadConfig_1.getArcaneAgentsPaths)();
|
|
186
|
+
try {
|
|
187
|
+
const configResult = ensureStarterConfig(paths);
|
|
188
|
+
if (configResult.created) {
|
|
189
|
+
console.log(`[arcane-agents] wrote starter config: ${paths.configPath}`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(`[arcane-agents] config: ${paths.configPath}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
197
|
+
console.error(`[arcane-agents] failed to prepare config: ${detail}`);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
const doctorExitCode = runDoctor();
|
|
201
|
+
if (doctorExitCode === 0) {
|
|
202
|
+
console.log("[arcane-agents] next: edit your config if needed with 'arcane-agents config edit', then run 'arcane-agents'.");
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log("[arcane-agents] fix the issues above, then rerun 'arcane-agents setup' or 'arcane-agents doctor'.");
|
|
206
|
+
}
|
|
207
|
+
return doctorExitCode;
|
|
208
|
+
}
|
|
129
209
|
function runConfig(args) {
|
|
130
210
|
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
131
211
|
printConfigHelp();
|
|
@@ -359,7 +439,18 @@ function runDoctor() {
|
|
|
359
439
|
checks.push({ status: "ok", label: "tmux", detail: tmuxPath });
|
|
360
440
|
}
|
|
361
441
|
else {
|
|
362
|
-
|
|
442
|
+
const installRecommendation = (0, prerequisites_1.recommendTmuxInstall)({
|
|
443
|
+
platform: process.platform,
|
|
444
|
+
lookupCommand: findExecutable,
|
|
445
|
+
isRootUser: process.getuid?.() === 0
|
|
446
|
+
});
|
|
447
|
+
checks.push({
|
|
448
|
+
status: "fail",
|
|
449
|
+
label: "tmux",
|
|
450
|
+
detail: installRecommendation
|
|
451
|
+
? `not found on PATH (install with: ${installRecommendation.command})`
|
|
452
|
+
: "not found on PATH"
|
|
453
|
+
});
|
|
363
454
|
}
|
|
364
455
|
const paths = (0, loadConfig_1.getArcaneAgentsPaths)();
|
|
365
456
|
if (node_fs_1.default.existsSync(paths.configPath)) {
|
|
@@ -369,7 +460,7 @@ function runDoctor() {
|
|
|
369
460
|
checks.push({
|
|
370
461
|
status: "warn",
|
|
371
462
|
label: "Config",
|
|
372
|
-
detail: `missing at ${paths.configPath} (auto-created on 'arcane-agents start'
|
|
463
|
+
detail: `missing at ${paths.configPath} (auto-created on 'arcane-agents start' or 'arcane-agents setup')`
|
|
373
464
|
});
|
|
374
465
|
}
|
|
375
466
|
const configResult = safeLoadConfig(paths);
|
|
@@ -468,6 +559,7 @@ function printHelp() {
|
|
|
468
559
|
Usage:
|
|
469
560
|
arcane-agents [start] [--session <name>]
|
|
470
561
|
arcane-agents init [--force]
|
|
562
|
+
arcane-agents setup
|
|
471
563
|
arcane-agents config [path|show|edit]
|
|
472
564
|
arcane-agents sessions [list|delete <name>]
|
|
473
565
|
arcane-agents doctor
|
|
@@ -477,6 +569,7 @@ Usage:
|
|
|
477
569
|
Commands:
|
|
478
570
|
start Start the Arcane Agents server
|
|
479
571
|
init Write ~/.config/arcane-agents/config.yaml from config.example.yaml
|
|
572
|
+
setup Guided first-run setup for tmux, config, and dependency checks
|
|
480
573
|
config Print, show, or edit config files
|
|
481
574
|
sessions List or delete named sessions
|
|
482
575
|
doctor Check dependencies and runtime command availability
|
|
@@ -493,6 +586,20 @@ Config paths:
|
|
|
493
586
|
local override: ${paths.localOverridePath}
|
|
494
587
|
`);
|
|
495
588
|
}
|
|
589
|
+
function printSetupHelp() {
|
|
590
|
+
console.log(`Arcane Agents setup
|
|
591
|
+
|
|
592
|
+
Usage:
|
|
593
|
+
arcane-agents setup
|
|
594
|
+
|
|
595
|
+
What it does:
|
|
596
|
+
- checks whether tmux is installed
|
|
597
|
+
- suggests a platform-specific tmux install command
|
|
598
|
+
- can run that command after confirmation in an interactive terminal
|
|
599
|
+
- ensures ~/.config/arcane-agents/config.yaml exists
|
|
600
|
+
- runs 'arcane-agents doctor'
|
|
601
|
+
`);
|
|
602
|
+
}
|
|
496
603
|
function printVersion() {
|
|
497
604
|
console.log(readPackageVersion());
|
|
498
605
|
}
|
|
@@ -556,6 +663,17 @@ function resolvePathToken(token) {
|
|
|
556
663
|
function hasFlag(args, flag) {
|
|
557
664
|
return args.includes(flag);
|
|
558
665
|
}
|
|
666
|
+
function runShellCommand(command) {
|
|
667
|
+
const result = (0, node_child_process_1.spawnSync)("sh", ["-lc", command], {
|
|
668
|
+
stdio: "inherit"
|
|
669
|
+
});
|
|
670
|
+
if (result.error) {
|
|
671
|
+
const detail = result.error.message;
|
|
672
|
+
console.error(`[arcane-agents] failed to launch shell command '${command}': ${detail}`);
|
|
673
|
+
return 1;
|
|
674
|
+
}
|
|
675
|
+
return result.status ?? 1;
|
|
676
|
+
}
|
|
559
677
|
void runCli()
|
|
560
678
|
.then((exitCode) => {
|
|
561
679
|
if (exitCode !== 0) {
|
|
@@ -21,7 +21,8 @@ const projectSchema = zod_1.z.object({
|
|
|
21
21
|
});
|
|
22
22
|
const runtimeSchema = zod_1.z.object({
|
|
23
23
|
command: zod_1.z.array(zod_1.z.string().min(1)).min(1),
|
|
24
|
-
label: zod_1.z.string().min(1)
|
|
24
|
+
label: zod_1.z.string().min(1),
|
|
25
|
+
freshnessWindowMs: zod_1.z.number().int().min(1_000).optional()
|
|
25
26
|
});
|
|
26
27
|
const shortcutSchema = zod_1.z.object({
|
|
27
28
|
label: zod_1.z.string().min(1),
|
|
@@ -41,6 +42,7 @@ const discoveryRuleSchema = zod_1.z.object({
|
|
|
41
42
|
});
|
|
42
43
|
const backendSchema = zod_1.z.object({
|
|
43
44
|
tmux: zod_1.z.object({
|
|
45
|
+
socketName: zod_1.z.string().min(1),
|
|
44
46
|
sessionName: zod_1.z.string().min(1),
|
|
45
47
|
pollIntervalMs: zod_1.z.number().int().min(250)
|
|
46
48
|
})
|
|
@@ -73,6 +75,7 @@ exports.partialConfigSchema = zod_1.z
|
|
|
73
75
|
.object({
|
|
74
76
|
tmux: zod_1.z
|
|
75
77
|
.object({
|
|
78
|
+
socketName: zod_1.z.string().min(1).optional(),
|
|
76
79
|
sessionName: zod_1.z.string().min(1).optional(),
|
|
77
80
|
pollIntervalMs: zod_1.z.number().int().min(250).optional()
|
|
78
81
|
})
|
|
@@ -128,6 +131,7 @@ function createDefaultConfig() {
|
|
|
128
131
|
},
|
|
129
132
|
backend: {
|
|
130
133
|
tmux: {
|
|
134
|
+
socketName: "arcane-agents",
|
|
131
135
|
sessionName: "arcane-agents",
|
|
132
136
|
pollIntervalMs: 2500
|
|
133
137
|
}
|
|
@@ -76,6 +76,16 @@ function registerApiRoutes(app, { orchestrator, hub, statusMonitor }) {
|
|
|
76
76
|
(0, errorResponse_1.handleRequestError)(res, error);
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
|
+
app.post("/api/workers/:workerId/restart", async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const worker = await orchestrator.restart(req.params.workerId);
|
|
82
|
+
hub.broadcast({ type: "worker-updated", worker });
|
|
83
|
+
res.json(worker);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
(0, errorResponse_1.handleRequestError)(res, error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
79
89
|
app.patch("/api/workers/:workerId/position", (req, res) => {
|
|
80
90
|
try {
|
|
81
91
|
const x = Number(req.body?.x);
|
|
@@ -102,6 +102,35 @@ class OrchestratorService {
|
|
|
102
102
|
alreadyStopped: !removed
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
+
async restart(workerId) {
|
|
106
|
+
const worker = this.requireWorker(workerId);
|
|
107
|
+
try {
|
|
108
|
+
await this.tmux.stop(worker.tmuxRef);
|
|
109
|
+
const tmuxRef = await this.tmux.spawnWorker({
|
|
110
|
+
workerId: worker.id,
|
|
111
|
+
windowName: worker.name,
|
|
112
|
+
projectPath: worker.projectPath,
|
|
113
|
+
command: worker.command,
|
|
114
|
+
projectId: worker.projectId,
|
|
115
|
+
runtimeId: worker.runtimeId,
|
|
116
|
+
runtimeLabel: worker.runtimeLabel
|
|
117
|
+
});
|
|
118
|
+
const restarted = {
|
|
119
|
+
...worker,
|
|
120
|
+
status: "idle",
|
|
121
|
+
activityText: undefined,
|
|
122
|
+
activityTool: undefined,
|
|
123
|
+
activityPath: undefined,
|
|
124
|
+
tmuxRef,
|
|
125
|
+
updatedAt: new Date().toISOString()
|
|
126
|
+
};
|
|
127
|
+
this.workers.saveWorker(restarted);
|
|
128
|
+
return restarted;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw (0, appError_1.conflictError)(`Failed to restart agent '${workerId}'.`, "worker_restart_failed");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
105
134
|
updatePosition(workerId, position) {
|
|
106
135
|
const updated = this.workers.updatePosition(workerId, position);
|
|
107
136
|
if (!updated) {
|
|
@@ -20,6 +20,7 @@ function createConfig() {
|
|
|
20
20
|
},
|
|
21
21
|
backend: {
|
|
22
22
|
tmux: {
|
|
23
|
+
socketName: "arcane-agents",
|
|
23
24
|
sessionName: "arcane-agents",
|
|
24
25
|
pollIntervalMs: 2500
|
|
25
26
|
}
|
|
@@ -112,3 +113,61 @@ function createWorker() {
|
|
|
112
113
|
(0, vitest_1.expect)(workers.deleteWorker).not.toHaveBeenCalled();
|
|
113
114
|
});
|
|
114
115
|
});
|
|
116
|
+
(0, vitest_1.describe)("OrchestratorService.restart", () => {
|
|
117
|
+
(0, vitest_1.it)("restarts tmux in place and preserves the worker record", async () => {
|
|
118
|
+
const worker = createWorker();
|
|
119
|
+
const workers = {
|
|
120
|
+
getWorker: vitest_1.vi.fn(() => worker),
|
|
121
|
+
saveWorker: vitest_1.vi.fn()
|
|
122
|
+
};
|
|
123
|
+
const nextTmuxRef = { session: "arcane-agents", window: "worker-1", pane: "%7" };
|
|
124
|
+
const tmux = {
|
|
125
|
+
stop: vitest_1.vi.fn(async () => undefined),
|
|
126
|
+
spawnWorker: vitest_1.vi.fn(async () => nextTmuxRef)
|
|
127
|
+
};
|
|
128
|
+
const service = new orchestratorService_1.OrchestratorService(createConfig(), workers, tmux);
|
|
129
|
+
const result = await service.restart(worker.id);
|
|
130
|
+
(0, vitest_1.expect)(tmux.stop).toHaveBeenCalledWith(worker.tmuxRef);
|
|
131
|
+
(0, vitest_1.expect)(tmux.spawnWorker).toHaveBeenCalledWith({
|
|
132
|
+
workerId: worker.id,
|
|
133
|
+
windowName: worker.name,
|
|
134
|
+
projectPath: worker.projectPath,
|
|
135
|
+
command: worker.command,
|
|
136
|
+
projectId: worker.projectId,
|
|
137
|
+
runtimeId: worker.runtimeId,
|
|
138
|
+
runtimeLabel: worker.runtimeLabel
|
|
139
|
+
});
|
|
140
|
+
(0, vitest_1.expect)(result).toMatchObject({
|
|
141
|
+
...worker,
|
|
142
|
+
status: "idle",
|
|
143
|
+
tmuxRef: nextTmuxRef,
|
|
144
|
+
activityText: undefined,
|
|
145
|
+
activityTool: undefined,
|
|
146
|
+
activityPath: undefined,
|
|
147
|
+
updatedAt: vitest_1.expect.any(String)
|
|
148
|
+
});
|
|
149
|
+
(0, vitest_1.expect)(workers.saveWorker).toHaveBeenCalledWith(result);
|
|
150
|
+
const stopCallOrder = tmux.stop.mock.invocationCallOrder[0] ?? 0;
|
|
151
|
+
const spawnCallOrder = tmux.spawnWorker.mock.invocationCallOrder[0] ?? 0;
|
|
152
|
+
(0, vitest_1.expect)(stopCallOrder).toBeLessThan(spawnCallOrder);
|
|
153
|
+
});
|
|
154
|
+
(0, vitest_1.it)("surfaces a conflict when restart fails", async () => {
|
|
155
|
+
const worker = createWorker();
|
|
156
|
+
const workers = {
|
|
157
|
+
getWorker: vitest_1.vi.fn(() => worker),
|
|
158
|
+
saveWorker: vitest_1.vi.fn()
|
|
159
|
+
};
|
|
160
|
+
const tmux = {
|
|
161
|
+
stop: vitest_1.vi.fn(async () => {
|
|
162
|
+
throw new Error("tmux failure");
|
|
163
|
+
}),
|
|
164
|
+
spawnWorker: vitest_1.vi.fn()
|
|
165
|
+
};
|
|
166
|
+
const service = new orchestratorService_1.OrchestratorService(createConfig(), workers, tmux);
|
|
167
|
+
await (0, vitest_1.expect)(service.restart(worker.id)).rejects.toMatchObject({
|
|
168
|
+
status: 409,
|
|
169
|
+
code: "worker_restart_failed"
|
|
170
|
+
});
|
|
171
|
+
(0, vitest_1.expect)(workers.saveWorker).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recommendTmuxInstall = recommendTmuxInstall;
|
|
4
|
+
function recommendTmuxInstall(options) {
|
|
5
|
+
const sudoPrefix = options.isRootUser ? "" : "sudo ";
|
|
6
|
+
if (options.platform === "darwin" && options.lookupCommand("brew")) {
|
|
7
|
+
return {
|
|
8
|
+
dependency: "tmux",
|
|
9
|
+
packageManager: "Homebrew",
|
|
10
|
+
command: "brew install tmux",
|
|
11
|
+
note: "This installs tmux only."
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (options.platform !== "linux") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
if (options.lookupCommand("brew")) {
|
|
18
|
+
return {
|
|
19
|
+
dependency: "tmux",
|
|
20
|
+
packageManager: "Homebrew",
|
|
21
|
+
command: "brew install tmux",
|
|
22
|
+
note: "This installs tmux only."
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (options.lookupCommand("apt")) {
|
|
26
|
+
return {
|
|
27
|
+
dependency: "tmux",
|
|
28
|
+
packageManager: "apt",
|
|
29
|
+
command: `${sudoPrefix}apt install -y tmux`,
|
|
30
|
+
note: "This installs tmux only and does not run a full system upgrade."
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (options.lookupCommand("apt-get")) {
|
|
34
|
+
return {
|
|
35
|
+
dependency: "tmux",
|
|
36
|
+
packageManager: "apt-get",
|
|
37
|
+
command: `${sudoPrefix}apt-get install -y tmux`,
|
|
38
|
+
note: "This installs tmux only and does not run a full system upgrade."
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (options.lookupCommand("dnf")) {
|
|
42
|
+
return {
|
|
43
|
+
dependency: "tmux",
|
|
44
|
+
packageManager: "dnf",
|
|
45
|
+
command: `${sudoPrefix}dnf install -y tmux`,
|
|
46
|
+
note: "This installs tmux only."
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (options.lookupCommand("pacman")) {
|
|
50
|
+
return {
|
|
51
|
+
dependency: "tmux",
|
|
52
|
+
packageManager: "pacman",
|
|
53
|
+
command: `${sudoPrefix}pacman -S --needed tmux`,
|
|
54
|
+
note: "This installs tmux only and skips reinstalling it if already present."
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (options.lookupCommand("zypper")) {
|
|
58
|
+
return {
|
|
59
|
+
dependency: "tmux",
|
|
60
|
+
packageManager: "zypper",
|
|
61
|
+
command: `${sudoPrefix}zypper install -y tmux`,
|
|
62
|
+
note: "This installs tmux only."
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (options.lookupCommand("apk")) {
|
|
66
|
+
return {
|
|
67
|
+
dependency: "tmux",
|
|
68
|
+
packageManager: "apk",
|
|
69
|
+
command: `${sudoPrefix}apk add tmux`,
|
|
70
|
+
note: "This installs tmux only."
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const prerequisites_1 = require("./prerequisites");
|
|
5
|
+
function lookupFor(commands) {
|
|
6
|
+
const available = new Set(commands);
|
|
7
|
+
return (command) => (available.has(command) ? `/usr/bin/${command}` : undefined);
|
|
8
|
+
}
|
|
9
|
+
(0, vitest_1.describe)("recommendTmuxInstall", () => {
|
|
10
|
+
(0, vitest_1.it)("prefers Homebrew on macOS", () => {
|
|
11
|
+
(0, vitest_1.expect)((0, prerequisites_1.recommendTmuxInstall)({
|
|
12
|
+
platform: "darwin",
|
|
13
|
+
lookupCommand: lookupFor(["brew"])
|
|
14
|
+
})).toEqual({
|
|
15
|
+
dependency: "tmux",
|
|
16
|
+
packageManager: "Homebrew",
|
|
17
|
+
command: "brew install tmux",
|
|
18
|
+
note: "This installs tmux only."
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.it)("returns an apt install command on Debian-like systems", () => {
|
|
22
|
+
(0, vitest_1.expect)((0, prerequisites_1.recommendTmuxInstall)({
|
|
23
|
+
platform: "linux",
|
|
24
|
+
lookupCommand: lookupFor(["apt"])
|
|
25
|
+
})).toEqual({
|
|
26
|
+
dependency: "tmux",
|
|
27
|
+
packageManager: "apt",
|
|
28
|
+
command: "sudo apt install -y tmux",
|
|
29
|
+
note: "This installs tmux only and does not run a full system upgrade."
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)("omits sudo when already running as root", () => {
|
|
33
|
+
(0, vitest_1.expect)((0, prerequisites_1.recommendTmuxInstall)({
|
|
34
|
+
platform: "linux",
|
|
35
|
+
lookupCommand: lookupFor(["pacman"]),
|
|
36
|
+
isRootUser: true
|
|
37
|
+
})).toEqual({
|
|
38
|
+
dependency: "tmux",
|
|
39
|
+
packageManager: "pacman",
|
|
40
|
+
command: "pacman -S --needed tmux",
|
|
41
|
+
note: "This installs tmux only and skips reinstalling it if already present."
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.it)("returns undefined when no supported package manager is available", () => {
|
|
45
|
+
(0, vitest_1.expect)((0, prerequisites_1.recommendTmuxInstall)({
|
|
46
|
+
platform: "linux",
|
|
47
|
+
lookupCommand: lookupFor([])
|
|
48
|
+
})).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -12,7 +12,7 @@ const toolMatchers = [
|
|
|
12
12
|
{ tool: "task", label: "Subtask", regex: /\b(Task|subagent|agent)\b/i },
|
|
13
13
|
{ tool: "todo", label: "Planning", regex: /\b(TodoWrite|todo\b)\b/i },
|
|
14
14
|
{ tool: "web", label: "Fetching", regex: /\b(WebFetch|http|https)\b/i },
|
|
15
|
-
{ tool: "terminal", label: "Terminal", regex: /\b(claude|terminal|tmux)\b/i }
|
|
15
|
+
{ tool: "terminal", label: "Terminal", regex: /\b(claude|codex|terminal|tmux)\b/i }
|
|
16
16
|
];
|
|
17
17
|
const inputPromptLineMatchers = [
|
|
18
18
|
/\[(?:Y\/n|y\/N|y\/n|N\/y)\]\s*$/i,
|
|
@@ -3,17 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildWorkerStatusSignalContext = buildWorkerStatusSignalContext;
|
|
4
4
|
const activityParser_1 = require("../activityParser");
|
|
5
5
|
const runtimeSignals_1 = require("../runtimeSignals");
|
|
6
|
-
function buildWorkerStatusSignalContext({ worker, currentCommand, output, observation, transcriptSnapshot, nowMs, interactiveCommands }) {
|
|
6
|
+
function buildWorkerStatusSignalContext({ worker, currentCommand, output, observation, transcriptSnapshot, runtimeProcess, nowMs, interactiveCommands, runtimeFreshnessWindowMs }) {
|
|
7
7
|
const parsed = (0, activityParser_1.parseActivity)(currentCommand, output);
|
|
8
8
|
const commandLower = currentCommand.toLowerCase();
|
|
9
|
-
const
|
|
9
|
+
const wrappedRuntime = runtimeProcess?.runtime;
|
|
10
|
+
const isClaude = wrappedRuntime === "claude" || (0, runtimeSignals_1.isLikelyClaudeSession)(worker, commandLower);
|
|
11
|
+
const claudeSignals = (0, runtimeSignals_1.detectClaudeSignals)(output);
|
|
10
12
|
const openCodeSignals = (0, runtimeSignals_1.detectOpenCodeSignals)(output);
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const codexSignals = (0, runtimeSignals_1.detectCodexSignals)(output);
|
|
14
|
+
const isOpenCode = wrappedRuntime === "opencode" || (0, runtimeSignals_1.isLikelyOpenCodeSession)(worker, commandLower) || openCodeSignals.prompt || openCodeSignals.active;
|
|
15
|
+
const isCodex = wrappedRuntime === "codex" || (0, runtimeSignals_1.isLikelyCodexSession)(worker, commandLower) || codexSignals.prompt || codexSignals.active;
|
|
16
|
+
const runtimeActivityText = (0, runtimeSignals_1.extractRuntimeActivityText)(output, { isClaude, isOpenCode, isCodex });
|
|
17
|
+
const activeClaudeTask = isClaude ? (0, runtimeSignals_1.extractClaudeActiveTask)(output) : undefined;
|
|
18
|
+
const hasClaudePromptSignal = isClaude && claudeSignals.prompt;
|
|
19
|
+
const hasClaudeProgressSignal = isClaude && claudeSignals.active;
|
|
15
20
|
const openCodePromptSignal = isOpenCode && openCodeSignals.prompt;
|
|
16
21
|
const openCodeActiveSignal = isOpenCode && openCodeSignals.active;
|
|
22
|
+
const codexPromptSignal = isCodex && codexSignals.prompt;
|
|
23
|
+
const codexActiveSignal = isCodex && codexSignals.active;
|
|
17
24
|
const outputQuietForMs = Math.max(0, nowMs - observation.lastOutputChangeAtMs);
|
|
18
25
|
const commandQuietForMs = Math.max(0, nowMs - observation.lastCommandChangeAtMs);
|
|
19
26
|
const createdAtMs = Date.parse(worker.createdAt);
|
|
@@ -29,14 +36,20 @@ function buildWorkerStatusSignalContext({ worker, currentCommand, output, observ
|
|
|
29
36
|
parsed,
|
|
30
37
|
runtimeActivityText,
|
|
31
38
|
activeClaudeTask,
|
|
39
|
+
activeRuntimeProcess: runtimeProcess,
|
|
40
|
+
hasClaudePromptSignal,
|
|
32
41
|
hasClaudeProgressSignal,
|
|
33
42
|
hasOpenCodePromptSignal: openCodePromptSignal,
|
|
34
43
|
hasOpenCodeActiveSignal: openCodeActiveSignal,
|
|
44
|
+
hasCodexPromptSignal: codexPromptSignal,
|
|
45
|
+
hasCodexActiveSignal: codexActiveSignal,
|
|
35
46
|
isClaudeSession: isClaude,
|
|
36
47
|
isOpenCodeSession: isOpenCode,
|
|
48
|
+
isCodexSession: isCodex,
|
|
37
49
|
outputQuietForMs,
|
|
38
50
|
commandQuietForMs,
|
|
39
51
|
workerAgeMs,
|
|
40
|
-
interactiveCommands
|
|
52
|
+
interactiveCommands,
|
|
53
|
+
runtimeFreshnessWindowMs
|
|
41
54
|
};
|
|
42
55
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.recoverableToolErrorMatchers = exports.fatalRuntimeErrorMatchers = exports.openCodeWorkingFreshWindowMs = exports.claudeWorkingFreshWindowMs = exports.genericWorkingFreshWindowMs = exports.openCodeSpawnGraceMs = exports.claudeSpawnGraceMs = exports.cachedActivityWindowMs = exports.stickyWorkingWindowMs = exports.commandWarmupWindowMs = exports.recentErrorSignalWindowMs = exports.parsedStrongEvidenceWindowMs = void 0;
|
|
3
|
+
exports.recoverableToolErrorMatchers = exports.fatalRuntimeErrorMatchers = exports.codexWorkingFreshWindowMs = exports.openCodeWorkingFreshWindowMs = exports.claudeWorkingFreshWindowMs = exports.genericWorkingFreshWindowMs = exports.codexSpawnGraceMs = exports.openCodeSpawnGraceMs = exports.claudeSpawnGraceMs = exports.cachedActivityWindowMs = exports.stickyWorkingWindowMs = exports.commandWarmupWindowMs = exports.recentErrorSignalWindowMs = exports.parsedStrongEvidenceWindowMs = void 0;
|
|
4
4
|
const parsedStrongEvidenceWindowMs = 8_000;
|
|
5
5
|
exports.parsedStrongEvidenceWindowMs = parsedStrongEvidenceWindowMs;
|
|
6
6
|
const recentErrorSignalWindowMs = 15_000;
|
|
@@ -15,12 +15,16 @@ const claudeSpawnGraceMs = 5_000;
|
|
|
15
15
|
exports.claudeSpawnGraceMs = claudeSpawnGraceMs;
|
|
16
16
|
const openCodeSpawnGraceMs = 5_000;
|
|
17
17
|
exports.openCodeSpawnGraceMs = openCodeSpawnGraceMs;
|
|
18
|
-
const
|
|
18
|
+
const codexSpawnGraceMs = 5_000;
|
|
19
|
+
exports.codexSpawnGraceMs = codexSpawnGraceMs;
|
|
20
|
+
const genericWorkingFreshWindowMs = 20_000;
|
|
19
21
|
exports.genericWorkingFreshWindowMs = genericWorkingFreshWindowMs;
|
|
20
22
|
const claudeWorkingFreshWindowMs = 10_000;
|
|
21
23
|
exports.claudeWorkingFreshWindowMs = claudeWorkingFreshWindowMs;
|
|
22
24
|
const openCodeWorkingFreshWindowMs = 12_000;
|
|
23
25
|
exports.openCodeWorkingFreshWindowMs = openCodeWorkingFreshWindowMs;
|
|
26
|
+
const codexWorkingFreshWindowMs = 10_000;
|
|
27
|
+
exports.codexWorkingFreshWindowMs = codexWorkingFreshWindowMs;
|
|
24
28
|
const fatalRuntimeErrorMatchers = [
|
|
25
29
|
/^traceback\b/i,
|
|
26
30
|
/^unhandled(?:\s+\w+)?\s+exception\b/i,
|
|
@@ -26,6 +26,18 @@ function deriveWorkerStatusDecision(context) {
|
|
|
26
26
|
parsedStrongSignal: false
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
if (context.hasCodexPromptSignal && !context.hasCodexActiveSignal && !(0, helpers_1.isInteractiveCommand)(context)) {
|
|
30
|
+
pushReason({ code: "codex-approval-prompt", message: "Codex is waiting on an approval or question response." });
|
|
31
|
+
return finalizeDecision(context, {
|
|
32
|
+
status: "attention",
|
|
33
|
+
activityText: context.runtimeActivityText ?? context.parsed.activity.text ?? "Waiting for approval",
|
|
34
|
+
activityTool: "terminal",
|
|
35
|
+
activityPath: undefined,
|
|
36
|
+
confidence: 0.94,
|
|
37
|
+
reasons,
|
|
38
|
+
parsedStrongSignal: false
|
|
39
|
+
});
|
|
40
|
+
}
|
|
29
41
|
if (context.parsed.activity.needsInput && !(0, helpers_1.isInteractiveCommand)(context)) {
|
|
30
42
|
pushReason({ code: "parser-input-prompt", message: "Terminal output indicates input is required." });
|
|
31
43
|
return finalizeDecision(context, {
|
|
@@ -200,10 +212,15 @@ function finalizeDecision(context, partial) {
|
|
|
200
212
|
workerAgeMs: context.workerAgeMs,
|
|
201
213
|
isClaudeSession: context.isClaudeSession,
|
|
202
214
|
isOpenCodeSession: context.isOpenCodeSession,
|
|
215
|
+
isCodexSession: context.isCodexSession,
|
|
216
|
+
hasClaudePromptSignal: context.hasClaudePromptSignal,
|
|
203
217
|
hasOpenCodePromptSignal: context.hasOpenCodePromptSignal,
|
|
204
218
|
hasOpenCodeActiveSignal: context.hasOpenCodeActiveSignal,
|
|
219
|
+
hasCodexPromptSignal: context.hasCodexPromptSignal,
|
|
220
|
+
hasCodexActiveSignal: context.hasCodexActiveSignal,
|
|
205
221
|
hasClaudeProgressSignal: context.hasClaudeProgressSignal,
|
|
206
222
|
hasActiveClaudeTask: Boolean(context.activeClaudeTask),
|
|
223
|
+
hasActiveRuntimeProcess: Boolean(context.activeRuntimeProcess),
|
|
207
224
|
hasRuntimeActivityText: Boolean(context.runtimeActivityText),
|
|
208
225
|
hasParsedStrongSignal: partial.parsedStrongSignal,
|
|
209
226
|
hasParsedNeedsInput: context.parsed.activity.needsInput,
|