arcane-agents 1.2.1 → 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 (29) 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-BCnWppkv.js → index-DHyA1AST.js} +20 -20
  5. package/dist/client/index.html +2 -2
  6. package/dist/server/server/bootstrap/serverContext.js +5 -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/orchestrator/orchestratorService.test.js +1 -0
  10. package/dist/server/server/orchestrator/spawn/resolveSpawnPlan.test.js +1 -0
  11. package/dist/server/server/setup/prerequisites.js +74 -0
  12. package/dist/server/server/setup/prerequisites.test.js +50 -0
  13. package/dist/server/server/status/engine/signalContext.js +3 -2
  14. package/dist/server/server/status/engine/stateMachine/constants.js +1 -1
  15. package/dist/server/server/status/engine/stateMachine/decision.test.js +1 -0
  16. package/dist/server/server/status/engine/stateMachine/helpers.js +3 -0
  17. package/dist/server/server/status/engine/stateMachine/helpers.test.js +4 -1
  18. package/dist/server/server/status/engine/stateMachine/workingEvidence.js +15 -0
  19. package/dist/server/server/status/statusEvaluator.js +3 -2
  20. package/dist/server/server/status/statusMonitor.js +14 -3
  21. package/dist/server/server/status/statusMonitor.test.js +3 -2
  22. package/dist/server/server/status/statusPipeline.js +5 -3
  23. package/dist/server/server/tmux/tmuxAdapter.js +30 -51
  24. package/dist/server/server/tmux/tmuxClient.js +35 -0
  25. package/dist/server/server/tmux/tmuxClient.test.js +58 -0
  26. package/dist/server/server/ws/terminalBridge.js +5 -2
  27. package/dist/server/shared/mapConstants.js +4 -0
  28. package/package.json +4 -3
  29. package/dist/client/assets/index-Di_KBFPW.css +0 -32
@@ -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-BCnWppkv.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-Di_KBFPW.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,7 +48,7 @@ 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();
52
54
  },
@@ -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
  }
@@ -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
  }
@@ -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
+ });
@@ -3,7 +3,7 @@ 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, runtimeProcess, 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
9
  const wrappedRuntime = runtimeProcess?.runtime;
@@ -49,6 +49,7 @@ function buildWorkerStatusSignalContext({ worker, currentCommand, output, observ
49
49
  outputQuietForMs,
50
50
  commandQuietForMs,
51
51
  workerAgeMs,
52
- interactiveCommands
52
+ interactiveCommands,
53
+ runtimeFreshnessWindowMs
53
54
  };
54
55
  }
@@ -17,7 +17,7 @@ const openCodeSpawnGraceMs = 5_000;
17
17
  exports.openCodeSpawnGraceMs = openCodeSpawnGraceMs;
18
18
  const codexSpawnGraceMs = 5_000;
19
19
  exports.codexSpawnGraceMs = codexSpawnGraceMs;
20
- const genericWorkingFreshWindowMs = 12_000;
20
+ const genericWorkingFreshWindowMs = 20_000;
21
21
  exports.genericWorkingFreshWindowMs = genericWorkingFreshWindowMs;
22
22
  const claudeWorkingFreshWindowMs = 10_000;
23
23
  exports.claudeWorkingFreshWindowMs = claudeWorkingFreshWindowMs;
@@ -64,6 +64,7 @@ function createContext(overrides = {}) {
64
64
  commandQuietForMs: 5_000,
65
65
  workerAgeMs: 30_000,
66
66
  interactiveCommands: new Set(),
67
+ runtimeFreshnessWindowMs: undefined,
67
68
  ...overrides
68
69
  };
69
70
  }
@@ -53,6 +53,9 @@ function looksLikeActiveRuntimeText(activityText) {
53
53
  return normalized.startsWith("thinking");
54
54
  }
55
55
  function statusFreshnessWindowMs(context) {
56
+ if (context.runtimeFreshnessWindowMs !== undefined) {
57
+ return context.runtimeFreshnessWindowMs;
58
+ }
56
59
  if (context.isClaudeSession) {
57
60
  return constants_1.claudeWorkingFreshWindowMs;
58
61
  }
@@ -64,6 +64,7 @@ function createContext(overrides = {}) {
64
64
  commandQuietForMs: 300,
65
65
  workerAgeMs: 10_000,
66
66
  interactiveCommands: new Set(),
67
+ runtimeFreshnessWindowMs: undefined,
67
68
  ...overrides
68
69
  };
69
70
  }
@@ -94,7 +95,9 @@ function createContext(overrides = {}) {
94
95
  (0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isClaudeSession: true }))).toBe(10_000);
95
96
  (0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isOpenCodeSession: true }))).toBe(12_000);
96
97
  (0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isCodexSession: true }))).toBe(10_000);
97
- (0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext())).toBe(12_000);
98
+ (0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext())).toBe(20_000);
99
+ (0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ runtimeFreshnessWindowMs: 45_000 }))).toBe(45_000);
100
+ (0, vitest_1.expect)((0, helpers_1.statusFreshnessWindowMs)(createContext({ isClaudeSession: true, runtimeFreshnessWindowMs: 30_000 }))).toBe(30_000);
98
101
  });
99
102
  (0, vitest_1.it)("handles helper utility behavior", () => {
100
103
  const values = [];
@@ -93,6 +93,21 @@ function collectWorkingEvidence(context, hasRecoverableParserError) {
93
93
  activityToolCandidates.push(context.parsed.activity.tool);
94
94
  (0, helpers_1.pushMaybe)(activityPathCandidates, context.parsed.activity.filePath);
95
95
  }
96
+ if (context.worker.status === "working" &&
97
+ !(0, helpers_1.isShellCommand)(context.commandLower) &&
98
+ !(0, helpers_1.isInteractiveCommand)(context) &&
99
+ context.outputQuietForMs <= (0, helpers_1.statusFreshnessWindowMs)(context) &&
100
+ strongReasons.length === 0 &&
101
+ weakReasons.length === 0) {
102
+ weakReasons.push({
103
+ code: "output-still-fresh",
104
+ message: "Output is still within the freshness window; maintaining working status.",
105
+ detail: `${Math.round(context.outputQuietForMs)}ms quiet (window: ${(0, helpers_1.statusFreshnessWindowMs)(context)}ms)`
106
+ });
107
+ (0, helpers_1.pushMaybe)(activityTextCandidates, context.runtimeActivityText ?? context.worker.activityText);
108
+ activityToolCandidates.push(context.worker.activityTool ?? "terminal");
109
+ (0, helpers_1.pushMaybe)(activityPathCandidates, context.worker.activityPath);
110
+ }
96
111
  return {
97
112
  strongReasons,
98
113
  weakReasons,
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.evaluateWorkerStatus = evaluateWorkerStatus;
4
4
  const signalContext_1 = require("./engine/signalContext");
5
5
  const stateMachine_1 = require("./engine/stateMachine");
6
- function evaluateWorkerStatus({ worker, currentCommand, output, observation, transcriptSnapshot, runtimeProcess, interactiveCommands }) {
6
+ function evaluateWorkerStatus({ worker, currentCommand, output, observation, transcriptSnapshot, runtimeProcess, interactiveCommands, runtimeFreshnessWindowMs }) {
7
7
  const context = (0, signalContext_1.buildWorkerStatusSignalContext)({
8
8
  worker,
9
9
  currentCommand,
@@ -12,7 +12,8 @@ function evaluateWorkerStatus({ worker, currentCommand, output, observation, tra
12
12
  transcriptSnapshot,
13
13
  runtimeProcess,
14
14
  nowMs: Date.now(),
15
- interactiveCommands
15
+ interactiveCommands,
16
+ runtimeFreshnessWindowMs
16
17
  });
17
18
  return (0, stateMachine_1.deriveWorkerStatusDecision)(context);
18
19
  }
@@ -46,6 +46,7 @@ class StatusMonitor {
46
46
  traceMode = resolveStatusTraceMode();
47
47
  workerPollConcurrency = resolveStatusPollConcurrency();
48
48
  interactiveCommands;
49
+ runtimeFreshnessOverrides;
49
50
  constructor(workers, tmux, pollIntervalMs, onWorkerUpdated, onWorkerRemoved, config) {
50
51
  this.workers = workers;
51
52
  this.tmux = tmux;
@@ -53,6 +54,13 @@ class StatusMonitor {
53
54
  this.onWorkerUpdated = onWorkerUpdated;
54
55
  this.onWorkerRemoved = onWorkerRemoved;
55
56
  this.interactiveCommands = new Set(config.status.interactiveCommands.map((cmd) => cmd.toLowerCase()));
57
+ const freshnessOverrides = new Map();
58
+ for (const [id, runtime] of Object.entries(config.runtimes)) {
59
+ if (runtime.freshnessWindowMs !== undefined) {
60
+ freshnessOverrides.set(id, runtime.freshnessWindowMs);
61
+ }
62
+ }
63
+ this.runtimeFreshnessOverrides = freshnessOverrides;
56
64
  }
57
65
  start() {
58
66
  if (this.intervalId) {
@@ -175,7 +183,8 @@ class StatusMonitor {
175
183
  tmux: this.tmux,
176
184
  paneObservation: this.paneObservation,
177
185
  claudeTranscript: this.claudeTranscript,
178
- interactiveCommands: this.interactiveCommands
186
+ interactiveCommands: this.interactiveCommands,
187
+ runtimeFreshnessWindowMs: this.runtimeFreshnessOverrides.get(worker.runtimeId)
179
188
  });
180
189
  if (!signals) {
181
190
  this.removeWorker(worker.id);
@@ -281,7 +290,8 @@ class StatusMonitor {
281
290
  if (this.traceMode !== "verbose") {
282
291
  return;
283
292
  }
284
- console.log(`[arcane-agents][status] poll workers=${timing.workerCount} duration=${Math.round(timing.durationMs)}ms ` +
293
+ const timestamp = new Date().toLocaleTimeString("en-AU", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
294
+ console.log(`[arcane-agents][status] ${timestamp} poll workers=${timing.workerCount} duration=${Math.round(timing.durationMs)}ms ` +
285
295
  `avgWorker=${Math.round(timing.averageWorkerDurationMs)}ms maxWorker=${Math.round(timing.maxWorkerDurationMs)}ms ` +
286
296
  `outcomes={updated:${timing.outcomeCounts.updated},unchanged:${timing.outcomeCounts.unchanged},removed:${timing.outcomeCounts.removed},failed:${timing.outcomeCounts.failed}}`);
287
297
  }
@@ -313,7 +323,8 @@ class StatusMonitor {
313
323
  `opencode=${evaluation.facts.isOpenCodeSession ? 1 : 0} ` +
314
324
  `codex=${evaluation.facts.isCodexSession ? 1 : 0} ` +
315
325
  `runtimeProc=${evaluation.facts.hasActiveRuntimeProcess ? 1 : 0}`;
316
- console.log(`[arcane-agents][status] ${worker.displayName ?? worker.name} ${fromTo} (${Math.round(evaluation.confidence * 100)}%)${activityText} reasons=[${reasonText}] ${traceFacts}`);
326
+ const timestamp = new Date().toLocaleTimeString("en-AU", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
327
+ console.log(`[arcane-agents][status] ${timestamp} ${worker.displayName ?? worker.name} ${fromTo} (${Math.round(evaluation.confidence * 100)}%)${activityText} reasons=[${reasonText}] ${traceFacts}`);
317
328
  }
318
329
  recordStatusTransition(worker, evaluation) {
319
330
  if (evaluation.status === worker.status) {
@@ -8,7 +8,7 @@ vitest_1.vi.mock("./statusPipeline", () => ({
8
8
  evaluateWorkerStatusSignals: vitest_1.vi.fn(),
9
9
  normalizeWorkerStatusEvaluation: vitest_1.vi.fn((evaluation) => evaluation)
10
10
  }));
11
- const testConfig = { status: { interactiveCommands: [] } };
11
+ const testConfig = { status: { interactiveCommands: [] }, runtimes: {} };
12
12
  const defaultFacts = {
13
13
  command: "claude",
14
14
  commandQuietForMs: 0,
@@ -96,7 +96,8 @@ function createSignals() {
96
96
  },
97
97
  transcriptSnapshot: undefined,
98
98
  runtimeProcess: undefined,
99
- interactiveCommands: new Set()
99
+ interactiveCommands: new Set(),
100
+ runtimeFreshnessWindowMs: undefined
100
101
  };
101
102
  }
102
103
  function createEvaluation(status) {
@@ -7,7 +7,7 @@ const runtimeSignals_1 = require("./runtimeSignals");
7
7
  const runtimeProcess_1 = require("./runtime/runtimeProcess");
8
8
  const statusEvaluator_1 = require("./statusEvaluator");
9
9
  const paneObservation_1 = require("./paneObservation");
10
- async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claudeTranscript, interactiveCommands }) {
10
+ async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claudeTranscript, interactiveCommands, runtimeFreshnessWindowMs }) {
11
11
  const paneState = await tmux.getPaneState(worker.tmuxRef);
12
12
  if (paneState.isDead) {
13
13
  return undefined;
@@ -28,7 +28,8 @@ async function collectWorkerStatusSignals({ worker, tmux, paneObservation, claud
28
28
  observation,
29
29
  transcriptSnapshot,
30
30
  runtimeProcess,
31
- interactiveCommands
31
+ interactiveCommands,
32
+ runtimeFreshnessWindowMs
32
33
  };
33
34
  }
34
35
  function evaluateWorkerStatusSignals(worker, signals) {
@@ -39,7 +40,8 @@ function evaluateWorkerStatusSignals(worker, signals) {
39
40
  observation: signals.observation,
40
41
  transcriptSnapshot: signals.transcriptSnapshot,
41
42
  runtimeProcess: signals.runtimeProcess,
42
- interactiveCommands: signals.interactiveCommands
43
+ interactiveCommands: signals.interactiveCommands,
44
+ runtimeFreshnessWindowMs: signals.runtimeFreshnessWindowMs
43
45
  });
44
46
  }
45
47
  function normalizeWorkerStatusEvaluation(evaluation) {