aoaoe 0.186.0 → 0.187.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/health-score.d.ts +11 -0
- package/dist/health-score.js +100 -0
- package/dist/index.js +188 -0
- package/dist/input.js +24 -0
- package/dist/pin-presets.d.ts +10 -0
- package/dist/pin-presets.js +72 -0
- package/dist/reasoner/claude-code.js +1 -1
- package/dist/reasoner/opencode.js +1 -1
- package/dist/reasoner/prompt-templates.d.ts +10 -0
- package/dist/reasoner/prompt-templates.js +111 -0
- package/dist/reasoner/prompt.d.ts +1 -1
- package/dist/reasoner/prompt.js +5 -3
- package/dist/task-cli.js +98 -4
- package/dist/task-manager.d.ts +1 -0
- package/dist/task-manager.js +16 -0
- package/dist/task-templates.d.ts +11 -0
- package/dist/task-templates.js +96 -0
- package/dist/tui.d.ts +4 -0
- package/dist/tui.js +22 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TaskState } from "./types.js";
|
|
2
|
+
export interface SessionHealth {
|
|
3
|
+
session: string;
|
|
4
|
+
score: number;
|
|
5
|
+
grade: "healthy" | "ok" | "degraded" | "critical" | "inactive";
|
|
6
|
+
factors: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function computeHealth(task: TaskState): SessionHealth;
|
|
9
|
+
export declare function computeAllHealth(tasks: TaskState[]): SessionHealth[];
|
|
10
|
+
export declare function formatHealthReport(tasks: TaskState[]): string;
|
|
11
|
+
//# sourceMappingURL=health-score.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// health-score.ts — compute per-session health from task state + progress patterns.
|
|
2
|
+
// health is a 0-100 score where 100 = healthy, active, progressing.
|
|
3
|
+
import { BOLD, DIM, GREEN, YELLOW, RED, CYAN, RESET } from "./colors.js";
|
|
4
|
+
// compute health score for a single task
|
|
5
|
+
export function computeHealth(task) {
|
|
6
|
+
const factors = [];
|
|
7
|
+
let score = 100;
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
// status penalties
|
|
10
|
+
if (task.status === "completed") {
|
|
11
|
+
return { session: task.sessionTitle, score: 100, grade: "healthy", factors: ["completed"] };
|
|
12
|
+
}
|
|
13
|
+
if (task.status === "failed") {
|
|
14
|
+
return { session: task.sessionTitle, score: 0, grade: "critical", factors: ["task failed"] };
|
|
15
|
+
}
|
|
16
|
+
if (task.status === "paused") {
|
|
17
|
+
return { session: task.sessionTitle, score: 30, grade: "degraded", factors: ["paused"] };
|
|
18
|
+
}
|
|
19
|
+
if (task.status === "pending") {
|
|
20
|
+
const hasDeps = task.dependsOn && task.dependsOn.length > 0;
|
|
21
|
+
return { session: task.sessionTitle, score: 50, grade: "inactive", factors: [hasDeps ? "waiting on dependencies" : "not started"] };
|
|
22
|
+
}
|
|
23
|
+
// progress recency (active tasks)
|
|
24
|
+
if (task.lastProgressAt) {
|
|
25
|
+
const ageMs = now - task.lastProgressAt;
|
|
26
|
+
if (ageMs < 10 * 60_000) {
|
|
27
|
+
factors.push("progress <10m ago");
|
|
28
|
+
}
|
|
29
|
+
else if (ageMs < 30 * 60_000) {
|
|
30
|
+
score -= 10;
|
|
31
|
+
factors.push("progress 10-30m ago");
|
|
32
|
+
}
|
|
33
|
+
else if (ageMs < 60 * 60_000) {
|
|
34
|
+
score -= 25;
|
|
35
|
+
factors.push("progress 30-60m ago");
|
|
36
|
+
}
|
|
37
|
+
else if (ageMs < 4 * 60 * 60_000) {
|
|
38
|
+
score -= 40;
|
|
39
|
+
factors.push("progress 1-4h ago");
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
score -= 55;
|
|
43
|
+
factors.push(`progress ${Math.round(ageMs / 3_600_000)}h ago`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
score -= 30;
|
|
48
|
+
factors.push("no progress recorded");
|
|
49
|
+
}
|
|
50
|
+
// stuck nudge count
|
|
51
|
+
const nudges = task.stuckNudgeCount ?? 0;
|
|
52
|
+
if (nudges > 0) {
|
|
53
|
+
score -= Math.min(nudges * 10, 30);
|
|
54
|
+
factors.push(`${nudges} stuck nudge(s)`);
|
|
55
|
+
}
|
|
56
|
+
// progress velocity (more entries = healthier)
|
|
57
|
+
const recentEntries = task.progress.filter((p) => p.at >= now - 4 * 60 * 60_000).length;
|
|
58
|
+
if (recentEntries >= 3) {
|
|
59
|
+
score = Math.min(score + 10, 100);
|
|
60
|
+
factors.push(`${recentEntries} progress entries in 4h`);
|
|
61
|
+
}
|
|
62
|
+
else if (recentEntries === 0 && task.progress.length > 0) {
|
|
63
|
+
score -= 10;
|
|
64
|
+
factors.push("no progress in last 4h");
|
|
65
|
+
}
|
|
66
|
+
score = Math.max(0, Math.min(100, score));
|
|
67
|
+
const grade = score >= 80 ? "healthy" :
|
|
68
|
+
score >= 60 ? "ok" :
|
|
69
|
+
score >= 40 ? "degraded" :
|
|
70
|
+
"critical";
|
|
71
|
+
return { session: task.sessionTitle, score, grade, factors };
|
|
72
|
+
}
|
|
73
|
+
// compute health for all tasks
|
|
74
|
+
export function computeAllHealth(tasks) {
|
|
75
|
+
return tasks.map(computeHealth);
|
|
76
|
+
}
|
|
77
|
+
// format health report for display
|
|
78
|
+
export function formatHealthReport(tasks) {
|
|
79
|
+
const healths = computeAllHealth(tasks);
|
|
80
|
+
if (healths.length === 0)
|
|
81
|
+
return " (no tasks)";
|
|
82
|
+
const lines = [];
|
|
83
|
+
lines.push(` ${BOLD}SESSION HEALTH${RESET}`);
|
|
84
|
+
lines.push(` ${"-".repeat(60)}`);
|
|
85
|
+
for (const h of healths) {
|
|
86
|
+
const color = h.grade === "healthy" ? GREEN
|
|
87
|
+
: h.grade === "ok" ? CYAN
|
|
88
|
+
: h.grade === "degraded" ? YELLOW
|
|
89
|
+
: h.grade === "critical" ? RED
|
|
90
|
+
: DIM;
|
|
91
|
+
const bar = "█".repeat(Math.round(h.score / 10)) + "░".repeat(10 - Math.round(h.score / 10));
|
|
92
|
+
lines.push(` ${color}${bar}${RESET} ${h.score.toString().padStart(3)} ${BOLD}${h.session}${RESET} ${DIM}(${h.grade})${RESET}`);
|
|
93
|
+
lines.push(` ${DIM} ${h.factors.join(" · ")}${RESET}`);
|
|
94
|
+
}
|
|
95
|
+
const avg = Math.round(healths.reduce((sum, h) => sum + h.score, 0) / healths.length);
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push(` ${DIM}fleet average: ${avg}/100${RESET}`);
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=health-score.js.map
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,9 @@ import { sendNotification, sendTestNotification, formatNotifyFilters, parseNotif
|
|
|
24
24
|
import { startHealthServer } from "./health.js";
|
|
25
25
|
import { loadTuiHistory, searchHistory, TUI_HISTORY_FILE, computeHistoryStats } from "./tui-history.js";
|
|
26
26
|
import { appendSupervisorEvent, loadSupervisorEvents } from "./supervisor-history.js";
|
|
27
|
+
import { savePreset, deletePreset, getPreset, formatPresetList } from "./pin-presets.js";
|
|
28
|
+
import { resolvePromptTemplate, formatPromptTemplateList } from "./reasoner/prompt-templates.js";
|
|
29
|
+
import { formatHealthReport } from "./health-score.js";
|
|
27
30
|
import { ConfigWatcher, formatConfigChange } from "./config-watcher.js";
|
|
28
31
|
import { parseActionLogEntries, parseActivityEntries, mergeTimeline, filterByAge, parseDuration, formatTimelineJson, formatTimelineMarkdown } from "./export.js";
|
|
29
32
|
import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
|
|
@@ -2295,6 +2298,150 @@ async function main() {
|
|
|
2295
2298
|
const userMessage = userMessages.length > 0 ? formatUserMessages(userMessages) : undefined;
|
|
2296
2299
|
// handle built-in command markers (from stdin or chat.ts file IPC)
|
|
2297
2300
|
for (const cmd of commands) {
|
|
2301
|
+
if (cmd.startsWith("__CMD_PIN_SAVE__")) {
|
|
2302
|
+
const name = cmd.slice("__CMD_PIN_SAVE__".length).trim();
|
|
2303
|
+
if (!name) {
|
|
2304
|
+
const msg = "usage: /pin-save <preset-name>";
|
|
2305
|
+
if (tui)
|
|
2306
|
+
tui.log("error", msg);
|
|
2307
|
+
else
|
|
2308
|
+
log(msg);
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
if (tui) {
|
|
2312
|
+
const titles = tui.getPinnedTitles();
|
|
2313
|
+
if (titles.length === 0) {
|
|
2314
|
+
const msg = "no sessions pinned — /pin a session first";
|
|
2315
|
+
tui.log("status", msg);
|
|
2316
|
+
}
|
|
2317
|
+
else {
|
|
2318
|
+
savePreset(name, titles);
|
|
2319
|
+
const msg = `pin preset saved: ${name} (${titles.join(", ")})`;
|
|
2320
|
+
tui.log("system", msg);
|
|
2321
|
+
pushSupervisorEvent(`pin-save: ${name} [${titles.length} sessions]`);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
else {
|
|
2325
|
+
log("pin presets require TUI mode");
|
|
2326
|
+
}
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
if (cmd.startsWith("__CMD_PIN_LOAD__")) {
|
|
2330
|
+
const name = cmd.slice("__CMD_PIN_LOAD__".length).trim();
|
|
2331
|
+
if (!name) {
|
|
2332
|
+
const msg = "usage: /pin-load <preset-name>";
|
|
2333
|
+
if (tui)
|
|
2334
|
+
tui.log("error", msg);
|
|
2335
|
+
else
|
|
2336
|
+
log(msg);
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
const titles = getPreset(name);
|
|
2340
|
+
if (!titles) {
|
|
2341
|
+
const msg = `pin preset not found: ${name}`;
|
|
2342
|
+
if (tui)
|
|
2343
|
+
tui.log("error", msg);
|
|
2344
|
+
else
|
|
2345
|
+
log(msg);
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
if (tui) {
|
|
2349
|
+
const count = tui.loadPinPreset(titles);
|
|
2350
|
+
const msg = `pin preset loaded: ${name} (${count} sessions pinned)`;
|
|
2351
|
+
tui.log("system", msg);
|
|
2352
|
+
pushSupervisorEvent(`pin-load: ${name} [${count} pinned]`);
|
|
2353
|
+
}
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
if (cmd.startsWith("__CMD_PIN_DELETE__")) {
|
|
2357
|
+
const name = cmd.slice("__CMD_PIN_DELETE__".length).trim();
|
|
2358
|
+
if (!name) {
|
|
2359
|
+
const msg = "usage: /pin-delete <preset-name>";
|
|
2360
|
+
if (tui)
|
|
2361
|
+
tui.log("error", msg);
|
|
2362
|
+
else
|
|
2363
|
+
log(msg);
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
const ok = deletePreset(name);
|
|
2367
|
+
const msg = ok ? `pin preset deleted: ${name}` : `pin preset not found: ${name}`;
|
|
2368
|
+
if (tui)
|
|
2369
|
+
tui.log(ok ? "system" : "error", msg);
|
|
2370
|
+
else
|
|
2371
|
+
log(msg);
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
if (cmd === "__CMD_PIN_PRESETS__") {
|
|
2375
|
+
const msg = formatPresetList();
|
|
2376
|
+
for (const line of msg.split("\n")) {
|
|
2377
|
+
if (tui)
|
|
2378
|
+
tui.log("status", line);
|
|
2379
|
+
else
|
|
2380
|
+
log(line);
|
|
2381
|
+
}
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
if (cmd.startsWith("__CMD_PROMPT_TEMPLATE__")) {
|
|
2385
|
+
const name = cmd.slice("__CMD_PROMPT_TEMPLATE__".length).trim();
|
|
2386
|
+
if (!name) {
|
|
2387
|
+
const current = config.promptTemplate || "default";
|
|
2388
|
+
const msg = `current prompt template: ${current}`;
|
|
2389
|
+
if (tui)
|
|
2390
|
+
tui.log("status", msg);
|
|
2391
|
+
else
|
|
2392
|
+
log(msg);
|
|
2393
|
+
const list = formatPromptTemplateList();
|
|
2394
|
+
for (const line of list.split("\n")) {
|
|
2395
|
+
if (tui)
|
|
2396
|
+
tui.log("status", line);
|
|
2397
|
+
else
|
|
2398
|
+
log(line);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
else {
|
|
2402
|
+
const tmpl = resolvePromptTemplate(name);
|
|
2403
|
+
if (!tmpl) {
|
|
2404
|
+
const msg = `unknown prompt template: ${name}`;
|
|
2405
|
+
if (tui)
|
|
2406
|
+
tui.log("error", msg);
|
|
2407
|
+
else
|
|
2408
|
+
log(msg);
|
|
2409
|
+
const list = formatPromptTemplateList();
|
|
2410
|
+
for (const line of list.split("\n")) {
|
|
2411
|
+
if (tui)
|
|
2412
|
+
tui.log("status", line);
|
|
2413
|
+
else
|
|
2414
|
+
log(line);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
else {
|
|
2418
|
+
config.promptTemplate = tmpl.name;
|
|
2419
|
+
const msg = `prompt template set to: ${tmpl.name} — ${tmpl.description}`;
|
|
2420
|
+
if (tui)
|
|
2421
|
+
tui.log("system", msg);
|
|
2422
|
+
else
|
|
2423
|
+
log(msg);
|
|
2424
|
+
pushSupervisorEvent(`prompt-template: ${tmpl.name}`);
|
|
2425
|
+
reasonerConsole.writeSystem(msg);
|
|
2426
|
+
// note: takes effect on next reasoner init (next reasoning cycle)
|
|
2427
|
+
if (tui)
|
|
2428
|
+
tui.log("status", `(takes effect on next reasoning cycle)`);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
if (cmd === "__CMD_HEALTH__") {
|
|
2434
|
+
const tasks = taskManager?.tasks ?? [];
|
|
2435
|
+
const report = formatHealthReport(tasks);
|
|
2436
|
+
for (const line of report.split("\n")) {
|
|
2437
|
+
if (tui)
|
|
2438
|
+
tui.log("status", line);
|
|
2439
|
+
else
|
|
2440
|
+
log(line);
|
|
2441
|
+
reasonerConsole.writeSystem(line);
|
|
2442
|
+
}
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2298
2445
|
if (cmd === "__CMD_STATUS__") {
|
|
2299
2446
|
const isPausedNow = paused || input.isPaused();
|
|
2300
2447
|
const modeMsg = `status: poll #${pollCount}, mode=${getRunMode()}, reasoner=${getReasonerLabel()}, paused=${isPausedNow}`;
|
|
@@ -3600,6 +3747,36 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
|
|
|
3600
3747
|
}
|
|
3601
3748
|
}
|
|
3602
3749
|
}
|
|
3750
|
+
// auto-pause tracking: record stuck nudges for send_input actions targeting stuck sessions.
|
|
3751
|
+
// a "stuck nudge" = send_input to a session that hasn't had progress in >30 min.
|
|
3752
|
+
if (taskManager) {
|
|
3753
|
+
const maxNudges = config.policies.maxStuckNudgesBeforePause ?? 0;
|
|
3754
|
+
const stuckThresholdMs = 30 * 60 * 1000;
|
|
3755
|
+
const now = Date.now();
|
|
3756
|
+
for (const entry of executed) {
|
|
3757
|
+
if (entry.action.action !== "send_input" || !entry.success)
|
|
3758
|
+
continue;
|
|
3759
|
+
const sid = actionSession(entry.action);
|
|
3760
|
+
const title = sid ? (sessionTitleMap.get(sid) ?? sid) : undefined;
|
|
3761
|
+
if (!title)
|
|
3762
|
+
continue;
|
|
3763
|
+
const task = taskManager.getTaskForSession(title);
|
|
3764
|
+
if (!task || task.status !== "active")
|
|
3765
|
+
continue;
|
|
3766
|
+
const lastProgress = task.lastProgressAt ?? 0;
|
|
3767
|
+
if (lastProgress > 0 && (now - lastProgress) > stuckThresholdMs) {
|
|
3768
|
+
const paused = taskManager.recordStuckNudge(title, maxNudges);
|
|
3769
|
+
if (paused) {
|
|
3770
|
+
const msg = `auto-paused '${title}' after ${task.stuckNudgeCount} stuck nudges`;
|
|
3771
|
+
if (tui)
|
|
3772
|
+
tui.log("system", msg);
|
|
3773
|
+
else
|
|
3774
|
+
log(msg);
|
|
3775
|
+
appendSupervisorEvent({ at: Date.now(), detail: `auto-pause: ${title}` });
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3603
3780
|
// fire lifecycle hooks for completed actions
|
|
3604
3781
|
if (tui) {
|
|
3605
3782
|
const hooks = tui.getLifecycleHooks();
|
|
@@ -4224,7 +4401,18 @@ async function showProgressDigest(since, asJson = false) {
|
|
|
4224
4401
|
console.log("no tasks defined.");
|
|
4225
4402
|
return;
|
|
4226
4403
|
}
|
|
4404
|
+
// enrich with live session status
|
|
4405
|
+
const liveStatus = await probeLiveSessionStatus();
|
|
4406
|
+
const liveCount = liveStatus.size;
|
|
4407
|
+
const missingFromTasks = [...liveStatus.keys()].filter((title) => !tasks.some((t) => t.sessionTitle.toLowerCase() === title));
|
|
4227
4408
|
console.log("");
|
|
4409
|
+
if (liveCount > 0) {
|
|
4410
|
+
console.log(` ${DIM}live sessions: ${liveCount} (${[...liveStatus.entries()].map(([t, s]) => `${t}=${s}`).join(", ")})${RESET}`);
|
|
4411
|
+
if (missingFromTasks.length > 0) {
|
|
4412
|
+
console.log(` ${YELLOW}untracked sessions: ${missingFromTasks.join(", ")} — use /task <name> :: <goal> to adopt${RESET}`);
|
|
4413
|
+
}
|
|
4414
|
+
console.log("");
|
|
4415
|
+
}
|
|
4228
4416
|
console.log(formatProgressDigest(tasks, maxAgeMs));
|
|
4229
4417
|
}
|
|
4230
4418
|
// `aoaoe history` -- review recent actions from the persistent action log
|
package/dist/input.js
CHANGED
|
@@ -709,6 +709,10 @@ ${BOLD}navigation:${RESET}
|
|
|
709
709
|
/sort [mode] sort sessions: status, name, activity, default (or cycle)
|
|
710
710
|
/compact toggle compact mode (dense session panel)
|
|
711
711
|
/pin [N|name] pin/unpin a session to the top (toggle)
|
|
712
|
+
/pin-save <name> save current pins as a named preset
|
|
713
|
+
/pin-load <name> restore a saved pin preset
|
|
714
|
+
/pin-delete <name> delete a saved preset
|
|
715
|
+
/pin-presets list saved pin presets
|
|
712
716
|
/bell toggle terminal bell on errors/completions
|
|
713
717
|
/focus toggle focus mode (show only pinned sessions)
|
|
714
718
|
/mute [N|name] mute/unmute a session's activity entries (toggle)
|
|
@@ -797,6 +801,8 @@ ${BOLD}navigation:${RESET}
|
|
|
797
801
|
${BOLD}info:${RESET}
|
|
798
802
|
/status show daemon state
|
|
799
803
|
/progress [opts] what each session accomplished recently; opts: --since <1h|8h|24h> --json
|
|
804
|
+
/health session health scores (0-100 per task, fleet average)
|
|
805
|
+
/prompt-template [name] set/show reasoner prompt strategy (default, hands-off, aggressive, review-focused, shipping)
|
|
800
806
|
/incident [opts] quick incident view; opts: --since <30m|2h|1d> --limit N --json --ndjson --follow (watch via CLI)
|
|
801
807
|
/runbook [section] show operator playbook (opts: quickstart|response-flow|incident|all, --json)
|
|
802
808
|
/supervisor [opts] show judge/orchestrator status; opts: --all --since <1h|30m|2d> --limit N --json
|
|
@@ -994,6 +1000,24 @@ ${BOLD}other:${RESET}
|
|
|
994
1000
|
}
|
|
995
1001
|
break;
|
|
996
1002
|
}
|
|
1003
|
+
case "/pin-save":
|
|
1004
|
+
this.queue.push(`__CMD_PIN_SAVE__${line.slice("/pin-save".length)}`);
|
|
1005
|
+
break;
|
|
1006
|
+
case "/pin-load":
|
|
1007
|
+
this.queue.push(`__CMD_PIN_LOAD__${line.slice("/pin-load".length)}`);
|
|
1008
|
+
break;
|
|
1009
|
+
case "/pin-delete":
|
|
1010
|
+
this.queue.push(`__CMD_PIN_DELETE__${line.slice("/pin-delete".length)}`);
|
|
1011
|
+
break;
|
|
1012
|
+
case "/pin-presets":
|
|
1013
|
+
this.queue.push("__CMD_PIN_PRESETS__");
|
|
1014
|
+
break;
|
|
1015
|
+
case "/prompt-template":
|
|
1016
|
+
this.queue.push(`__CMD_PROMPT_TEMPLATE__${line.slice("/prompt-template".length)}`);
|
|
1017
|
+
break;
|
|
1018
|
+
case "/health":
|
|
1019
|
+
this.queue.push("__CMD_HEALTH__");
|
|
1020
|
+
break;
|
|
997
1021
|
case "/bell":
|
|
998
1022
|
if (this.bellHandler) {
|
|
999
1023
|
this.bellHandler();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface PinPresets {
|
|
2
|
+
[name: string]: string[];
|
|
3
|
+
}
|
|
4
|
+
export declare function loadPinPresets(filePath?: string): PinPresets;
|
|
5
|
+
export declare function savePinPresets(presets: PinPresets, filePath?: string): void;
|
|
6
|
+
export declare function savePreset(name: string, titles: string[], filePath?: string): void;
|
|
7
|
+
export declare function deletePreset(name: string, filePath?: string): boolean;
|
|
8
|
+
export declare function getPreset(name: string, filePath?: string): string[] | undefined;
|
|
9
|
+
export declare function formatPresetList(filePath?: string): string;
|
|
10
|
+
//# sourceMappingURL=pin-presets.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// pin-presets.ts — save/restore named sets of pinned session titles.
|
|
2
|
+
// stored in ~/.aoaoe/pin-presets.json as { presetName: ["title1", "title2"] }.
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { BOLD, DIM, GREEN, RESET } from "./colors.js";
|
|
7
|
+
const AOAOE_DIR = join(homedir(), ".aoaoe");
|
|
8
|
+
const PRESETS_FILE = join(AOAOE_DIR, "pin-presets.json");
|
|
9
|
+
export function loadPinPresets(filePath = PRESETS_FILE) {
|
|
10
|
+
try {
|
|
11
|
+
if (!existsSync(filePath))
|
|
12
|
+
return {};
|
|
13
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
14
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
15
|
+
return {};
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
18
|
+
if (Array.isArray(v) && v.every((s) => typeof s === "string")) {
|
|
19
|
+
result[k] = v;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function savePinPresets(presets, filePath = PRESETS_FILE) {
|
|
29
|
+
try {
|
|
30
|
+
if (!existsSync(AOAOE_DIR))
|
|
31
|
+
mkdirSync(AOAOE_DIR, { recursive: true });
|
|
32
|
+
writeFileSync(filePath, JSON.stringify(presets, null, 2) + "\n");
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.error(`failed to save pin presets: ${e}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function savePreset(name, titles, filePath = PRESETS_FILE) {
|
|
39
|
+
const presets = loadPinPresets(filePath);
|
|
40
|
+
presets[name] = titles;
|
|
41
|
+
savePinPresets(presets, filePath);
|
|
42
|
+
}
|
|
43
|
+
export function deletePreset(name, filePath = PRESETS_FILE) {
|
|
44
|
+
const presets = loadPinPresets(filePath);
|
|
45
|
+
if (!(name in presets))
|
|
46
|
+
return false;
|
|
47
|
+
delete presets[name];
|
|
48
|
+
savePinPresets(presets, filePath);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
export function getPreset(name, filePath = PRESETS_FILE) {
|
|
52
|
+
const presets = loadPinPresets(filePath);
|
|
53
|
+
const lower = name.toLowerCase();
|
|
54
|
+
const key = Object.keys(presets).find((k) => k.toLowerCase() === lower);
|
|
55
|
+
return key ? presets[key] : undefined;
|
|
56
|
+
}
|
|
57
|
+
export function formatPresetList(filePath = PRESETS_FILE) {
|
|
58
|
+
const presets = loadPinPresets(filePath);
|
|
59
|
+
const names = Object.keys(presets);
|
|
60
|
+
if (names.length === 0)
|
|
61
|
+
return ` ${DIM}(no saved presets)${RESET}\n ${DIM}save: /pin-save <name>${RESET}`;
|
|
62
|
+
const lines = [];
|
|
63
|
+
lines.push(` ${BOLD}saved pin presets:${RESET}`);
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
const titles = presets[name];
|
|
66
|
+
lines.push(` ${GREEN}${name}${RESET} — ${titles.join(", ")} ${DIM}(${titles.length} sessions)${RESET}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push(` ${DIM}restore: /pin-load <name> | delete: /pin-delete <name>${RESET}`);
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=pin-presets.js.map
|
|
@@ -9,7 +9,7 @@ export class ClaudeCodeReasoner {
|
|
|
9
9
|
sessionId = null;
|
|
10
10
|
constructor(config, globalContext) {
|
|
11
11
|
this.config = config;
|
|
12
|
-
this.systemPrompt = buildSystemPrompt(globalContext);
|
|
12
|
+
this.systemPrompt = buildSystemPrompt(globalContext, config.promptTemplate);
|
|
13
13
|
}
|
|
14
14
|
async init() {
|
|
15
15
|
// verify claude is available
|
|
@@ -20,7 +20,7 @@ export class OpencodeReasoner {
|
|
|
20
20
|
static MAX_SESSION_MESSAGES = 7;
|
|
21
21
|
constructor(config, globalContext) {
|
|
22
22
|
this.config = config;
|
|
23
|
-
this.systemPrompt = buildSystemPrompt(globalContext);
|
|
23
|
+
this.systemPrompt = buildSystemPrompt(globalContext, config.promptTemplate);
|
|
24
24
|
}
|
|
25
25
|
async init() {
|
|
26
26
|
// try to connect to existing opencode server first, then start one
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface PromptTemplate {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
preamble: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function getAllPromptTemplates(): PromptTemplate[];
|
|
7
|
+
export declare function resolvePromptTemplate(name: string): PromptTemplate | undefined;
|
|
8
|
+
export declare function applyPromptTemplate(basePrompt: string, templateName: string): string;
|
|
9
|
+
export declare function formatPromptTemplateList(): string;
|
|
10
|
+
//# sourceMappingURL=prompt-templates.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// prompt-templates.ts — named system prompt strategies for the reasoner.
|
|
2
|
+
// the active template is set via config or /prompt-template command.
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { BOLD, DIM, GREEN, RESET } from "../colors.js";
|
|
7
|
+
const BUILTIN_TEMPLATES = [
|
|
8
|
+
{
|
|
9
|
+
name: "default",
|
|
10
|
+
description: "balanced supervisor — intervene only when needed",
|
|
11
|
+
preamble: "",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "hands-off",
|
|
15
|
+
description: "minimal intervention — only act on errors and permission prompts",
|
|
16
|
+
preamble: `IMPORTANT OVERRIDE: You are in hands-off mode. Be extremely conservative with interventions.
|
|
17
|
+
- Only send_input when a session has an ERROR or a PERMISSION prompt that needs clearing.
|
|
18
|
+
- Do NOT nudge idle sessions — they may be thinking or compiling.
|
|
19
|
+
- Do NOT send motivational or progress-check messages.
|
|
20
|
+
- Prefer "wait" in almost all cases. Let agents work autonomously.
|
|
21
|
+
`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "aggressive",
|
|
25
|
+
description: "proactive supervisor — nudge idle agents, push for progress",
|
|
26
|
+
preamble: `IMPORTANT OVERRIDE: You are in aggressive supervision mode. Be proactive.
|
|
27
|
+
- If a session has been idle for more than 60 seconds, nudge it with a prompt.
|
|
28
|
+
- If a session seems to be going in circles, redirect it with clearer instructions.
|
|
29
|
+
- Report progress frequently — every meaningful commit or test pass.
|
|
30
|
+
- Push agents toward completing their goals. Don't let them drift.
|
|
31
|
+
- Still avoid micromanaging specific code decisions.
|
|
32
|
+
`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "review-focused",
|
|
36
|
+
description: "PR/MR review cycle — focus on CI, reviewer feedback, rebasing",
|
|
37
|
+
preamble: `IMPORTANT OVERRIDE: You are in review-focused mode. Prioritize PR/MR lifecycle.
|
|
38
|
+
- Watch for CI pipeline failures and push fixes immediately.
|
|
39
|
+
- Monitor for reviewer comments and address them promptly.
|
|
40
|
+
- If a PR is approved, push to merge. If blocked, ping the reviewer.
|
|
41
|
+
- Prefer rebasing over merge commits. Keep commit history clean.
|
|
42
|
+
- Report progress for each PR status change (CI green, review received, merged).
|
|
43
|
+
`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "shipping",
|
|
47
|
+
description: "ship mode — focus on commits, pushes, and version bumps",
|
|
48
|
+
preamble: `IMPORTANT OVERRIDE: You are in shipping mode. Focus on getting code out the door.
|
|
49
|
+
- Push agents to commit and push frequently. Small atomic commits are better than big batches.
|
|
50
|
+
- If an agent has uncommitted work, nudge them to commit.
|
|
51
|
+
- Report progress for every push/commit.
|
|
52
|
+
- Don't let perfect be the enemy of good — ship working code, iterate later.
|
|
53
|
+
- Complete tasks as soon as their core goal is met. Don't gold-plate.
|
|
54
|
+
`,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const AOAOE_DIR = join(homedir(), ".aoaoe");
|
|
58
|
+
const USER_TEMPLATES_FILE = join(AOAOE_DIR, "prompt-templates.json");
|
|
59
|
+
function loadUserTemplates() {
|
|
60
|
+
try {
|
|
61
|
+
if (!existsSync(USER_TEMPLATES_FILE))
|
|
62
|
+
return [];
|
|
63
|
+
const raw = JSON.parse(readFileSync(USER_TEMPLATES_FILE, "utf-8"));
|
|
64
|
+
if (!Array.isArray(raw))
|
|
65
|
+
return [];
|
|
66
|
+
return raw.filter((t) => !!t && typeof t === "object" &&
|
|
67
|
+
typeof t.name === "string" &&
|
|
68
|
+
typeof t.preamble === "string").map((t) => ({
|
|
69
|
+
...t,
|
|
70
|
+
description: t.description || "(user-defined)",
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function getAllPromptTemplates() {
|
|
78
|
+
const user = loadUserTemplates();
|
|
79
|
+
const userNames = new Set(user.map((t) => t.name.toLowerCase()));
|
|
80
|
+
const builtins = BUILTIN_TEMPLATES.filter((t) => !userNames.has(t.name.toLowerCase()));
|
|
81
|
+
return [...builtins, ...user];
|
|
82
|
+
}
|
|
83
|
+
export function resolvePromptTemplate(name) {
|
|
84
|
+
const templates = getAllPromptTemplates();
|
|
85
|
+
const lower = name.toLowerCase();
|
|
86
|
+
return (templates.find((t) => t.name.toLowerCase() === lower) ??
|
|
87
|
+
templates.find((t) => t.name.toLowerCase().startsWith(lower)));
|
|
88
|
+
}
|
|
89
|
+
export function applyPromptTemplate(basePrompt, templateName) {
|
|
90
|
+
if (!templateName || templateName === "default")
|
|
91
|
+
return basePrompt;
|
|
92
|
+
const template = resolvePromptTemplate(templateName);
|
|
93
|
+
if (!template || !template.preamble)
|
|
94
|
+
return basePrompt;
|
|
95
|
+
return `${template.preamble}\n${basePrompt}`;
|
|
96
|
+
}
|
|
97
|
+
export function formatPromptTemplateList() {
|
|
98
|
+
const templates = getAllPromptTemplates();
|
|
99
|
+
const lines = [];
|
|
100
|
+
lines.push(` ${BOLD}available prompt templates:${RESET}`);
|
|
101
|
+
lines.push("");
|
|
102
|
+
for (const t of templates) {
|
|
103
|
+
const preview = t.preamble ? t.preamble.split("\n")[0].slice(0, 60) : "(no preamble — base prompt only)";
|
|
104
|
+
lines.push(` ${GREEN}${t.name}${RESET} — ${t.description}`);
|
|
105
|
+
lines.push(` ${DIM} ${preview}${t.preamble.length > 60 ? "..." : ""}${RESET}`);
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
lines.push(` ${DIM}custom templates: ${USER_TEMPLATES_FILE}${RESET}`);
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=prompt-templates.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Observation, TaskState } from "../types.js";
|
|
2
|
-
export declare function buildSystemPrompt(globalContext?: string): string;
|
|
2
|
+
export declare function buildSystemPrompt(globalContext?: string, promptTemplate?: string): string;
|
|
3
3
|
export interface SessionPolicyState {
|
|
4
4
|
sessionId: string;
|
|
5
5
|
lastOutputChangeAt: number;
|
package/dist/reasoner/prompt.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { goalToList } from "../types.js";
|
|
2
|
+
import { applyPromptTemplate } from "./prompt-templates.js";
|
|
2
3
|
// base system prompt -- global context appended at runtime via buildSystemPrompt()
|
|
3
4
|
const BASE_SYSTEM_PROMPT = `You are a supervisor managing multiple AI coding agents in an agent-of-empires (AoE) tmux session.
|
|
4
5
|
|
|
@@ -43,10 +44,11 @@ Rules:
|
|
|
43
44
|
- Use "complete_task" when a task's goal has been fully achieved and the agent has nothing left to do.
|
|
44
45
|
This will clean up the session. Only use when truly done, not just idle.`;
|
|
45
46
|
// build the full system prompt with global context appended
|
|
46
|
-
export function buildSystemPrompt(globalContext) {
|
|
47
|
+
export function buildSystemPrompt(globalContext, promptTemplate) {
|
|
48
|
+
const base = applyPromptTemplate(BASE_SYSTEM_PROMPT, promptTemplate ?? "default");
|
|
47
49
|
if (!globalContext)
|
|
48
|
-
return
|
|
49
|
-
return `${
|
|
50
|
+
return base;
|
|
51
|
+
return `${base}\n${globalContext}`;
|
|
50
52
|
}
|
|
51
53
|
// detect permission prompts in tmux output.
|
|
52
54
|
// the prompt-watcher module (prompt-watcher.ts) handles these reactively via
|
package/dist/task-cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadConfig } from "./config.js";
|
|
|
8
8
|
import { resolveProfiles } from "./tui.js";
|
|
9
9
|
import { loadTaskState, saveTaskState, formatTaskTable, syncTaskDefinitionsFromState, taskStateKey, resolveTaskRepoPath, TaskManager, loadTaskDefinitions, injectGoalToSession } from "./task-manager.js";
|
|
10
10
|
import { goalToList } from "./types.js";
|
|
11
|
+
import { resolveTemplate, formatTemplateList } from "./task-templates.js";
|
|
11
12
|
import { BOLD, DIM, GREEN, YELLOW, RED, RESET } from "./colors.js";
|
|
12
13
|
function buildProfileAwareAoeArgs(profile, tailArgs) {
|
|
13
14
|
if (!profile || profile === "default")
|
|
@@ -25,9 +26,15 @@ function getTaskProfiles() {
|
|
|
25
26
|
function taskCommandHelp(prefix = "aoaoe task") {
|
|
26
27
|
return [
|
|
27
28
|
`${prefix} list show tracked tasks`,
|
|
29
|
+
`${prefix} templates show available task templates`,
|
|
28
30
|
`${prefix} reconcile link/create sessions now`,
|
|
29
31
|
`${prefix} new <title> <path> create task + session`,
|
|
32
|
+
`${prefix} new <t> <p> --template roadmap create with template goal`,
|
|
30
33
|
`${prefix} start|stop <task> control task session`,
|
|
34
|
+
`${prefix} start-all start all paused/pending tasks`,
|
|
35
|
+
`${prefix} stop-all stop all active tasks`,
|
|
36
|
+
`${prefix} pause-all pause all active tasks (no session stop)`,
|
|
37
|
+
`${prefix} resume-all resume all paused tasks`,
|
|
31
38
|
`${prefix} edit <task> <goal> update task goal`,
|
|
32
39
|
`${prefix} rm <task> remove task + session`,
|
|
33
40
|
"step-in quick path: /task <session> :: <new instructions>",
|
|
@@ -331,6 +338,67 @@ export async function runTaskCli(argv) {
|
|
|
331
338
|
await taskStop(args[0]);
|
|
332
339
|
return;
|
|
333
340
|
}
|
|
341
|
+
case "start-all": {
|
|
342
|
+
const states = loadTaskState();
|
|
343
|
+
const tasks = [...states.values()].filter((t) => t.status === "paused" || t.status === "pending");
|
|
344
|
+
if (tasks.length === 0) {
|
|
345
|
+
console.log(`${DIM}no paused/pending tasks to start${RESET}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
for (const t of tasks)
|
|
349
|
+
await taskStart(t.sessionTitle);
|
|
350
|
+
console.log(`${GREEN}started ${tasks.length} task(s)${RESET}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
case "stop-all": {
|
|
354
|
+
const states = loadTaskState();
|
|
355
|
+
const tasks = [...states.values()].filter((t) => t.status === "active");
|
|
356
|
+
if (tasks.length === 0) {
|
|
357
|
+
console.log(`${DIM}no active tasks to stop${RESET}`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
for (const t of tasks)
|
|
361
|
+
await taskStop(t.sessionTitle);
|
|
362
|
+
console.log(`${YELLOW}stopped ${tasks.length} task(s)${RESET}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
case "pause-all": {
|
|
366
|
+
const states = loadTaskState();
|
|
367
|
+
let count = 0;
|
|
368
|
+
for (const [key, t] of states) {
|
|
369
|
+
if (t.status === "active") {
|
|
370
|
+
t.status = "paused";
|
|
371
|
+
states.set(key, t);
|
|
372
|
+
count++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (count === 0) {
|
|
376
|
+
console.log(`${DIM}no active tasks to pause${RESET}`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
saveTaskState(states);
|
|
380
|
+
console.log(`${YELLOW}paused ${count} task(s)${RESET}`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
case "resume-all": {
|
|
384
|
+
const states = loadTaskState();
|
|
385
|
+
let count = 0;
|
|
386
|
+
for (const [key, t] of states) {
|
|
387
|
+
if (t.status === "paused") {
|
|
388
|
+
t.status = "active";
|
|
389
|
+
t.stuckNudgeCount = 0;
|
|
390
|
+
states.set(key, t);
|
|
391
|
+
count++;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (count === 0) {
|
|
395
|
+
console.log(`${DIM}no paused tasks to resume${RESET}`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
saveTaskState(states);
|
|
399
|
+
console.log(`${GREEN}resumed ${count} task(s)${RESET}`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
334
402
|
case "edit": {
|
|
335
403
|
if (!args[0] || !args[1]) {
|
|
336
404
|
console.error(`usage: aoaoe task edit <name|id> <new goal text>`);
|
|
@@ -343,7 +411,7 @@ export async function runTaskCli(argv) {
|
|
|
343
411
|
case "create":
|
|
344
412
|
case "add": {
|
|
345
413
|
if (!args[0] || !args[1]) {
|
|
346
|
-
console.error(`usage: aoaoe task new <title> <path> [--tool opencode] [--mode new|existing|auto]`);
|
|
414
|
+
console.error(`usage: aoaoe task new <title> <path> [--template roadmap] [--tool opencode] [--mode new|existing|auto]`);
|
|
347
415
|
return;
|
|
348
416
|
}
|
|
349
417
|
const title = args[0];
|
|
@@ -360,7 +428,30 @@ export async function runTaskCli(argv) {
|
|
|
360
428
|
const profileIdx = args.indexOf("--profile");
|
|
361
429
|
if (profileIdx !== -1 && args[profileIdx + 1])
|
|
362
430
|
profile = args[profileIdx + 1];
|
|
363
|
-
|
|
431
|
+
// apply template if specified
|
|
432
|
+
const templateIdx = args.indexOf("--template");
|
|
433
|
+
let templateGoal;
|
|
434
|
+
if (templateIdx !== -1 && args[templateIdx + 1]) {
|
|
435
|
+
const tmpl = resolveTemplate(args[templateIdx + 1]);
|
|
436
|
+
if (!tmpl) {
|
|
437
|
+
console.error(`${RED}unknown template: ${args[templateIdx + 1]}${RESET}`);
|
|
438
|
+
console.log(formatTemplateList());
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
templateGoal = tmpl.goal;
|
|
442
|
+
if (tmpl.tool)
|
|
443
|
+
tool = tmpl.tool;
|
|
444
|
+
console.log(`${DIM}using template: ${tmpl.name}${RESET}`);
|
|
445
|
+
}
|
|
446
|
+
const ok = await taskNew(title, path, tool, mode, profile);
|
|
447
|
+
if (ok && templateGoal) {
|
|
448
|
+
await taskEdit(title, templateGoal);
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
case "templates":
|
|
453
|
+
case "template": {
|
|
454
|
+
console.log(formatTemplateList());
|
|
364
455
|
return;
|
|
365
456
|
}
|
|
366
457
|
case "reconcile": {
|
|
@@ -383,7 +474,7 @@ export async function runTaskCli(argv) {
|
|
|
383
474
|
}
|
|
384
475
|
default:
|
|
385
476
|
console.error(`unknown task subcommand: ${sub}`);
|
|
386
|
-
console.error(`usage: aoaoe task [list|start|stop|edit|new|rm|reconcile|help]`);
|
|
477
|
+
console.error(`usage: aoaoe task [list|start|stop|edit|new|rm|reconcile|templates|help]`);
|
|
387
478
|
}
|
|
388
479
|
}
|
|
389
480
|
/**
|
|
@@ -529,6 +620,9 @@ export async function handleTaskSlashCommand(args) {
|
|
|
529
620
|
const { created, linked, goalsInjected } = await tm.reconcileSessions();
|
|
530
621
|
return `reconciled tasks: +${created.length} created, +${linked.length} linked, +${goalsInjected.length} goals injected`;
|
|
531
622
|
}
|
|
532
|
-
|
|
623
|
+
if (sub === "templates" || sub === "template") {
|
|
624
|
+
return formatTemplateList();
|
|
625
|
+
}
|
|
626
|
+
return "usage: /task [list|start|stop|edit|new|rm|reconcile|templates|help] [args]";
|
|
533
627
|
}
|
|
534
628
|
//# sourceMappingURL=task-cli.js.map
|
package/dist/task-manager.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare class TaskManager {
|
|
|
31
31
|
goalsInjected: string[];
|
|
32
32
|
}>;
|
|
33
33
|
reportProgress(sessionTitle: string, summary: string): void;
|
|
34
|
+
recordStuckNudge(sessionTitle: string, maxNudges: number): boolean;
|
|
34
35
|
completeTask(sessionTitle: string, summary: string, cleanupSession?: boolean): Promise<void>;
|
|
35
36
|
private save;
|
|
36
37
|
}
|
package/dist/task-manager.js
CHANGED
|
@@ -460,12 +460,28 @@ export class TaskManager {
|
|
|
460
460
|
const entry = { at: Date.now(), summary };
|
|
461
461
|
task.progress.push(entry);
|
|
462
462
|
task.lastProgressAt = Date.now();
|
|
463
|
+
task.stuckNudgeCount = 0; // progress means it's not stuck anymore
|
|
463
464
|
// keep progress bounded (last 50 entries)
|
|
464
465
|
if (task.progress.length > 50) {
|
|
465
466
|
task.progress = task.progress.slice(-50);
|
|
466
467
|
}
|
|
467
468
|
this.save();
|
|
468
469
|
}
|
|
470
|
+
// record that the reasoner nudged a stuck session. returns true if the task should be auto-paused.
|
|
471
|
+
recordStuckNudge(sessionTitle, maxNudges) {
|
|
472
|
+
const task = this.getTaskForSession(sessionTitle);
|
|
473
|
+
if (!task || task.status !== "active")
|
|
474
|
+
return false;
|
|
475
|
+
task.stuckNudgeCount = (task.stuckNudgeCount ?? 0) + 1;
|
|
476
|
+
if (maxNudges > 0 && task.stuckNudgeCount >= maxNudges) {
|
|
477
|
+
task.status = "paused";
|
|
478
|
+
log(`auto-paused '${sessionTitle}' after ${task.stuckNudgeCount} stuck nudges (threshold: ${maxNudges})`);
|
|
479
|
+
this.save();
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
this.save();
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
469
485
|
// mark a task as completed, optionally clean up its session.
|
|
470
486
|
// if the task definition has continueOnRoadmap:true, resets with next backlog items instead.
|
|
471
487
|
async completeTask(sessionTitle, summary, cleanupSession = true) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface TaskTemplate {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
goal: string;
|
|
5
|
+
tool?: string;
|
|
6
|
+
continueOnRoadmap?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function getAllTemplates(): TaskTemplate[];
|
|
9
|
+
export declare function resolveTemplate(name: string): TaskTemplate | undefined;
|
|
10
|
+
export declare function formatTemplateList(): string;
|
|
11
|
+
//# sourceMappingURL=task-templates.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// task-templates.ts — built-in + user-defined task goal templates.
|
|
2
|
+
// templates are just named goal strings that can be applied via --template flag.
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { BOLD, DIM, GREEN, RESET } from "./colors.js";
|
|
7
|
+
// built-in templates derived from real usage patterns
|
|
8
|
+
const BUILTIN_TEMPLATES = [
|
|
9
|
+
{
|
|
10
|
+
name: "roadmap",
|
|
11
|
+
description: "grind through claude.md/AGENTS.md roadmap items, commit each improvement",
|
|
12
|
+
goal: "Find the roadmap (check claude.md, README.md, AGENTS.md). Pick the next item and implement it. Add new ideas to the roadmap as you or the human think of them. Commit and push after each self-contained improvement.",
|
|
13
|
+
continueOnRoadmap: true,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "roadmap-strict",
|
|
17
|
+
description: "roadmap grind with full tests required before push",
|
|
18
|
+
goal: "Find the roadmap (check claude.md, README.md, AGENTS.md). Pick the next item and implement it with full tests. Add new ideas to the roadmap as you or the human think of them. Commit and push after each self-contained improvement.",
|
|
19
|
+
continueOnRoadmap: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "pr-review",
|
|
23
|
+
description: "monitor open PRs/MRs, address review feedback, push fixes, ping reviewers",
|
|
24
|
+
goal: "Check for open PRs/MRs in this repo. Address any review feedback by pushing fixes. Monitor CI pipelines and fix failures. Ping reviewers when changes are ready for re-review. Update claude.md with PR status.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "bugfix",
|
|
28
|
+
description: "reproduce, fix, test, and commit a specific bug",
|
|
29
|
+
goal: "Investigate the reported bug. Reproduce it with a minimal test case. Fix the root cause. Add regression tests. Commit and push the fix.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "explore",
|
|
33
|
+
description: "read and understand the codebase, update claude.md with findings",
|
|
34
|
+
goal: "Explore the codebase. Read AGENTS.md and claude.md for context. Understand the architecture, key modules, and current state. Update claude.md with your findings and any issues you discover.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "ci-fix",
|
|
38
|
+
description: "fix failing CI pipelines — build errors, test failures, flaky tests",
|
|
39
|
+
goal: "Check CI pipeline status. Fix any build errors, test failures, or flaky tests. Push fixes and verify pipelines go green. Add retry config for transient infra failures if needed.",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
const AOAOE_DIR = join(homedir(), ".aoaoe");
|
|
43
|
+
const USER_TEMPLATES_FILE = join(AOAOE_DIR, "templates.json");
|
|
44
|
+
// load user-defined templates from ~/.aoaoe/templates.json (optional)
|
|
45
|
+
function loadUserTemplates() {
|
|
46
|
+
try {
|
|
47
|
+
if (!existsSync(USER_TEMPLATES_FILE))
|
|
48
|
+
return [];
|
|
49
|
+
const raw = JSON.parse(readFileSync(USER_TEMPLATES_FILE, "utf-8"));
|
|
50
|
+
if (!Array.isArray(raw))
|
|
51
|
+
return [];
|
|
52
|
+
return raw.filter((t) => !!t && typeof t === "object" &&
|
|
53
|
+
typeof t.name === "string" &&
|
|
54
|
+
typeof t.goal === "string").map((t) => ({
|
|
55
|
+
...t,
|
|
56
|
+
description: t.description || "(user-defined)",
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// get all templates — user templates override builtins with the same name
|
|
64
|
+
export function getAllTemplates() {
|
|
65
|
+
const user = loadUserTemplates();
|
|
66
|
+
const userNames = new Set(user.map((t) => t.name.toLowerCase()));
|
|
67
|
+
const builtins = BUILTIN_TEMPLATES.filter((t) => !userNames.has(t.name.toLowerCase()));
|
|
68
|
+
return [...builtins, ...user];
|
|
69
|
+
}
|
|
70
|
+
// resolve a template by name (case-insensitive, prefix match)
|
|
71
|
+
export function resolveTemplate(name) {
|
|
72
|
+
const templates = getAllTemplates();
|
|
73
|
+
const lower = name.toLowerCase();
|
|
74
|
+
return (templates.find((t) => t.name.toLowerCase() === lower) ??
|
|
75
|
+
templates.find((t) => t.name.toLowerCase().startsWith(lower)));
|
|
76
|
+
}
|
|
77
|
+
// format template list for display
|
|
78
|
+
export function formatTemplateList() {
|
|
79
|
+
const templates = getAllTemplates();
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push(` ${BOLD}available task templates:${RESET}`);
|
|
82
|
+
lines.push("");
|
|
83
|
+
for (const t of templates) {
|
|
84
|
+
const goalPreview = t.goal.length > 70 ? t.goal.slice(0, 67) + "..." : t.goal;
|
|
85
|
+
lines.push(` ${GREEN}${t.name}${RESET} — ${t.description}`);
|
|
86
|
+
lines.push(` ${DIM} goal: ${goalPreview}${RESET}`);
|
|
87
|
+
if (t.tool)
|
|
88
|
+
lines.push(` ${DIM} tool: ${t.tool}${RESET}`);
|
|
89
|
+
if (t.continueOnRoadmap)
|
|
90
|
+
lines.push(` ${DIM} continueOnRoadmap: true${RESET}`);
|
|
91
|
+
lines.push("");
|
|
92
|
+
}
|
|
93
|
+
lines.push(` ${DIM}custom templates: ${USER_TEMPLATES_FILE}${RESET}`);
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=task-templates.js.map
|
package/dist/tui.d.ts
CHANGED
|
@@ -579,6 +579,10 @@ export declare class TUI {
|
|
|
579
579
|
isPinned(id: string): boolean;
|
|
580
580
|
/** Return count of pinned sessions. */
|
|
581
581
|
getPinnedCount(): number;
|
|
582
|
+
/** Return titles of all pinned sessions. */
|
|
583
|
+
getPinnedTitles(): string[];
|
|
584
|
+
/** Load a pin preset: clear current pins, pin sessions matching given titles. Returns count pinned. */
|
|
585
|
+
loadPinPreset(titles: string[]): number;
|
|
582
586
|
/** Enable or disable focus mode. When focused, only pinned sessions are visible. */
|
|
583
587
|
setFocus(enabled: boolean): void;
|
|
584
588
|
/** Return whether focus mode is enabled. */
|
package/dist/tui.js
CHANGED
|
@@ -971,6 +971,8 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
971
971
|
"/supervisor",
|
|
972
972
|
"/runbook",
|
|
973
973
|
"/incident",
|
|
974
|
+
"/pin-save", "/pin-load", "/pin-delete", "/pin-presets",
|
|
975
|
+
"/prompt-template", "/health",
|
|
974
976
|
"/explain", "/verbose", "/clear", "/view", "/back", "/sort", "/compact",
|
|
975
977
|
"/pin", "/bell", "/focus", "/mute", "/unmute-all", "/filter", "/who",
|
|
976
978
|
"/uptime", "/auto-pin", "/note", "/notes", "/clip", "/diff", "/mark",
|
|
@@ -1442,6 +1444,26 @@ export class TUI {
|
|
|
1442
1444
|
getPinnedCount() {
|
|
1443
1445
|
return this.pinnedIds.size;
|
|
1444
1446
|
}
|
|
1447
|
+
/** Return titles of all pinned sessions. */
|
|
1448
|
+
getPinnedTitles() {
|
|
1449
|
+
return this.sessions.filter((s) => this.pinnedIds.has(s.id)).map((s) => s.title);
|
|
1450
|
+
}
|
|
1451
|
+
/** Load a pin preset: clear current pins, pin sessions matching given titles. Returns count pinned. */
|
|
1452
|
+
loadPinPreset(titles) {
|
|
1453
|
+
this.pinnedIds.clear();
|
|
1454
|
+
const lowerTitles = new Set(titles.map((t) => t.toLowerCase()));
|
|
1455
|
+
let count = 0;
|
|
1456
|
+
for (const s of this.sessions) {
|
|
1457
|
+
if (lowerTitles.has(s.title.toLowerCase())) {
|
|
1458
|
+
this.pinnedIds.add(s.id);
|
|
1459
|
+
count++;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
|
|
1463
|
+
if (this.active)
|
|
1464
|
+
this.paintAll();
|
|
1465
|
+
return count;
|
|
1466
|
+
}
|
|
1445
1467
|
/** Enable or disable focus mode. When focused, only pinned sessions are visible. */
|
|
1446
1468
|
setFocus(enabled) {
|
|
1447
1469
|
if (enabled === this.focusMode)
|
package/dist/types.d.ts
CHANGED
|
@@ -111,12 +111,14 @@ export interface AoaoeConfig {
|
|
|
111
111
|
actionCooldownMs?: number;
|
|
112
112
|
userActivityThresholdMs?: number;
|
|
113
113
|
allowDestructive?: boolean;
|
|
114
|
+
maxStuckNudgesBeforePause?: number;
|
|
114
115
|
};
|
|
115
116
|
contextFiles: string[];
|
|
116
117
|
sessionDirs: Record<string, string>;
|
|
117
118
|
protectedSessions: string[];
|
|
118
119
|
captureLinesCount: number;
|
|
119
120
|
reasonIntervalMs: number;
|
|
121
|
+
promptTemplate?: string;
|
|
120
122
|
verbose: boolean;
|
|
121
123
|
dryRun: boolean;
|
|
122
124
|
observe: boolean;
|
|
@@ -200,6 +202,7 @@ export interface TaskState {
|
|
|
200
202
|
createdAt?: number;
|
|
201
203
|
lastProgressAt?: number;
|
|
202
204
|
completedAt?: number;
|
|
205
|
+
stuckNudgeCount?: number;
|
|
203
206
|
progress: TaskProgress[];
|
|
204
207
|
}
|
|
205
208
|
export declare function toTaskState(raw: unknown): TaskState | null;
|
package/dist/types.js
CHANGED
|
@@ -91,6 +91,7 @@ export function toTaskState(raw) {
|
|
|
91
91
|
goal: r.goal,
|
|
92
92
|
status: r.status,
|
|
93
93
|
dependsOn: Array.isArray(r.dependsOn) ? r.dependsOn.filter((d) => typeof d === "string" && d.length > 0) : undefined,
|
|
94
|
+
stuckNudgeCount: typeof r.stuckNudgeCount === "number" ? r.stuckNudgeCount : undefined,
|
|
94
95
|
sessionId: typeof r.sessionId === "string" ? r.sessionId : undefined,
|
|
95
96
|
createdAt: typeof r.createdAt === "number" ? r.createdAt : undefined,
|
|
96
97
|
lastProgressAt: typeof r.lastProgressAt === "number" ? r.lastProgressAt : undefined,
|