aoaoe 0.190.0 → 0.192.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/README.md CHANGED
@@ -657,6 +657,7 @@ Config lives at `~/.aoaoe/aoaoe.config.json` (canonical, written by `aoaoe init`
657
657
  | `policies.autoAnswerPermissions` | Auto-approve permission prompts | `true` |
658
658
  | `policies.allowDestructive` | Allow `remove_agent` and `stop_session` actions | `false` |
659
659
  | `policies.maxStuckNudgesBeforePause` | Auto-pause task after N nudges with no progress (0 = disabled) | `0` |
660
+ | `policies.quietHours` | Skip reasoning during these hours, e.g. `"01:00-06:00"` (polling continues) | (none) |
660
661
  | `promptTemplate` | Reasoner prompt strategy: `default`, `hands-off`, `aggressive`, `review-focused`, `shipping` | `"default"` |
661
662
  | `policies.userActivityThresholdMs` | Ignore sessions with recent human keystrokes | `30000` |
662
663
  | `policies.actionCooldownMs` | Minimum ms between actions on the same session | `30000` |
@@ -0,0 +1,11 @@
1
+ export declare function createBackup(outputPath?: string): Promise<string>;
2
+ export declare function restoreBackup(inputPath: string): Promise<{
3
+ restored: string[];
4
+ skipped: string[];
5
+ }>;
6
+ export declare function formatBackupResult(path: string): string;
7
+ export declare function formatRestoreResult(result: {
8
+ restored: string[];
9
+ skipped: string[];
10
+ }): string;
11
+ //# sourceMappingURL=backup.d.ts.map
package/dist/backup.js ADDED
@@ -0,0 +1,121 @@
1
+ // backup.ts — backup and restore aoaoe state + config for portability.
2
+ // backs up ~/.aoaoe/ contents to a timestamped tarball or directory.
3
+ import { existsSync, mkdirSync, readdirSync, copyFileSync, statSync } from "node:fs";
4
+ import { join, basename } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { exec } from "./shell.js";
7
+ import { DIM, GREEN, YELLOW, RESET } from "./colors.js";
8
+ const AOAOE_DIR = join(homedir(), ".aoaoe");
9
+ // files to include in backup (relative to ~/.aoaoe/)
10
+ const BACKUP_FILES = [
11
+ "aoaoe.config.json",
12
+ "task-state.json",
13
+ "supervisor-history.jsonl",
14
+ "pin-presets.json",
15
+ "templates.json",
16
+ "prompt-templates.json",
17
+ "tui-prefs.json",
18
+ ];
19
+ export async function createBackup(outputPath) {
20
+ if (!existsSync(AOAOE_DIR)) {
21
+ throw new Error("~/.aoaoe/ does not exist — nothing to back up");
22
+ }
23
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
24
+ const defaultPath = join(process.cwd(), `aoaoe-backup-${timestamp}`);
25
+ const target = outputPath || defaultPath;
26
+ // also include aoaoe.tasks.json from cwd if it exists
27
+ const cwdTasksFile = join(process.cwd(), "aoaoe.tasks.json");
28
+ // try tar first (most portable)
29
+ const tarTarget = target.endsWith(".tar.gz") ? target : `${target}.tar.gz`;
30
+ const filesToBackup = [];
31
+ for (const f of BACKUP_FILES) {
32
+ const full = join(AOAOE_DIR, f);
33
+ if (existsSync(full))
34
+ filesToBackup.push(full);
35
+ }
36
+ if (existsSync(cwdTasksFile))
37
+ filesToBackup.push(cwdTasksFile);
38
+ if (filesToBackup.length === 0) {
39
+ throw new Error("no files to back up");
40
+ }
41
+ const result = await exec("tar", [
42
+ "czf", tarTarget,
43
+ ...filesToBackup.map((f) => f),
44
+ ]);
45
+ if (result.exitCode === 0) {
46
+ const size = statSync(tarTarget).size;
47
+ const sizeStr = size < 1024 ? `${size}B` : `${(size / 1024).toFixed(1)}KB`;
48
+ return `${tarTarget} (${filesToBackup.length} files, ${sizeStr})`;
49
+ }
50
+ // tar failed — fall back to directory copy
51
+ mkdirSync(target, { recursive: true });
52
+ let count = 0;
53
+ for (const full of filesToBackup) {
54
+ const name = basename(full);
55
+ copyFileSync(full, join(target, name));
56
+ count++;
57
+ }
58
+ return `${target}/ (${count} files copied)`;
59
+ }
60
+ export async function restoreBackup(inputPath) {
61
+ const restored = [];
62
+ const skipped = [];
63
+ if (!existsSync(inputPath)) {
64
+ throw new Error(`backup not found: ${inputPath}`);
65
+ }
66
+ if (!existsSync(AOAOE_DIR)) {
67
+ mkdirSync(AOAOE_DIR, { recursive: true });
68
+ }
69
+ // try tar extraction
70
+ if (inputPath.endsWith(".tar.gz") || inputPath.endsWith(".tgz")) {
71
+ const result = await exec("tar", ["xzf", inputPath, "-C", "/"]);
72
+ if (result.exitCode === 0) {
73
+ // check what was restored
74
+ for (const f of BACKUP_FILES) {
75
+ if (existsSync(join(AOAOE_DIR, f)))
76
+ restored.push(f);
77
+ }
78
+ return { restored, skipped };
79
+ }
80
+ throw new Error(`tar extraction failed: ${result.stderr}`);
81
+ }
82
+ // directory restore
83
+ if (!statSync(inputPath).isDirectory()) {
84
+ throw new Error(`${inputPath} is not a directory or tar.gz archive`);
85
+ }
86
+ const files = readdirSync(inputPath);
87
+ for (const f of files) {
88
+ const src = join(inputPath, f);
89
+ if (f === "aoaoe.tasks.json") {
90
+ // restore to cwd
91
+ copyFileSync(src, join(process.cwd(), f));
92
+ restored.push(f);
93
+ }
94
+ else if (BACKUP_FILES.includes(f)) {
95
+ copyFileSync(src, join(AOAOE_DIR, f));
96
+ restored.push(f);
97
+ }
98
+ else {
99
+ skipped.push(f);
100
+ }
101
+ }
102
+ return { restored, skipped };
103
+ }
104
+ export function formatBackupResult(path) {
105
+ return ` ${GREEN}✓${RESET} backup created: ${path}`;
106
+ }
107
+ export function formatRestoreResult(result) {
108
+ const lines = [];
109
+ if (result.restored.length > 0) {
110
+ lines.push(` ${GREEN}✓${RESET} restored ${result.restored.length} file(s):`);
111
+ for (const f of result.restored)
112
+ lines.push(` ${DIM}${f}${RESET}`);
113
+ }
114
+ if (result.skipped.length > 0) {
115
+ lines.push(` ${YELLOW}!${RESET} skipped ${result.skipped.length} file(s):`);
116
+ for (const f of result.skipped)
117
+ lines.push(` ${DIM}${f}${RESET}`);
118
+ }
119
+ return lines.join("\n");
120
+ }
121
+ //# sourceMappingURL=backup.js.map
package/dist/config.d.ts CHANGED
@@ -61,6 +61,10 @@ export declare function parseCliArgs(argv: string[]): {
61
61
  configDiff: boolean;
62
62
  notifyTest: boolean;
63
63
  runDoctor: boolean;
64
+ runBackup: boolean;
65
+ backupOutput?: string;
66
+ runRestore: boolean;
67
+ restoreInput?: string;
64
68
  runLogs: boolean;
65
69
  logsActions: boolean;
66
70
  logsGrep?: string;
@@ -69,6 +73,7 @@ export declare function parseCliArgs(argv: string[]): {
69
73
  exportFormat?: string;
70
74
  exportOutput?: string;
71
75
  exportLast?: string;
76
+ exportTasks: boolean;
72
77
  runInit: boolean;
73
78
  initForce: boolean;
74
79
  runTaskCli: boolean;
package/dist/config.js CHANGED
@@ -333,7 +333,7 @@ export function parseCliArgs(argv) {
333
333
  let initForce = false;
334
334
  let runTaskCli = false;
335
335
  let registerTitle;
336
- 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, runSummary: false, runAdopt: false, adoptTemplate: undefined, 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 };
336
+ 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, runSummary: false, runAdopt: false, adoptTemplate: undefined, 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, runBackup: false, backupOutput: undefined, runRestore: false, restoreInput: undefined, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, exportTasks: false, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
337
337
  // check for subcommand as first non-flag arg
338
338
  if (argv[2] === "test-context") {
339
339
  return { ...defaults, testContext: true };
@@ -531,6 +531,14 @@ export function parseCliArgs(argv) {
531
531
  if (argv[2] === "doctor") {
532
532
  return { ...defaults, runDoctor: true };
533
533
  }
534
+ if (argv[2] === "backup") {
535
+ const output = argv[3] && !argv[3].startsWith("-") ? argv[3] : undefined;
536
+ return { ...defaults, runBackup: true, backupOutput: output };
537
+ }
538
+ if (argv[2] === "restore") {
539
+ const input = argv[3] && !argv[3].startsWith("-") ? argv[3] : undefined;
540
+ return { ...defaults, runRestore: true, restoreInput: input };
541
+ }
534
542
  if (argv[2] === "logs") {
535
543
  const actions = argv.includes("--actions") || argv.includes("-a");
536
544
  let grep;
@@ -551,7 +559,10 @@ export function parseCliArgs(argv) {
551
559
  let format;
552
560
  let output;
553
561
  let last;
562
+ const exportTasks = argv.includes("--tasks");
554
563
  for (let i = 3; i < argv.length; i++) {
564
+ if (argv[i] === "--tasks")
565
+ continue;
555
566
  if ((argv[i] === "--format" || argv[i] === "-f") && argv[i + 1]) {
556
567
  format = argv[++i];
557
568
  }
@@ -562,7 +573,7 @@ export function parseCliArgs(argv) {
562
573
  last = argv[++i];
563
574
  }
564
575
  }
565
- return { ...defaults, runExport: true, exportFormat: format, exportOutput: output, exportLast: last };
576
+ return { ...defaults, runExport: true, exportFormat: format, exportOutput: output, exportLast: last, exportTasks };
566
577
  }
567
578
  if (argv[2] === "init") {
568
579
  const force = argv.includes("--force") || argv.includes("-f");
@@ -717,7 +728,7 @@ export function parseCliArgs(argv) {
717
728
  break;
718
729
  }
719
730
  }
720
- return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, runHealth: false, healthJson: false, runSummary: false, runAdopt: false, adoptTemplate: undefined, 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 };
731
+ return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showTasksJson: false, runProgress: false, progressSince: undefined, progressJson: false, runHealth: false, healthJson: false, runSummary: false, runAdopt: false, adoptTemplate: undefined, 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, runBackup: false, backupOutput: undefined, runRestore: false, restoreInput: undefined, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, exportTasks: false, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
721
732
  }
722
733
  export function printHelp() {
723
734
  console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
@@ -761,6 +772,8 @@ commands:
761
772
  config --validate validate config + check tool availability
762
773
  config --diff show only fields that differ from defaults
763
774
  notify-test send a test notification to configured webhooks
775
+ backup [path] backup ~/.aoaoe/ state + config to tarball
776
+ restore <path> restore from backup tarball or directory
764
777
  doctor comprehensive health check (config, tools, daemon, disk)
765
778
  logs show recent conversation log entries (last 50)
766
779
  logs --actions show action log entries (from ~/.aoaoe/actions.log)
@@ -770,6 +783,8 @@ commands:
770
783
  export --format <json|markdown> output format (default: json)
771
784
  export --output <file> write to file (default: stdout)
772
785
  export --last <duration> time window: 1h, 6h, 24h, 7d (default: 24h)
786
+ export --tasks export task history instead of timeline
787
+ export --tasks --format md export task history as markdown
773
788
  stats show aggregate daemon statistics (actions, sessions, activity)
774
789
  stats --last <duration> time window: 1h, 6h, 24h, 7d (default: all time)
775
790
  replay play back tui-history.jsonl like a movie with simulated timing
package/dist/executor.js CHANGED
@@ -213,8 +213,11 @@ export class Executor {
213
213
  }
214
214
  const snap = this.resolveSession(sessionId, snapshots);
215
215
  const title = snap?.session.title ?? sessionId;
216
- await this.taskManager.completeTask(title, summary);
217
- return this.logAction({ action: "complete_task", session: sessionId, summary }, true, `task completed for ${title}: ${summary}`);
216
+ const unblockedTitles = await this.taskManager.completeTask(title, summary);
217
+ const detail = unblockedTitles.length > 0
218
+ ? `task completed for ${title}: ${summary} (unblocked: ${unblockedTitles.join(", ")})`
219
+ : `task completed for ${title}: ${summary}`;
220
+ return this.logAction({ action: "complete_task", session: sessionId, summary }, true, detail);
218
221
  }
219
222
  // resolve a session reference (exact ID, prefix, or title) to the matching snapshot
220
223
  // single source of truth for session resolution — both tmux name and ID derive from this
package/dist/export.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { TaskState } from "./types.js";
1
2
  import type { HistoryEntry } from "./tui-history.js";
2
3
  export interface TimelineEntry {
3
4
  ts: number;
@@ -14,4 +15,6 @@ export declare function filterByAge(entries: TimelineEntry[], maxAgeMs: number,
14
15
  export declare function parseDuration(input: string): number | null;
15
16
  export declare function formatTimelineJson(entries: TimelineEntry[]): string;
16
17
  export declare function formatTimelineMarkdown(entries: TimelineEntry[]): string;
18
+ export declare function formatTaskExportJson(tasks: TaskState[]): string;
19
+ export declare function formatTaskExportMarkdown(tasks: TaskState[]): string;
17
20
  //# sourceMappingURL=export.d.ts.map
package/dist/export.js CHANGED
@@ -129,4 +129,61 @@ export function formatTimelineMarkdown(entries) {
129
129
  lines.push("");
130
130
  return lines.join("\n");
131
131
  }
132
+ // ── task export ─────────────────────────────────────────────────────────────
133
+ export function formatTaskExportJson(tasks) {
134
+ const payload = tasks.map((t) => ({
135
+ session: t.sessionTitle,
136
+ repo: t.repo,
137
+ status: t.status,
138
+ goal: t.goal,
139
+ dependsOn: t.dependsOn ?? [],
140
+ createdAt: t.createdAt ?? null,
141
+ completedAt: t.completedAt ?? null,
142
+ lastProgressAt: t.lastProgressAt ?? null,
143
+ progressCount: t.progress.length,
144
+ progress: t.progress.map((p) => ({
145
+ at: p.at,
146
+ time: new Date(p.at).toISOString(),
147
+ summary: p.summary,
148
+ })),
149
+ }));
150
+ return JSON.stringify(payload, null, 2);
151
+ }
152
+ export function formatTaskExportMarkdown(tasks) {
153
+ const lines = [];
154
+ lines.push("# Task History Export");
155
+ lines.push("");
156
+ lines.push(`Generated: ${new Date().toISOString()}`);
157
+ lines.push(`Tasks: ${tasks.length}`);
158
+ lines.push("");
159
+ for (const t of tasks) {
160
+ const icon = t.status === "completed" ? "✓"
161
+ : t.status === "active" ? "●"
162
+ : t.status === "paused" ? "◎"
163
+ : t.status === "failed" ? "✗"
164
+ : "○";
165
+ lines.push(`## ${icon} ${t.sessionTitle} (${t.status})`);
166
+ lines.push("");
167
+ lines.push(`- **Goal**: ${t.goal}`);
168
+ lines.push(`- **Repo**: ${t.repo}`);
169
+ if (t.dependsOn?.length)
170
+ lines.push(`- **Depends on**: ${t.dependsOn.join(", ")}`);
171
+ if (t.createdAt)
172
+ lines.push(`- **Created**: ${new Date(t.createdAt).toISOString()}`);
173
+ if (t.completedAt)
174
+ lines.push(`- **Completed**: ${new Date(t.completedAt).toISOString()}`);
175
+ lines.push(`- **Progress entries**: ${t.progress.length}`);
176
+ lines.push("");
177
+ if (t.progress.length > 0) {
178
+ lines.push("### Progress");
179
+ lines.push("");
180
+ for (const p of t.progress) {
181
+ const time = new Date(p.at).toLocaleString();
182
+ lines.push(`- **${time}**: ${p.summary}`);
183
+ }
184
+ lines.push("");
185
+ }
186
+ }
187
+ return lines.join("\n");
188
+ }
132
189
  //# sourceMappingURL=export.js.map
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { TaskManager, loadTaskDefinitions, loadTaskState, saveTaskState, formatT
18
18
  import { goalToList } from "./types.js";
19
19
  import { runTaskCli, handleTaskSlashCommand, quickTaskUpdate } from "./task-cli.js";
20
20
  import { parsePaneMilestones } from "./task-parser.js";
21
- import { TUI, hitTestSession, nextSortMode, SORT_MODES, formatUptime, formatClipText, loadTuiPrefs, saveTuiPrefs, validateGroupName, CONTEXT_BURN_THRESHOLD, buildSnapshotData, formatSnapshotJson, formatSnapshotMarkdown, formatBroadcastSummary, rankSessions, TOP_SORT_MODES, formatIdleSince, CONTEXT_CEILING_THRESHOLD, buildSessionStats, formatSessionStatsLines, formatStatsJson, validateSessionTag, validateColorName, computeErrorTrend, parseQuietHoursRange, computeCostSummary, formatSessionReport, formatQuietStatus, formatSessionAge, formatHealthTrendChart, isOverBudget, DRAIN_ICON, formatSessionsTable, buildFanOutTemplate, TRUST_STABLE_TICKS_TO_ESCALATE, formatTrustLadderStatus, computeContextBudgets, formatContextBudgetTable, CTX_BUDGET_DEFAULT_GLOBAL, resolveProfiles, formatProfileSummary, parseContextCeiling, shouldCompactContext, formatCompactionNudge, formatCompactionAlert, buildSessionDependencyGraph, formatDependencyGraph, formatRelayRules, matchRelayRules, detectOOM, shouldRestartOnOOM, formatOOMAlert, searchSessionOutputs, formatSearchResults, formatThrottleConfig, diffSessionOutput, formatSessionDiff, formatAlertPatterns, matchAlertPatterns, formatLifecycleHooks, matchLifecycleHooks, buildHookEnv } from "./tui.js";
21
+ import { TUI, hitTestSession, nextSortMode, SORT_MODES, formatUptime, formatClipText, loadTuiPrefs, saveTuiPrefs, validateGroupName, CONTEXT_BURN_THRESHOLD, buildSnapshotData, formatSnapshotJson, formatSnapshotMarkdown, formatBroadcastSummary, rankSessions, TOP_SORT_MODES, formatIdleSince, CONTEXT_CEILING_THRESHOLD, buildSessionStats, formatSessionStatsLines, formatStatsJson, validateSessionTag, validateColorName, computeErrorTrend, isQuietHour, parseQuietHoursRange, computeCostSummary, formatSessionReport, formatQuietStatus, formatSessionAge, formatHealthTrendChart, isOverBudget, DRAIN_ICON, formatSessionsTable, buildFanOutTemplate, TRUST_STABLE_TICKS_TO_ESCALATE, formatTrustLadderStatus, computeContextBudgets, formatContextBudgetTable, CTX_BUDGET_DEFAULT_GLOBAL, resolveProfiles, formatProfileSummary, parseContextCeiling, shouldCompactContext, formatCompactionNudge, formatCompactionAlert, buildSessionDependencyGraph, formatDependencyGraph, formatRelayRules, matchRelayRules, detectOOM, shouldRestartOnOOM, formatOOMAlert, searchSessionOutputs, formatSearchResults, formatThrottleConfig, diffSessionOutput, formatSessionDiff, formatAlertPatterns, matchAlertPatterns, formatLifecycleHooks, matchLifecycleHooks, buildHookEnv } from "./tui.js";
22
22
  import { isDaemonRunningFromState } from "./chat.js";
23
23
  import { sendNotification, sendTestNotification, formatNotifyFilters, parseNotifyEvents, shouldNotifySession } from "./notify.js";
24
24
  import { startHealthServer } from "./health.js";
@@ -28,7 +28,7 @@ import { savePreset, deletePreset, getPreset, formatPresetList } from "./pin-pre
28
28
  import { resolvePromptTemplate, formatPromptTemplateList } from "./reasoner/prompt-templates.js";
29
29
  import { formatHealthReport, computeAllHealth } from "./health-score.js";
30
30
  import { ConfigWatcher, formatConfigChange } from "./config-watcher.js";
31
- import { parseActionLogEntries, parseActivityEntries, mergeTimeline, filterByAge, parseDuration, formatTimelineJson, formatTimelineMarkdown } from "./export.js";
31
+ import { parseActionLogEntries, parseActivityEntries, mergeTimeline, filterByAge, parseDuration, formatTimelineJson, formatTimelineMarkdown, formatTaskExportJson, formatTaskExportMarkdown } from "./export.js";
32
32
  import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
33
33
  import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
34
34
  import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
@@ -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, runHealth, healthJson, runSummary, runAdopt, adoptTemplate, 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, runSummary, runAdopt, adoptTemplate, 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, runBackup, backupOutput, runRestore, restoreInput, 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);
@@ -71,7 +71,7 @@ async function main() {
71
71
  return;
72
72
  }
73
73
  // suppress noisy [tasks] [config] log lines in one-shot CLI commands
74
- if (showTasks || runProgress || runHealth || runSummary || runAdopt || showStatus || runRunbook || runIncident || runSupervisor) {
74
+ if (showTasks || runProgress || runHealth || runSummary || runAdopt || runBackup || runRestore || runExport || showStatus || runRunbook || runIncident || runSupervisor) {
75
75
  process.env.AOAOE_QUIET = "1";
76
76
  }
77
77
  // `aoaoe tasks` -- show current task state
@@ -147,6 +147,33 @@ async function main() {
147
147
  await runDoctorCheck();
148
148
  return;
149
149
  }
150
+ if (runBackup) {
151
+ try {
152
+ const result = await createBackup(backupOutput);
153
+ console.log(formatBackupResult(result));
154
+ }
155
+ catch (err) {
156
+ console.error(`backup failed: ${err instanceof Error ? err.message : err}`);
157
+ process.exitCode = 1;
158
+ }
159
+ return;
160
+ }
161
+ if (runRestore) {
162
+ if (!restoreInput) {
163
+ console.error("usage: aoaoe restore <backup-path>");
164
+ process.exitCode = 1;
165
+ return;
166
+ }
167
+ try {
168
+ const result = await restoreBackup(restoreInput);
169
+ console.log(formatRestoreResult(result));
170
+ }
171
+ catch (err) {
172
+ console.error(`restore failed: ${err instanceof Error ? err.message : err}`);
173
+ process.exitCode = 1;
174
+ }
175
+ return;
176
+ }
150
177
  // `aoaoe logs` -- show conversation or action log entries
151
178
  if (runLogs) {
152
179
  await showLogs(logsActions, logsGrep, logsCount);
@@ -154,9 +181,42 @@ async function main() {
154
181
  }
155
182
  // `aoaoe export` -- export session timeline as JSON or Markdown
156
183
  if (runExport) {
157
- await runTimelineExport(exportFormat, exportOutput, exportLast);
184
+ const { exportTasks } = parseCliArgs(process.argv);
185
+ if (exportTasks) {
186
+ await runTaskExport(exportFormat, exportOutput);
187
+ }
188
+ else {
189
+ await runTimelineExport(exportFormat, exportOutput, exportLast);
190
+ }
158
191
  return;
159
192
  }
193
+ async function runTaskExport(format, output) {
194
+ const fmt = format ?? "json";
195
+ if (fmt !== "json" && fmt !== "markdown" && fmt !== "md") {
196
+ console.error(`error: --format must be "json" or "markdown", got "${fmt}"`);
197
+ process.exit(1);
198
+ }
199
+ const basePath = process.cwd();
200
+ const defs = loadTaskDefinitions(basePath);
201
+ const taskProfiles = resolveProfiles(loadConfig());
202
+ const tm = defs.length > 0 ? new TaskManager(basePath, defs, taskProfiles) : undefined;
203
+ const tasks = tm?.tasks ?? [...loadTaskState().values()];
204
+ if (tasks.length === 0) {
205
+ console.error("no tasks to export");
206
+ return;
207
+ }
208
+ const isMarkdown = fmt === "markdown" || fmt === "md";
209
+ const content = isMarkdown ? formatTaskExportMarkdown(tasks) : formatTaskExportJson(tasks);
210
+ if (output) {
211
+ writeFileSync(output, content);
212
+ console.log(`exported ${tasks.length} task(s) to ${output}`);
213
+ }
214
+ else {
215
+ process.stdout.write(content);
216
+ if (!content.endsWith("\n"))
217
+ process.stdout.write("\n");
218
+ }
219
+ }
160
220
  // `aoaoe stats` -- show aggregate daemon statistics
161
221
  if (isStats) {
162
222
  await runStatsCommand(statsLast);
@@ -3230,11 +3290,22 @@ async function main() {
3230
3290
  // - Always reason if there's a user message (immediate response)
3231
3291
  // - Always reason if forceDashboard is set
3232
3292
  // - Otherwise gate on reasonIntervalMs elapsed since last reasoning call
3293
+ // - Skip if in quiet hours (unless user message forces it)
3233
3294
  const msSinceLastReason = Date.now() - lastReasonerAt;
3234
- const reasonDue = lastReasonerAt === 0
3295
+ const quietSpec = config.policies.quietHours;
3296
+ const inQuietHours = quietSpec ? isQuietHour(new Date().getHours(), [parseQuietHoursRange(quietSpec)].filter(Boolean)) : false;
3297
+ const reasonDue = (lastReasonerAt === 0
3235
3298
  || msSinceLastReason >= config.reasonIntervalMs
3236
3299
  || !!userMessage
3237
- || forceDashboard;
3300
+ || forceDashboard)
3301
+ && (!inQuietHours || !!userMessage); // user messages always get through even in quiet hours
3302
+ if (inQuietHours && !userMessage && pollCount % 30 === 1) {
3303
+ const msg = `quiet hours active (${quietSpec}) — polling only, no reasoning`;
3304
+ if (tui)
3305
+ tui.log("status", msg);
3306
+ else
3307
+ log(msg);
3308
+ }
3238
3309
  if (!reasonDue) {
3239
3310
  // Observation-only tick: poll sessions, update TUI state, skip LLM
3240
3311
  const observation = await poller.poll();
@@ -3767,6 +3838,18 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
3767
3838
  });
3768
3839
  }
3769
3840
  }
3841
+ // notify: task lifecycle events
3842
+ if (entry.success && entry.action.action === "complete_task" && sessionTitle) {
3843
+ const nFilters = tui ? tui.getAllSessionNotifyFilters() : new Map();
3844
+ if (shouldNotifySession("task_completed", sessionTitle, nFilters, config.notifications?.events)) {
3845
+ sendNotification(config, {
3846
+ event: "task_completed",
3847
+ timestamp: Date.now(),
3848
+ session: sessionTitle,
3849
+ detail: `completed: ${actionText ?? "task finished"}`,
3850
+ });
3851
+ }
3852
+ }
3770
3853
  }
3771
3854
  // auto-pause tracking: record stuck nudges for send_input actions targeting stuck sessions.
3772
3855
  // a "stuck nudge" = send_input to a session that hasn't had progress in >30 min.
@@ -3794,6 +3877,15 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
3794
3877
  else
3795
3878
  log(msg);
3796
3879
  appendSupervisorEvent({ at: Date.now(), detail: `auto-pause: ${title}` });
3880
+ const nFilters = tui ? tui.getAllSessionNotifyFilters() : new Map();
3881
+ if (shouldNotifySession("task_stuck", title, nFilters, config.notifications?.events)) {
3882
+ sendNotification(config, {
3883
+ event: "task_stuck",
3884
+ timestamp: Date.now(),
3885
+ session: title,
3886
+ detail: `auto-paused after ${task.stuckNudgeCount} stuck nudges with no progress`,
3887
+ });
3888
+ }
3797
3889
  }
3798
3890
  }
3799
3891
  }
@@ -4437,6 +4529,7 @@ async function showProgressDigest(since, asJson = false) {
4437
4529
  console.log(formatProgressDigest(tasks, maxAgeMs));
4438
4530
  }
4439
4531
  import { resolveTemplate } from "./task-templates.js";
4532
+ import { createBackup, restoreBackup, formatBackupResult, formatRestoreResult } from "./backup.js";
4440
4533
  // adopt untracked live AoE sessions as tasks with optional template goal.
4441
4534
  async function adoptUntrackedSessions(templateName) {
4442
4535
  const basePath = process.cwd();
package/dist/notify.js CHANGED
@@ -200,6 +200,9 @@ function eventTitle(event) {
200
200
  case "action_failed": return "Action Failed";
201
201
  case "daemon_started": return "Daemon Started";
202
202
  case "daemon_stopped": return "Daemon Stopped";
203
+ case "task_completed": return "Task Completed";
204
+ case "task_stuck": return "Task Stuck";
205
+ case "task_unblocked": return "Task Unblocked";
203
206
  }
204
207
  }
205
208
  /**
@@ -245,6 +248,7 @@ export function formatNotifyFilters(sessionFilters) {
245
248
  */
246
249
  export const VALID_NOTIFY_EVENTS = [
247
250
  "session_error", "session_done", "action_executed", "action_failed", "daemon_started", "daemon_stopped",
251
+ "task_completed", "task_stuck", "task_unblocked",
248
252
  ];
249
253
  export function parseNotifyEvents(eventNames) {
250
254
  const valid = new Set(VALID_NOTIFY_EVENTS);
@@ -264,6 +268,9 @@ function eventIcon(event) {
264
268
  case "action_failed": return "\u274C"; // ❌
265
269
  case "daemon_started": return "\u{1F680}"; // 🚀
266
270
  case "daemon_stopped": return "\u{1F6D1}"; // 🛑
271
+ case "task_completed": return "\u{1F3C6}"; // 🏆
272
+ case "task_stuck": return "\u26A0\uFE0F"; // ⚠️
273
+ case "task_unblocked": return "\u{1F513}"; // 🔓
267
274
  }
268
275
  }
269
276
  //# sourceMappingURL=notify.js.map
package/dist/task-cli.js CHANGED
@@ -6,7 +6,7 @@ import { resolve, basename } from "node:path";
6
6
  import { buildProfileListArgs } from "./poller.js";
7
7
  import { loadConfig } from "./config.js";
8
8
  import { resolveProfiles } from "./tui.js";
9
- import { loadTaskState, saveTaskState, formatTaskTable, syncTaskDefinitionsFromState, taskStateKey, resolveTaskRepoPath, TaskManager, loadTaskDefinitions, injectGoalToSession } from "./task-manager.js";
9
+ import { loadTaskState, saveTaskState, formatTaskTable, formatTaskHistory, syncTaskDefinitionsFromState, taskStateKey, resolveTaskRepoPath, TaskManager, loadTaskDefinitions, injectGoalToSession } from "./task-manager.js";
10
10
  import { goalToList } from "./types.js";
11
11
  import { resolveTemplate, formatTemplateList } from "./task-templates.js";
12
12
  import { BOLD, DIM, GREEN, YELLOW, RED, RESET } from "./colors.js";
@@ -26,6 +26,7 @@ function getTaskProfiles() {
26
26
  function taskCommandHelp(prefix = "aoaoe task") {
27
27
  return [
28
28
  `${prefix} list show tracked tasks`,
29
+ `${prefix} history [session] show progress history (all or one session)`,
29
30
  `${prefix} templates show available task templates`,
30
31
  `${prefix} reconcile link/create sessions now`,
31
32
  `${prefix} new <title> <path> create task + session`,
@@ -454,6 +455,13 @@ export async function runTaskCli(argv) {
454
455
  console.log(formatTemplateList());
455
456
  return;
456
457
  }
458
+ case "history": {
459
+ const states = loadTaskState();
460
+ const tasks = [...states.values()];
461
+ const sessionFilter = args[0] || undefined;
462
+ console.log(formatTaskHistory(tasks, sessionFilter));
463
+ return;
464
+ }
457
465
  case "reconcile": {
458
466
  const basePath = process.cwd();
459
467
  const defs = loadTaskDefinitions(basePath);
@@ -623,6 +631,12 @@ export async function handleTaskSlashCommand(args) {
623
631
  if (sub === "templates" || sub === "template") {
624
632
  return formatTemplateList();
625
633
  }
626
- return "usage: /task [list|start|stop|edit|new|rm|reconcile|templates|help] [args]";
634
+ if (sub === "history") {
635
+ const states = loadTaskState();
636
+ const tasks = [...states.values()];
637
+ const sessionFilter = rest[0] || undefined;
638
+ return formatTaskHistory(tasks, sessionFilter);
639
+ }
640
+ return "usage: /task [list|start|stop|edit|new|rm|reconcile|templates|history|help] [args]";
627
641
  }
628
642
  //# sourceMappingURL=task-cli.js.map
@@ -32,7 +32,7 @@ export declare class TaskManager {
32
32
  }>;
33
33
  reportProgress(sessionTitle: string, summary: string): void;
34
34
  recordStuckNudge(sessionTitle: string, maxNudges: number): boolean;
35
- completeTask(sessionTitle: string, summary: string, cleanupSession?: boolean): Promise<void>;
35
+ completeTask(sessionTitle: string, summary: string, cleanupSession?: boolean): Promise<string[]>;
36
36
  private save;
37
37
  }
38
38
  export declare function deriveTitle(repo: string): string;
@@ -43,6 +43,7 @@ export declare function deriveTitle(repo: string): string;
43
43
  */
44
44
  export declare function readNextRoadmapItems(basePath: string, maxItems?: number): string;
45
45
  export declare function formatTaskTable(states: Map<string, TaskState> | TaskState[]): string;
46
+ export declare function formatTaskHistory(tasks: TaskState[], sessionFilter?: string): string;
46
47
  export declare function formatAgo(ms: number): string;
47
48
  export declare function formatProgressDigest(tasks: TaskState[], maxAgeMs?: number): string;
48
49
  //# sourceMappingURL=task-manager.d.ts.map
@@ -487,7 +487,7 @@ export class TaskManager {
487
487
  async completeTask(sessionTitle, summary, cleanupSession = true) {
488
488
  const task = this.getTaskForSession(sessionTitle);
489
489
  if (!task)
490
- return;
490
+ return [];
491
491
  // find original definition to check continueOnRoadmap
492
492
  const def = this.definitions.find((d) => (d.sessionTitle || deriveTitle(d.repo)).toLowerCase() === sessionTitle.toLowerCase());
493
493
  if (def?.continueOnRoadmap) {
@@ -499,7 +499,7 @@ export class TaskManager {
499
499
  task.lastProgressAt = Date.now();
500
500
  log(`continueOnRoadmap: recycled task '${sessionTitle}' with fresh roadmap goal`);
501
501
  this.save();
502
- return;
502
+ return [];
503
503
  }
504
504
  task.status = "completed";
505
505
  task.completedAt = Date.now();
@@ -516,6 +516,7 @@ export class TaskManager {
516
516
  log(`dependency met: activated '${downstream.sessionTitle}' (was waiting on '${task.sessionTitle}')`);
517
517
  }
518
518
  this.save();
519
+ return unblocked.map((t) => t.sessionTitle);
519
520
  }
520
521
  save() {
521
522
  saveTaskState(this.states);
@@ -628,6 +629,37 @@ export function formatTaskTable(states) {
628
629
  }
629
630
  return lines.join("\n");
630
631
  }
632
+ // format detailed progress history for one or all tasks.
633
+ export function formatTaskHistory(tasks, sessionFilter) {
634
+ const filtered = sessionFilter
635
+ ? tasks.filter((t) => t.sessionTitle.toLowerCase() === sessionFilter.toLowerCase()
636
+ || t.sessionTitle.toLowerCase().includes(sessionFilter.toLowerCase()))
637
+ : tasks;
638
+ if (filtered.length === 0) {
639
+ return sessionFilter ? ` no task found matching "${sessionFilter}"` : " (no tasks)";
640
+ }
641
+ const lines = [];
642
+ for (const t of filtered) {
643
+ const statusIcon = t.status === "completed" ? "✓"
644
+ : t.status === "active" ? "●"
645
+ : t.status === "paused" ? "◎"
646
+ : t.status === "failed" ? "✗"
647
+ : "○";
648
+ lines.push(` ${statusIcon} ${BOLD}${t.sessionTitle}${RESET} ${DIM}(${t.status})${RESET}`);
649
+ lines.push(` ${DIM}goal: ${t.goal.length > 80 ? t.goal.slice(0, 77) + "..." : t.goal}${RESET}`);
650
+ if (t.progress.length === 0) {
651
+ lines.push(` ${DIM}(no progress entries)${RESET}`);
652
+ }
653
+ else {
654
+ for (const p of t.progress) {
655
+ const ts = new Date(p.at).toLocaleString();
656
+ lines.push(` ${DIM}${ts}${RESET} ${p.summary}`);
657
+ }
658
+ }
659
+ lines.push("");
660
+ }
661
+ return lines.join("\n");
662
+ }
631
663
  export function formatAgo(ms) {
632
664
  if (ms < 60_000)
633
665
  return `${Math.round(ms / 1000)}s ago`;
package/dist/types.d.ts CHANGED
@@ -112,6 +112,7 @@ export interface AoaoeConfig {
112
112
  userActivityThresholdMs?: number;
113
113
  allowDestructive?: boolean;
114
114
  maxStuckNudgesBeforePause?: number;
115
+ quietHours?: string;
115
116
  };
116
117
  contextFiles: string[];
117
118
  sessionDirs: Record<string, string>;
@@ -132,7 +133,7 @@ export interface AoaoeConfig {
132
133
  healthPort?: number;
133
134
  tuiHistoryRetentionDays?: number;
134
135
  }
135
- export type NotificationEvent = "session_error" | "session_done" | "action_executed" | "action_failed" | "daemon_started" | "daemon_stopped";
136
+ export type NotificationEvent = "session_error" | "session_done" | "action_executed" | "action_failed" | "daemon_started" | "daemon_stopped" | "task_completed" | "task_stuck" | "task_unblocked";
136
137
  export type DaemonPhase = "sleeping" | "polling" | "reasoning" | "executing" | "interrupted";
137
138
  export interface DaemonSessionState {
138
139
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.190.0",
3
+ "version": "0.192.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",