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.
Files changed (43) hide show
  1. package/README.md +64 -48
  2. package/config.example.yaml +1 -0
  3. package/dist/client/assets/index-CT0NFttM.css +32 -0
  4. package/dist/client/assets/index-DHyA1AST.js +83 -0
  5. package/dist/client/index.html +2 -2
  6. package/dist/server/server/bootstrap/serverContext.js +8 -3
  7. package/dist/server/server/cli.js +120 -2
  8. package/dist/server/server/config/schema.js +5 -1
  9. package/dist/server/server/http/routes/registerApiRoutes.js +10 -0
  10. package/dist/server/server/orchestrator/orchestratorService.js +29 -0
  11. package/dist/server/server/orchestrator/orchestratorService.test.js +59 -0
  12. package/dist/server/server/orchestrator/spawn/resolveSpawnPlan.test.js +1 -0
  13. package/dist/server/server/setup/prerequisites.js +74 -0
  14. package/dist/server/server/setup/prerequisites.test.js +50 -0
  15. package/dist/server/server/status/activityParser.js +1 -1
  16. package/dist/server/server/status/engine/signalContext.js +20 -7
  17. package/dist/server/server/status/engine/stateMachine/constants.js +6 -2
  18. package/dist/server/server/status/engine/stateMachine/decision.js +17 -0
  19. package/dist/server/server/status/engine/stateMachine/decision.test.js +64 -0
  20. package/dist/server/server/status/engine/stateMachine/helpers.js +7 -1
  21. package/dist/server/server/status/engine/stateMachine/helpers.test.js +10 -1
  22. package/dist/server/server/status/engine/stateMachine/idleBlockers.js +13 -0
  23. package/dist/server/server/status/engine/stateMachine/workingEvidence.js +38 -0
  24. package/dist/server/server/status/runtime/activityTextExtractors.js +40 -0
  25. package/dist/server/server/status/runtime/claudeSignals.js +114 -33
  26. package/dist/server/server/status/runtime/claudeSignals.test.js +23 -0
  27. package/dist/server/server/status/runtime/codexSignals.js +132 -0
  28. package/dist/server/server/status/runtime/codexSignals.test.js +47 -0
  29. package/dist/server/server/status/runtime/runtimeProcess.js +86 -0
  30. package/dist/server/server/status/runtime/sessionDetection.js +15 -0
  31. package/dist/server/server/status/runtimeSignals.js +8 -1
  32. package/dist/server/server/status/statusEvaluator.js +4 -2
  33. package/dist/server/server/status/statusMonitor.js +22 -4
  34. package/dist/server/server/status/statusMonitor.test.js +9 -2
  35. package/dist/server/server/status/statusPipeline.js +14 -4
  36. package/dist/server/server/tmux/tmuxAdapter.js +30 -51
  37. package/dist/server/server/tmux/tmuxClient.js +35 -0
  38. package/dist/server/server/tmux/tmuxClient.test.js +58 -0
  39. package/dist/server/server/ws/terminalBridge.js +16 -2
  40. package/dist/server/shared/mapConstants.js +4 -0
  41. package/package.json +4 -3
  42. package/dist/client/assets/index-CWU29xaz.css +0 -32
  43. package/dist/client/assets/index-DNXJVqF0.js +0 -83
@@ -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-DNXJVqF0.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CWU29xaz.css">
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.sessionName);
33
- await tmux.ensureSessionClipboardDefaults();
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
- checks.push({ status: "fail", label: "tmux", detail: "not found on PATH" });
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'; use 'arcane-agents config edit')`
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
+ });
@@ -30,6 +30,7 @@ function createConfig() {
30
30
  },
31
31
  backend: {
32
32
  tmux: {
33
+ socketName: "arcane-agents",
33
34
  sessionName: "arcane-agents",
34
35
  pollIntervalMs: 2500
35
36
  }
@@ -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 isClaude = (0, runtimeSignals_1.isLikelyClaudeSession)(worker, commandLower);
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 isOpenCode = (0, runtimeSignals_1.isLikelyOpenCodeSession)(worker, commandLower) || openCodeSignals.prompt || openCodeSignals.active;
12
- const runtimeActivityText = (0, runtimeSignals_1.extractRuntimeActivityText)(output, { isClaude, isOpenCode });
13
- const activeClaudeTask = (0, runtimeSignals_1.extractClaudeActiveTask)(output);
14
- const hasClaudeProgressSignal = isClaude && (0, runtimeSignals_1.hasClaudeLiveProgressSignal)(output);
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 genericWorkingFreshWindowMs = 12_000;
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,