aoaoe 0.50.0 → 0.52.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/chat.js CHANGED
@@ -53,8 +53,9 @@ function main() {
53
53
  lastSize = currSize;
54
54
  }
55
55
  }
56
- catch {
56
+ catch (e) {
57
57
  // file may be truncated or removed — reset so we pick up from start of new file
58
+ console.error(`[chat] conversation log read failed: ${e}`);
58
59
  lastSize = 0;
59
60
  }
60
61
  };
@@ -341,7 +342,8 @@ async function captureTmuxPane(tmuxName) {
341
342
  const result = await exec("tmux", ["capture-pane", "-t", tmuxName, "-p", "-S", "-100"], 5_000);
342
343
  return result.exitCode === 0 ? result.stdout : null;
343
344
  }
344
- catch {
345
+ catch (e) {
346
+ console.error(`[chat] tmux capture-pane failed for ${tmuxName}: ${e}`);
345
347
  return null;
346
348
  }
347
349
  }
@@ -437,7 +439,9 @@ function appendToInput(msg) {
437
439
  try {
438
440
  appendFileSync(INPUT_FILE, msg + "\n");
439
441
  }
440
- catch { }
442
+ catch (e) {
443
+ console.error(`[chat] pending-input write failed: ${e}`);
444
+ }
441
445
  }
442
446
  function replayLog() {
443
447
  if (!existsSync(CONVO_LOG))
@@ -449,7 +453,9 @@ function replayLog() {
449
453
  console.log(`${DIM}--- end of history ---${RESET}\n`);
450
454
  }
451
455
  }
452
- catch { }
456
+ catch (e) {
457
+ console.error(`[chat] conversation log replay failed: ${e}`);
458
+ }
453
459
  }
454
460
  export function colorize(text) {
455
461
  // first pass: colorize tick separator lines (──── tick #N ────)
package/dist/config.d.ts CHANGED
@@ -18,6 +18,8 @@ export declare function parseCliArgs(argv: string[]): {
18
18
  runTest: boolean;
19
19
  showTasks: boolean;
20
20
  showHistory: boolean;
21
+ showStatus: boolean;
22
+ showConfig: boolean;
21
23
  runInit: boolean;
22
24
  initForce: boolean;
23
25
  runTaskCli: boolean;
package/dist/config.js CHANGED
@@ -91,6 +91,7 @@ const KNOWN_KEYS = {
91
91
  "maxIdleBeforeNudgeMs", "maxErrorsBeforeRestart", "autoAnswerPermissions",
92
92
  "actionCooldownMs", "userActivityThresholdMs", "allowDestructive",
93
93
  ]),
94
+ notifications: new Set(["webhookUrl", "slackWebhookUrl", "events"]),
94
95
  };
95
96
  export function warnUnknownKeys(raw, source) {
96
97
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
@@ -182,6 +183,34 @@ export function validateConfig(config) {
182
183
  if (config.policies?.allowDestructive !== undefined && typeof config.policies.allowDestructive !== "boolean") {
183
184
  errors.push(`policies.allowDestructive must be a boolean, got ${typeof config.policies.allowDestructive}`);
184
185
  }
186
+ // notifications.webhookUrl must be a string starting with http:// or https://
187
+ if (config.notifications?.webhookUrl !== undefined) {
188
+ const u = config.notifications.webhookUrl;
189
+ if (typeof u !== "string" || (!u.startsWith("http://") && !u.startsWith("https://"))) {
190
+ errors.push(`notifications.webhookUrl must be a URL starting with http:// or https://, got ${JSON.stringify(u)}`);
191
+ }
192
+ }
193
+ // notifications.slackWebhookUrl must be a string starting with http:// or https://
194
+ if (config.notifications?.slackWebhookUrl !== undefined) {
195
+ const u = config.notifications.slackWebhookUrl;
196
+ if (typeof u !== "string" || (!u.startsWith("http://") && !u.startsWith("https://"))) {
197
+ errors.push(`notifications.slackWebhookUrl must be a URL starting with http:// or https://, got ${JSON.stringify(u)}`);
198
+ }
199
+ }
200
+ // notifications.events must be an array of valid NotificationEvent values
201
+ if (config.notifications?.events !== undefined) {
202
+ const VALID_EVENTS = new Set(["session_error", "session_done", "action_executed", "action_failed", "daemon_started", "daemon_stopped"]);
203
+ if (!Array.isArray(config.notifications.events)) {
204
+ errors.push(`notifications.events must be an array, got ${typeof config.notifications.events}`);
205
+ }
206
+ else {
207
+ for (const e of config.notifications.events) {
208
+ if (!VALID_EVENTS.has(e)) {
209
+ errors.push(`notifications.events contains invalid event "${e}"`);
210
+ }
211
+ }
212
+ }
213
+ }
185
214
  if (errors.length > 0) {
186
215
  throw new Error(`invalid config:\n ${errors.join("\n ")}`);
187
216
  }
@@ -245,7 +274,7 @@ export function parseCliArgs(argv) {
245
274
  let initForce = false;
246
275
  let runTaskCli = false;
247
276
  let registerTitle;
248
- const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, runInit: false, initForce: false, runTaskCli: false };
277
+ const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, runInit: false, initForce: false, runTaskCli: false };
249
278
  // check for subcommand as first non-flag arg
250
279
  if (argv[2] === "test-context") {
251
280
  return { ...defaults, testContext: true };
@@ -262,6 +291,12 @@ export function parseCliArgs(argv) {
262
291
  if (argv[2] === "history") {
263
292
  return { ...defaults, showHistory: true };
264
293
  }
294
+ if (argv[2] === "status") {
295
+ return { ...defaults, showStatus: true };
296
+ }
297
+ if (argv[2] === "config") {
298
+ return { ...defaults, showConfig: true };
299
+ }
265
300
  if (argv[2] === "init") {
266
301
  const force = argv.includes("--force") || argv.includes("-f");
267
302
  return { ...defaults, runInit: true, initForce: force };
@@ -351,7 +386,7 @@ export function parseCliArgs(argv) {
351
386
  break;
352
387
  }
353
388
  }
354
- return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, runInit: false, initForce: false, runTaskCli: false };
389
+ return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, runInit: false, initForce: false, runTaskCli: false };
355
390
  }
356
391
  export function printHelp() {
357
392
  console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
@@ -367,6 +402,8 @@ getting started:
367
402
  commands:
368
403
  init detect tools + sessions, import history, generate config
369
404
  (none) start the supervisor daemon (interactive TUI)
405
+ status quick daemon health check (is it running? what's it doing?)
406
+ config show the effective resolved config (defaults + file)
370
407
  task manage tasks and sessions (list, start, stop, new, rm, edit)
371
408
  tasks show task progress (from aoaoe.tasks.json)
372
409
  history review recent actions (from ~/.aoaoe/actions.log)
package/dist/console.js CHANGED
@@ -91,7 +91,8 @@ export class ReasonerConsole {
91
91
  const st = statSync(INPUT_FILE);
92
92
  return st.size > 0;
93
93
  }
94
- catch {
94
+ catch (e) {
95
+ console.error(`[console] pending-input size check failed: ${e}`);
95
96
  return false;
96
97
  }
97
98
  }
@@ -133,7 +134,9 @@ export class ReasonerConsole {
133
134
  try {
134
135
  appendFileSync(CONVO_LOG, line + "\n");
135
136
  }
136
- catch { }
137
+ catch (e) {
138
+ console.error(`[console] conversation log write failed: ${e}`);
139
+ }
137
140
  // in inline mode, also print colorized output to stderr
138
141
  if (this.inlineMode) {
139
142
  process.stderr.write(colorizeConsoleLine(line) + "\n");
package/dist/context.js CHANGED
@@ -78,7 +78,8 @@ export function readContextFile(filePath) {
78
78
  evictCache();
79
79
  return content;
80
80
  }
81
- catch {
81
+ catch (e) {
82
+ console.error(`[context] context file read failed for ${filePath}: ${e}`);
82
83
  return "";
83
84
  }
84
85
  }
@@ -109,8 +110,9 @@ export function discoverContextFiles(dir) {
109
110
  seenInodes.add(inodeKey);
110
111
  }
111
112
  }
112
- catch {
113
+ catch (e) {
113
114
  // stat failed — still add by path to avoid silently dropping
115
+ console.error(`[context] inode de-dup stat failed for ${resolved}: ${e}`);
114
116
  }
115
117
  seenPaths.add(resolved);
116
118
  found.push(filePath);
package/dist/executor.js CHANGED
@@ -164,7 +164,8 @@ export class Executor {
164
164
  return this.logAction({ action: "create_agent", path, title, tool }, false, `path is not a directory: ${path}`);
165
165
  }
166
166
  }
167
- catch {
167
+ catch (e) {
168
+ console.error(`[executor] statSync failed for create_agent path ${path}: ${e}`);
168
169
  return this.logAction({ action: "create_agent", path, title, tool }, false, `cannot stat path: ${path}`);
169
170
  }
170
171
  // validate tool name
@@ -281,7 +282,9 @@ export class Executor {
281
282
  this.rotateLogIfNeeded();
282
283
  appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n");
283
284
  }
284
- catch { } // best-effort, don't crash the daemon
285
+ catch (e) {
286
+ console.error(`[executor] action log write failed: ${e}`);
287
+ } // best-effort, don't crash the daemon
285
288
  }
286
289
  return entry;
287
290
  }
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { loadConfig, validateEnvironment, parseCliArgs, printHelp, configFileExists } from "./config.js";
2
+ import { loadConfig, validateEnvironment, parseCliArgs, printHelp, configFileExists, findConfigFile } from "./config.js";
3
3
  import { Poller, computeTmuxName } from "./poller.js";
4
4
  import { createReasoner } from "./reasoner/index.js";
5
5
  import { Executor } from "./executor.js";
6
6
  import { printDashboard } from "./dashboard.js";
7
7
  import { InputReader } from "./input.js";
8
8
  import { ReasonerConsole } from "./console.js";
9
- import { writeState, buildSessionStates, checkInterrupt, clearInterrupt, cleanupState, acquireLock } from "./daemon-state.js";
9
+ import { writeState, buildSessionStates, checkInterrupt, clearInterrupt, cleanupState, acquireLock, readState } from "./daemon-state.js";
10
10
  import { formatSessionSummaries, formatActionDetail, formatPlainEnglishAction, narrateObservation, summarizeRecentActions, friendlyError } from "./console.js";
11
11
  import { loadGlobalContext, resolveProjectDirWithSource, discoverContextFiles, loadSessionContext } from "./context.js";
12
12
  import { tick as loopTick } from "./loop.js";
@@ -16,6 +16,8 @@ import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, h
16
16
  import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable } from "./task-manager.js";
17
17
  import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
18
18
  import { TUI } from "./tui.js";
19
+ import { isDaemonRunningFromState } from "./chat.js";
20
+ import { sendNotification } from "./notify.js";
19
21
  import { actionSession, actionDetail } from "./types.js";
20
22
  import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
21
23
  import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
@@ -26,7 +28,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
26
28
  const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
27
29
  const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
28
30
  async function main() {
29
- const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
31
+ const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
30
32
  if (help) {
31
33
  printHelp();
32
34
  process.exit(0);
@@ -66,6 +68,16 @@ async function main() {
66
68
  await showActionHistory();
67
69
  return;
68
70
  }
71
+ // `aoaoe status` -- quick one-shot daemon health check
72
+ if (showStatus) {
73
+ showDaemonStatus();
74
+ return;
75
+ }
76
+ // `aoaoe config` -- show effective resolved config
77
+ if (showConfig) {
78
+ showEffectiveConfig();
79
+ return;
80
+ }
69
81
  // `aoaoe task` -- task management CLI
70
82
  if (isTaskCli) {
71
83
  await runTaskCli(process.argv);
@@ -273,6 +285,8 @@ async function main() {
273
285
  console.error(` mode: dry-run (no execution)`);
274
286
  console.error("");
275
287
  log("shutting down...");
288
+ // notify: daemon stopped (fire-and-forget, don't block shutdown)
289
+ sendNotification(config, { event: "daemon_stopped", timestamp: Date.now(), detail: `polls: ${totalPolls}, actions: ${totalActionsExecuted}` });
276
290
  input.stop();
277
291
  Promise.resolve()
278
292
  .then(() => reasonerConsole.stop())
@@ -296,6 +310,8 @@ async function main() {
296
310
  else {
297
311
  log("entering main loop (Ctrl+C to stop)\n");
298
312
  }
313
+ // notify: daemon started
314
+ sendNotification(config, { event: "daemon_started", timestamp: Date.now(), detail: `reasoner: ${config.reasoner}` });
299
315
  // clear any stale interrupt from a previous run
300
316
  clearInterrupt();
301
317
  // auto-explain: on the very first tick with sessions, inject an explain prompt
@@ -669,6 +685,19 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
669
685
  }
670
686
  }
671
687
  }
688
+ // notify: session error/done events (fires for both TUI and non-TUI modes)
689
+ {
690
+ const changedSet = new Set(observation.changes.map((c) => c.title));
691
+ for (const snap of observation.sessions) {
692
+ const s = snap.session;
693
+ if (s.status === "error" && changedSet.has(s.title)) {
694
+ sendNotification(config, { event: "session_error", timestamp: Date.now(), session: s.title, detail: `status: ${s.status}` });
695
+ }
696
+ if (s.status === "done" && changedSet.has(s.title)) {
697
+ sendNotification(config, { event: "session_done", timestamp: Date.now(), session: s.title });
698
+ }
699
+ }
700
+ }
672
701
  if (skippedReason === "no changes") {
673
702
  if (config.verbose) {
674
703
  if (tui)
@@ -739,6 +768,13 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
739
768
  log(`[${icon}] ${displayText}`);
740
769
  }
741
770
  reasonerConsole.writeAction(entry.action.action, richDetail, entry.success);
771
+ // notify: action executed or failed
772
+ sendNotification(config, {
773
+ event: entry.success ? "action_executed" : "action_failed",
774
+ timestamp: Date.now(),
775
+ session: sessionTitle,
776
+ detail: `${entry.action.action}${actionText ? `: ${actionText.slice(0, 200)}` : ""}`,
777
+ });
742
778
  }
743
779
  const actionsOk = executed.filter((e) => e.success && e.action.action !== "wait").length;
744
780
  const actionsFail = executed.filter((e) => !e.success && e.action.action !== "wait").length;
@@ -1101,6 +1137,80 @@ async function runIntegrationTest() {
1101
1137
  // the integration test is a self-contained script that runs main() on import
1102
1138
  await import(testModule);
1103
1139
  }
1140
+ // `aoaoe status` -- quick one-shot health check: is the daemon running? what's it doing?
1141
+ function showDaemonStatus() {
1142
+ const state = readState();
1143
+ const running = isDaemonRunningFromState(state);
1144
+ const pkg = readPkgVersion();
1145
+ console.log("");
1146
+ console.log(` aoaoe${pkg ? ` v${pkg}` : ""} — daemon status`);
1147
+ console.log(` ${"─".repeat(50)}`);
1148
+ if (!running || !state) {
1149
+ console.log(` ${RED}●${RESET} daemon is ${BOLD}not running${RESET}`);
1150
+ const configPath = findConfigFile();
1151
+ console.log(` config: ${configPath ?? "none found (run 'aoaoe init')"}`);
1152
+ console.log("");
1153
+ console.log(" start with: aoaoe");
1154
+ console.log(" or observe: aoaoe --observe");
1155
+ console.log("");
1156
+ return;
1157
+ }
1158
+ // daemon is running — show details
1159
+ const elapsed = Date.now() - state.phaseStartedAt;
1160
+ const elapsedStr = elapsed < 60_000 ? `${Math.floor(elapsed / 1000)}s` : `${Math.floor(elapsed / 60_000)}m`;
1161
+ const phaseIcon = state.phase === "sleeping" ? `${DIM}○${RESET}` :
1162
+ state.phase === "reasoning" ? `${YELLOW}●${RESET}` :
1163
+ state.phase === "executing" ? `${GREEN}●${RESET}` :
1164
+ state.phase === "polling" ? `${YELLOW}○${RESET}` :
1165
+ `${RED}●${RESET}`;
1166
+ console.log(` ${GREEN}●${RESET} daemon is ${BOLD}running${RESET} (poll #${state.pollCount})`);
1167
+ console.log(` ${phaseIcon} phase: ${state.phase} (${elapsedStr})`);
1168
+ if (state.paused)
1169
+ console.log(` ${YELLOW}${BOLD} PAUSED${RESET}`);
1170
+ console.log(` poll interval: ${state.pollIntervalMs / 1000}s`);
1171
+ if (state.nextTickAt > Date.now()) {
1172
+ const countdown = Math.ceil((state.nextTickAt - Date.now()) / 1000);
1173
+ console.log(` next tick: ${countdown}s`);
1174
+ }
1175
+ console.log("");
1176
+ // sessions
1177
+ if (state.sessions.length === 0) {
1178
+ console.log(" no active sessions");
1179
+ }
1180
+ else {
1181
+ console.log(` ${state.sessions.length} session(s):`);
1182
+ for (const s of state.sessions) {
1183
+ const statusIcon = s.status === "working" || s.status === "running" ? `${GREEN}●${RESET}` :
1184
+ s.status === "idle" ? `${DIM}○${RESET}` :
1185
+ s.status === "error" ? `${RED}●${RESET}` :
1186
+ s.status === "done" ? `${GREEN}✓${RESET}` :
1187
+ `${DIM}?${RESET}`;
1188
+ const userTag = s.userActive ? ` ${DIM}(user active)${RESET}` : "";
1189
+ const taskTag = s.currentTask ? ` — ${DIM}${s.currentTask.slice(0, 50)}${RESET}` : "";
1190
+ console.log(` ${statusIcon} ${BOLD}${s.title}${RESET} (${s.tool}) ${s.status}${userTag}${taskTag}`);
1191
+ }
1192
+ }
1193
+ console.log("");
1194
+ }
1195
+ // `aoaoe config` -- show the effective resolved config (defaults + file + any notes)
1196
+ function showEffectiveConfig() {
1197
+ const configPath = findConfigFile();
1198
+ const configResult = loadConfig();
1199
+ // strip _configPath from output
1200
+ const { _configPath, ...config } = configResult;
1201
+ console.log("");
1202
+ console.log(" aoaoe — effective config");
1203
+ console.log(` ${"─".repeat(50)}`);
1204
+ console.log(` source: ${configPath ?? "defaults (no config file found)"}`);
1205
+ console.log("");
1206
+ console.log(JSON.stringify(config, null, 2));
1207
+ console.log("");
1208
+ // helpful notes
1209
+ if (!configPath) {
1210
+ console.log(` ${DIM}create a config: aoaoe init${RESET}`);
1211
+ console.log("");
1212
+ }
1213
+ }
1104
1214
  main().catch((err) => {
1105
1215
  console.error(`fatal: ${err}`);
1106
1216
  process.exit(1);
package/dist/init.js CHANGED
@@ -60,7 +60,8 @@ async function discoverSessions() {
60
60
  .filter((r) => r.status === "fulfilled")
61
61
  .map((r) => r.value);
62
62
  }
63
- catch {
63
+ catch (e) {
64
+ console.error(`[init] failed to parse session list: ${e}`);
64
65
  return [];
65
66
  }
66
67
  }
@@ -72,7 +73,8 @@ async function getSessionStatus(id) {
72
73
  const data = JSON.parse(result.stdout);
73
74
  return toSessionStatus(data.status);
74
75
  }
75
- catch {
76
+ catch (e) {
77
+ console.error(`[init] failed to parse session status for ${id}: ${e}`);
76
78
  return "unknown";
77
79
  }
78
80
  }
@@ -0,0 +1,13 @@
1
+ import type { AoaoeConfig, NotificationEvent } from "./types.js";
2
+ export interface NotificationPayload {
3
+ event: NotificationEvent;
4
+ timestamp: number;
5
+ session?: string;
6
+ detail?: string;
7
+ }
8
+ export declare function sendNotification(config: AoaoeConfig, payload: NotificationPayload): Promise<void>;
9
+ export declare function formatSlackPayload(payload: NotificationPayload): {
10
+ text: string;
11
+ blocks: object[];
12
+ };
13
+ //# sourceMappingURL=notify.d.ts.map
package/dist/notify.js ADDED
@@ -0,0 +1,104 @@
1
+ // send a notification to all configured webhooks.
2
+ // fire-and-forget: never throws, never blocks the daemon.
3
+ export async function sendNotification(config, payload) {
4
+ const n = config.notifications;
5
+ if (!n)
6
+ return;
7
+ // filter: only send if event is in the configured list (or no filter = send all)
8
+ if (n.events && n.events.length > 0 && !n.events.includes(payload.event))
9
+ return;
10
+ const promises = [];
11
+ if (n.webhookUrl) {
12
+ promises.push(sendGenericWebhook(n.webhookUrl, payload));
13
+ }
14
+ if (n.slackWebhookUrl) {
15
+ promises.push(sendSlackWebhook(n.slackWebhookUrl, payload));
16
+ }
17
+ // fire-and-forget — swallow all errors so the daemon never crashes on notification failure
18
+ await Promise.allSettled(promises);
19
+ }
20
+ // POST JSON payload to a generic webhook URL
21
+ async function sendGenericWebhook(url, payload) {
22
+ try {
23
+ await fetch(url, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify({
27
+ event: payload.event,
28
+ timestamp: payload.timestamp,
29
+ session: payload.session,
30
+ detail: payload.detail,
31
+ }),
32
+ signal: AbortSignal.timeout(5000),
33
+ });
34
+ }
35
+ catch (err) {
36
+ console.error(`[notify] generic webhook failed: ${err}`);
37
+ }
38
+ }
39
+ // POST Slack block format to a Slack incoming webhook URL
40
+ async function sendSlackWebhook(url, payload) {
41
+ try {
42
+ const body = formatSlackPayload(payload);
43
+ await fetch(url, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify(body),
47
+ signal: AbortSignal.timeout(5000),
48
+ });
49
+ }
50
+ catch (err) {
51
+ console.error(`[notify] slack webhook failed: ${err}`);
52
+ }
53
+ }
54
+ // format a notification payload into Slack block kit format.
55
+ // exported for testing.
56
+ export function formatSlackPayload(payload) {
57
+ const icon = eventIcon(payload.event);
58
+ const title = eventTitle(payload.event);
59
+ const fallbackText = payload.session
60
+ ? `${icon} ${title}: ${payload.session}${payload.detail ? ` — ${payload.detail}` : ""}`
61
+ : `${icon} ${title}${payload.detail ? ` — ${payload.detail}` : ""}`;
62
+ const blocks = [
63
+ {
64
+ type: "section",
65
+ text: {
66
+ type: "mrkdwn",
67
+ text: `*${icon} ${title}*${payload.session ? `\n*Session:* ${payload.session}` : ""}${payload.detail ? `\n${payload.detail}` : ""}`,
68
+ },
69
+ },
70
+ {
71
+ type: "context",
72
+ elements: [
73
+ {
74
+ type: "mrkdwn",
75
+ text: `aoaoe | ${new Date(payload.timestamp).toISOString()}`,
76
+ },
77
+ ],
78
+ },
79
+ ];
80
+ return { text: fallbackText, blocks };
81
+ }
82
+ // human-readable title for each event type
83
+ function eventTitle(event) {
84
+ switch (event) {
85
+ case "session_error": return "Session Error";
86
+ case "session_done": return "Session Done";
87
+ case "action_executed": return "Action Executed";
88
+ case "action_failed": return "Action Failed";
89
+ case "daemon_started": return "Daemon Started";
90
+ case "daemon_stopped": return "Daemon Stopped";
91
+ }
92
+ }
93
+ // emoji icon for each event type (used in Slack messages)
94
+ function eventIcon(event) {
95
+ switch (event) {
96
+ case "session_error": return "\u{1F6A8}"; // 🚨
97
+ case "session_done": return "\u2705"; // ✅
98
+ case "action_executed": return "\u2699\uFE0F"; // ⚙️
99
+ case "action_failed": return "\u274C"; // ❌
100
+ case "daemon_started": return "\u{1F680}"; // 🚀
101
+ case "daemon_stopped": return "\u{1F6D1}"; // 🛑
102
+ }
103
+ }
104
+ //# sourceMappingURL=notify.js.map
package/dist/poller.js CHANGED
@@ -115,7 +115,8 @@ export class Poller {
115
115
  const data = JSON.parse(result.stdout);
116
116
  return toSessionStatus(data.status);
117
117
  }
118
- catch {
118
+ catch (e) {
119
+ console.error(`[poller] failed to parse session status for ${id}: ${e}`);
119
120
  return "unknown";
120
121
  }
121
122
  }
@@ -232,7 +233,8 @@ export async function listAoeSessionsShared(timeoutMs = 10_000) {
232
233
  const parsed = JSON.parse(result.stdout);
233
234
  raw = Array.isArray(parsed) ? parsed : [];
234
235
  }
235
- catch {
236
+ catch (e) {
237
+ console.error(`[poller] failed to parse session list: ${e}`);
236
238
  return [];
237
239
  }
238
240
  const results = await Promise.allSettled(raw.map(async (s) => {
@@ -245,7 +247,9 @@ export async function listAoeSessionsShared(timeoutMs = 10_000) {
245
247
  status = JSON.parse(showResult.stdout).status ?? "unknown";
246
248
  }
247
249
  }
248
- catch { }
250
+ catch (e) {
251
+ console.error(`[poller] failed to parse session show for ${id}: ${e}`);
252
+ }
249
253
  return { id, title, tool: s.tool ?? "", status, tmuxName: computeTmuxName(id, title) };
250
254
  }));
251
255
  return results
package/dist/types.d.ts CHANGED
@@ -115,7 +115,13 @@ export interface AoaoeConfig {
115
115
  dryRun: boolean;
116
116
  observe: boolean;
117
117
  confirm: boolean;
118
+ notifications?: {
119
+ webhookUrl?: string;
120
+ slackWebhookUrl?: string;
121
+ events?: NotificationEvent[];
122
+ };
118
123
  }
124
+ export type NotificationEvent = "session_error" | "session_done" | "action_executed" | "action_failed" | "daemon_started" | "daemon_stopped";
119
125
  export type DaemonPhase = "sleeping" | "polling" | "reasoning" | "executing" | "interrupted";
120
126
  export interface DaemonSessionState {
121
127
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.50.0",
3
+ "version": "0.52.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",