aoaoe 0.187.0 → 0.188.0

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/dist/config.d.ts CHANGED
@@ -25,6 +25,8 @@ export declare function parseCliArgs(argv: string[]): {
25
25
  showTasks: boolean;
26
26
  showTasksJson: boolean;
27
27
  runProgress: boolean;
28
+ runHealth: boolean;
29
+ healthJson: boolean;
28
30
  progressSince?: string;
29
31
  progressJson: boolean;
30
32
  showHistory: boolean;
package/dist/config.js CHANGED
@@ -331,7 +331,7 @@ export function parseCliArgs(argv) {
331
331
  let initForce = false;
332
332
  let runTaskCli = false;
333
333
  let registerTitle;
334
- const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, showHistory: false, showStatus: false, runRunbook: false, runbookJson: false, runbookSection: undefined, runIncident: false, incidentSince: undefined, incidentLimit: undefined, incidentJson: false, incidentNdjson: false, incidentWatch: false, incidentChangesOnly: false, incidentHeartbeatSec: undefined, incidentIntervalMs: undefined, runSupervisor: false, supervisorAll: false, supervisorSince: undefined, supervisorLimit: undefined, supervisorJson: false, supervisorNdjson: false, supervisorWatch: false, supervisorChangesOnly: false, supervisorHeartbeatSec: undefined, supervisorIntervalMs: undefined, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
334
+ const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, runHealth: false, healthJson: false, showHistory: false, showStatus: false, runRunbook: false, runbookJson: false, runbookSection: undefined, runIncident: false, incidentSince: undefined, incidentLimit: undefined, incidentJson: false, incidentNdjson: false, incidentWatch: false, incidentChangesOnly: false, incidentHeartbeatSec: undefined, incidentIntervalMs: undefined, runSupervisor: false, supervisorAll: false, supervisorSince: undefined, supervisorLimit: undefined, supervisorJson: false, supervisorNdjson: false, supervisorWatch: false, supervisorChangesOnly: false, supervisorHeartbeatSec: undefined, supervisorIntervalMs: undefined, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
335
335
  // check for subcommand as first non-flag arg
336
336
  if (argv[2] === "test-context") {
337
337
  return { ...defaults, testContext: true };
@@ -355,6 +355,10 @@ export function parseCliArgs(argv) {
355
355
  }
356
356
  return { ...defaults, runProgress: true, progressSince: since, progressJson: json };
357
357
  }
358
+ if (argv[2] === "health") {
359
+ const json = argv.includes("--json");
360
+ return { ...defaults, runHealth: true, healthJson: json };
361
+ }
358
362
  if (argv[2] === "history") {
359
363
  return { ...defaults, showHistory: true };
360
364
  }
@@ -700,7 +704,7 @@ export function parseCliArgs(argv) {
700
704
  break;
701
705
  }
702
706
  }
703
- return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, showHistory: false, showStatus: false, runRunbook: false, runbookJson: false, runbookSection: undefined, runIncident: false, incidentSince: undefined, incidentLimit: undefined, incidentJson: false, incidentNdjson: false, incidentWatch: false, incidentChangesOnly: false, incidentHeartbeatSec: undefined, incidentIntervalMs: undefined, runSupervisor: false, supervisorAll: false, supervisorSince: undefined, supervisorLimit: undefined, supervisorJson: false, supervisorNdjson: false, supervisorWatch: false, supervisorChangesOnly: false, supervisorHeartbeatSec: undefined, supervisorIntervalMs: undefined, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
707
+ return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, runHealth: false, healthJson: false, showHistory: false, showStatus: false, runRunbook: false, runbookJson: false, runbookSection: undefined, runIncident: false, incidentSince: undefined, incidentLimit: undefined, incidentJson: false, incidentNdjson: false, incidentWatch: false, incidentChangesOnly: false, incidentHeartbeatSec: undefined, incidentIntervalMs: undefined, runSupervisor: false, supervisorAll: false, supervisorSince: undefined, supervisorLimit: undefined, supervisorJson: false, supervisorNdjson: false, supervisorWatch: false, supervisorChangesOnly: false, supervisorHeartbeatSec: undefined, supervisorIntervalMs: undefined, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
704
708
  }
705
709
  export function printHelp() {
706
710
  console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
@@ -768,6 +772,8 @@ commands:
768
772
  progress per-session accomplishment digest (last 24h)
769
773
  progress --since <duration> filter progress window (1h, 8h, 7d)
770
774
  progress --json machine-readable progress output
775
+ health per-session health scores (0-100, fleet average)
776
+ health --json machine-readable health output
771
777
  history review recent actions (from ~/.aoaoe/actions.log)
772
778
  test run integration test (creates sessions, tests, cleans up)
773
779
  test-context scan sessions + context files (read-only, no LLM, safe)
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import { loadTuiHistory, searchHistory, TUI_HISTORY_FILE, computeHistoryStats }
26
26
  import { appendSupervisorEvent, loadSupervisorEvents } from "./supervisor-history.js";
27
27
  import { savePreset, deletePreset, getPreset, formatPresetList } from "./pin-presets.js";
28
28
  import { resolvePromptTemplate, formatPromptTemplateList } from "./reasoner/prompt-templates.js";
29
- import { formatHealthReport } from "./health-score.js";
29
+ import { formatHealthReport, computeAllHealth } from "./health-score.js";
30
30
  import { ConfigWatcher, formatConfigChange } from "./config-watcher.js";
31
31
  import { parseActionLogEntries, parseActivityEntries, mergeTimeline, filterByAge, parseDuration, formatTimelineJson, formatTimelineMarkdown } from "./export.js";
32
32
  import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
@@ -40,7 +40,7 @@ const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
40
40
  const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
41
41
  const TASK_RECONCILE_EVERY_POLLS = 6;
42
42
  async function main() {
43
- const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showTasksJson, runProgress, progressSince, progressJson, showHistory, showStatus, runRunbook, runbookJson, runbookSection, runIncident, incidentSince, incidentLimit, incidentJson, incidentNdjson, incidentWatch, incidentChangesOnly, incidentHeartbeatSec, incidentIntervalMs, runSupervisor, supervisorAll, supervisorSince, supervisorLimit, supervisorJson, supervisorNdjson, supervisorWatch, supervisorChangesOnly, supervisorHeartbeatSec, supervisorIntervalMs, showConfig, configValidate, configDiff, notifyTest, runDoctor, runLogs, logsActions, logsGrep, logsCount, runExport, exportFormat, exportOutput, exportLast, runInit, initForce, runTaskCli: isTaskCli, runTail: isTail, tailFollow, tailCount, runStats: isStats, statsLast, runReplay: isReplay, replaySpeed, replayLast, registerTitle } = parseCliArgs(process.argv);
43
+ const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showTasksJson, runProgress, progressSince, progressJson, runHealth, healthJson, showHistory, showStatus, runRunbook, runbookJson, runbookSection, runIncident, incidentSince, incidentLimit, incidentJson, incidentNdjson, incidentWatch, incidentChangesOnly, incidentHeartbeatSec, incidentIntervalMs, runSupervisor, supervisorAll, supervisorSince, supervisorLimit, supervisorJson, supervisorNdjson, supervisorWatch, supervisorChangesOnly, supervisorHeartbeatSec, supervisorIntervalMs, showConfig, configValidate, configDiff, notifyTest, runDoctor, runLogs, logsActions, logsGrep, logsCount, runExport, exportFormat, exportOutput, exportLast, runInit, initForce, runTaskCli: isTaskCli, runTail: isTail, tailFollow, tailCount, runStats: isStats, statsLast, runReplay: isReplay, replaySpeed, replayLast, registerTitle } = parseCliArgs(process.argv);
44
44
  if (help) {
45
45
  printHelp();
46
46
  process.exit(0);
@@ -80,6 +80,11 @@ async function main() {
80
80
  await showProgressDigest(progressSince, progressJson);
81
81
  return;
82
82
  }
83
+ // `aoaoe health` -- per-session health scores
84
+ if (runHealth) {
85
+ showHealthStatus(healthJson);
86
+ return;
87
+ }
83
88
  // `aoaoe history` -- review recent actions
84
89
  if (showHistory) {
85
90
  await showActionHistory();
@@ -4415,6 +4420,29 @@ async function showProgressDigest(since, asJson = false) {
4415
4420
  }
4416
4421
  console.log(formatProgressDigest(tasks, maxAgeMs));
4417
4422
  }
4423
+ function showHealthStatus(asJson = false) {
4424
+ const basePath = process.cwd();
4425
+ const defs = loadTaskDefinitions(basePath);
4426
+ const taskProfiles = resolveProfiles(loadConfig());
4427
+ const tm = defs.length > 0 ? new TaskManager(basePath, defs, taskProfiles) : undefined;
4428
+ const tasks = tm?.tasks ?? [];
4429
+ if (tasks.length === 0) {
4430
+ if (asJson) {
4431
+ console.log("[]");
4432
+ return;
4433
+ }
4434
+ console.log("no tasks defined.");
4435
+ return;
4436
+ }
4437
+ if (asJson) {
4438
+ const healths = computeAllHealth(tasks);
4439
+ console.log(JSON.stringify(healths, null, 2));
4440
+ return;
4441
+ }
4442
+ console.log("");
4443
+ console.log(formatHealthReport(tasks));
4444
+ console.log("");
4445
+ }
4418
4446
  // `aoaoe history` -- review recent actions from the persistent action log
4419
4447
  async function showActionHistory() {
4420
4448
  const logFile = join(homedir(), ".aoaoe", "actions.log");
@@ -5253,6 +5281,47 @@ async function runDoctorCheck() {
5253
5281
  catch {
5254
5282
  console.log(` ${RED}✗${RESET} could not run 'aoe list --json'`);
5255
5283
  }
5284
+ // ── 7. tasks ────────────────────────────────────────────────────────────
5285
+ console.log(`\n ${BOLD}tasks${RESET}`);
5286
+ checks++;
5287
+ try {
5288
+ const defs = loadTaskDefinitions(process.cwd());
5289
+ const taskProfiles = resolveProfiles(config);
5290
+ if (defs.length === 0) {
5291
+ console.log(` ${DIM}○${RESET} no task definitions (create aoaoe.tasks.json or run 'aoaoe init')`);
5292
+ passed++;
5293
+ }
5294
+ else {
5295
+ const tm = new TaskManager(process.cwd(), defs, taskProfiles);
5296
+ const tasks = tm.tasks;
5297
+ const active = tasks.filter((t) => t.status === "active").length;
5298
+ const pending = tasks.filter((t) => t.status === "pending").length;
5299
+ const paused = tasks.filter((t) => t.status === "paused").length;
5300
+ const completed = tasks.filter((t) => t.status === "completed").length;
5301
+ const stuck = tasks.filter((t) => t.status === "active" && t.lastProgressAt && (Date.now() - t.lastProgressAt > 30 * 60_000)).length;
5302
+ console.log(` ${GREEN}✓${RESET} ${tasks.length} task(s): ${active} active, ${pending} pending, ${paused} paused, ${completed} completed`);
5303
+ if (stuck > 0) {
5304
+ console.log(` ${YELLOW}!${RESET} ${stuck} task(s) possibly stuck (no progress >30min)`);
5305
+ warnings++;
5306
+ }
5307
+ // check for untracked sessions
5308
+ try {
5309
+ const liveStatus = await probeLiveSessionStatus();
5310
+ const trackedTitles = new Set(tasks.map((t) => t.sessionTitle.toLowerCase()));
5311
+ const untracked = [...liveStatus.keys()].filter((t) => !trackedTitles.has(t));
5312
+ if (untracked.length > 0) {
5313
+ console.log(` ${YELLOW}!${RESET} ${untracked.length} untracked session(s): ${untracked.join(", ")}`);
5314
+ console.log(` ${DIM}adopt with: /task <name> :: <goal>${RESET}`);
5315
+ warnings++;
5316
+ }
5317
+ }
5318
+ catch { /* aoe not available */ }
5319
+ passed++;
5320
+ }
5321
+ }
5322
+ catch (err) {
5323
+ console.log(` ${RED}✗${RESET} task check failed: ${err instanceof Error ? err.message : err}`);
5324
+ }
5256
5325
  // ── summary ────────────────────────────────────────────────────────────
5257
5326
  const failed = checks - passed - warnings;
5258
5327
  console.log("");
@@ -140,6 +140,24 @@ export class OpencodeReasoner {
140
140
  if (errMsg.includes("401") || errMsg.includes("Unauthorized")) {
141
141
  this.log("hint: auth token may be expired — run `opencode auth login` to re-authenticate");
142
142
  }
143
+ // auto-restart opencode server on persistent 500s (likely stale state)
144
+ if (errMsg.includes("500") || errMsg.includes("ECONNREFUSED")) {
145
+ this.log("opencode server appears unhealthy — attempting restart");
146
+ this.killOrphanedServer();
147
+ try {
148
+ await this.startServer(this.config.opencode.port);
149
+ for (let i = 0; i < 15; i++) {
150
+ if (await this.tryConnect(this.config.opencode.port)) {
151
+ this.log("opencode server restarted successfully");
152
+ break;
153
+ }
154
+ await sleep(1000);
155
+ }
156
+ }
157
+ catch (restartErr) {
158
+ this.log(`opencode server restart failed: ${restartErr}`);
159
+ }
160
+ }
143
161
  this.sessionId = null;
144
162
  this.messageCount = 0;
145
163
  return { actions: [{ action: "wait", reason: "SDK session error" }] };
@@ -254,8 +272,10 @@ class OpencodeClient {
254
272
  headers: { "Content-Type": "application/json" },
255
273
  body: JSON.stringify({ title }),
256
274
  });
257
- if (!res.ok)
258
- throw new Error(`create session failed: ${res.status}`);
275
+ if (!res.ok) {
276
+ const body = await res.text().catch(() => "(no body)");
277
+ throw new Error(`create session failed: ${res.status} ${res.statusText} — ${body.slice(0, 200)}`);
278
+ }
259
279
  return (await res.json());
260
280
  }
261
281
  async sendMessage(sessionId, text, noReply, signal) {
@@ -268,8 +288,10 @@ class OpencodeClient {
268
288
  }),
269
289
  signal,
270
290
  });
271
- if (!res.ok)
272
- throw new Error(`send message failed: ${res.status}`);
291
+ if (!res.ok) {
292
+ const body = await res.text().catch(() => "(no body)");
293
+ throw new Error(`send message failed: ${res.status} ${res.statusText} — ${body.slice(0, 200)}`);
294
+ }
273
295
  if (noReply)
274
296
  return "";
275
297
  const data = (await res.json());
package/dist/wake.d.ts CHANGED
@@ -5,12 +5,13 @@ export interface WakeResult {
5
5
  }
6
6
  /**
7
7
  * Sleep for up to `ms` milliseconds, but wake early if:
8
- * - a file change is detected in `watchDir` (returns "wake")
8
+ * - a relevant file change is detected in `watchDir` (returns "wake")
9
9
  * - the AbortSignal fires (returns "abort")
10
10
  * - the timeout expires naturally (returns "timeout")
11
11
  *
12
12
  * Uses fs.watch on the directory — fires when pending-input.txt is written
13
- * or the interrupt flag file is created. No polling.
13
+ * or the interrupt flag file is created. Ignores daemon-state.json and other
14
+ * internal files to prevent wake storms.
14
15
  */
15
- export declare function wakeableSleep(ms: number, watchDir: string, signal?: AbortSignal): Promise<WakeResult>;
16
+ export declare function wakeableSleep(ms: number, watchDir: string, signal?: AbortSignal, minWakeIntervalMs?: number): Promise<WakeResult>;
16
17
  //# sourceMappingURL=wake.d.ts.map
package/dist/wake.js CHANGED
@@ -2,19 +2,29 @@
2
2
  // that wakes immediately when files appear in the watch directory.
3
3
  // this drops message latency from up to 10s (full poll interval) to ~100ms.
4
4
  import { watch } from "node:fs";
5
+ // minimum interval between wake events — prevents storm from daemon-state.json writes
6
+ const MIN_WAKE_INTERVAL_MS = 2000;
7
+ // files that should trigger a wake (user input, interrupt flag)
8
+ // daemon-state.json and supervisor-history.jsonl are excluded to prevent wake storms
9
+ const WAKE_FILES = new Set([
10
+ "pending-input.txt",
11
+ "interrupt",
12
+ ]);
5
13
  /**
6
14
  * Sleep for up to `ms` milliseconds, but wake early if:
7
- * - a file change is detected in `watchDir` (returns "wake")
15
+ * - a relevant file change is detected in `watchDir` (returns "wake")
8
16
  * - the AbortSignal fires (returns "abort")
9
17
  * - the timeout expires naturally (returns "timeout")
10
18
  *
11
19
  * Uses fs.watch on the directory — fires when pending-input.txt is written
12
- * or the interrupt flag file is created. No polling.
20
+ * or the interrupt flag file is created. Ignores daemon-state.json and other
21
+ * internal files to prevent wake storms.
13
22
  */
14
- export function wakeableSleep(ms, watchDir, signal) {
23
+ export function wakeableSleep(ms, watchDir, signal, minWakeIntervalMs = MIN_WAKE_INTERVAL_MS) {
15
24
  return new Promise((resolve) => {
16
25
  const start = Date.now();
17
26
  let settled = false;
27
+ let lastWakeAt = 0;
18
28
  const settle = (reason) => {
19
29
  if (settled)
20
30
  return;
@@ -27,7 +37,15 @@ export function wakeableSleep(ms, watchDir, signal) {
27
37
  // fs.watch on directory — fires on any file create/rename/change
28
38
  let watcher = null;
29
39
  try {
30
- watcher = watch(watchDir, { persistent: false }, (_event, _filename) => {
40
+ watcher = watch(watchDir, { persistent: false }, (_event, filename) => {
41
+ // only wake for relevant files (user input, interrupt) — not internal state files
42
+ if (filename && !WAKE_FILES.has(filename))
43
+ return;
44
+ // debounce: don't wake more than once per minWakeIntervalMs
45
+ const now = Date.now();
46
+ if (now - lastWakeAt < minWakeIntervalMs)
47
+ return;
48
+ lastWakeAt = now;
31
49
  settle("wake");
32
50
  });
33
51
  // fs.watch can emit 'error' on broken watchers (e.g. dir deleted)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.187.0",
3
+ "version": "0.188.0",
4
4
  "description": "Autonomous supervisor for agent-of-empires sessions using OpenCode or Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",