@staix/agent-hub 0.12.29 → 0.12.30
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/CHANGELOG.md +18 -0
- package/README.md +79 -219
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +1 -1
- package/docs/agent-notes/bus.md +3 -1
- package/docs/agent-notes/daemon.md +10 -0
- package/docs/agent-notes/local-worker.md +2 -2
- package/docs/agent-notes/tasks.md +9 -0
- package/docs/agent-notes/tests.md +1 -1
- package/docs/memory.md +70 -152
- package/docs/operations.md +199 -14
- package/docs/plans/2026-10-16-amem-failure-registry.md +101 -0
- package/docs/plans/2026-10-16-amem-p0-baseline.md +110 -0
- package/docs/plans/2026-10-16-amem-p2-read-tools.md +28 -0
- package/docs/plans/amem-cases.json +482 -0
- package/docs/quickstart.md +9 -2
- package/docs/security.md +13 -1
- package/docs/smoke.md +11 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +105 -4
- package/docs/verification/2026-10-17-0.12.30.md +70 -0
- package/docs/verified.json +13 -13
- package/package.json +5 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +64 -44
- package/src/cli/console-state.ts +92 -20
- package/src/cli/console.ts +22 -5
- package/src/cli/help.ts +3 -2
- package/src/cli/identity.ts +1 -1
- package/src/cli/main.js +2 -2
- package/src/cli/main.ts +36 -14
- package/src/cli/memory.ts +87 -187
- package/src/cli/output.ts +6 -0
- package/src/cli/recovery-package.ts +59 -10
- package/src/cli/upgrade-runtime.ts +54 -17
- package/src/cli/upgrade.ts +8 -0
- package/src/hub/board.ts +2 -1
- package/src/hub/bus.ts +33 -12
- package/src/hub/daemon.ts +134 -27
- package/src/hub/envelope.ts +30 -0
- package/src/hub/git.ts +2 -38
- package/src/hub/hub-tools.ts +5 -1
- package/src/hub/manager.ts +1 -1
- package/src/hub/memory-candidates.ts +103 -0
- package/src/hub/memory-fail.ts +25 -0
- package/src/hub/memory-observations.ts +47 -0
- package/src/hub/memory-plan-guard.ts +33 -0
- package/src/hub/memory-policy.ts +42 -0
- package/src/hub/memory-profile-check.ts +170 -0
- package/src/hub/memory-profile.ts +192 -0
- package/src/hub/memory-staleness.ts +55 -0
- package/src/hub/path-policy.ts +8 -0
- package/src/hub/project.ts +4 -37
- package/src/hub/read-tools.ts +127 -0
- package/src/hub/tasks.ts +201 -41
- package/src/local/deny.ts +4 -17
- package/src/memory/brief.ts +8 -5
- package/src/memory/capture.ts +2 -1
- package/src/memory/recall.ts +50 -110
- package/src/pi/process-signature.ts +2 -42
- package/src/memory/import.ts +0 -90
- package/src/memory/store.ts +0 -805
- package/src/memory/sync.ts +0 -145
package/src/cli/console-state.ts
CHANGED
|
@@ -29,7 +29,9 @@ export interface ConsoleState {
|
|
|
29
29
|
approvals: Approval[]; events: ConsoleEvent[]; peers: Record<string, any>; budget: Record<string, any>;
|
|
30
30
|
tasks: any[]; tasksKnown?: boolean; taskCounts?: Record<string, number>; queue: any[]; detail?: Detail; detailOffset: number; help: boolean; notice: string; noticeAt?: number; noticeTone?: Tone;
|
|
31
31
|
peerFilter?: string; kindFilter?: string; project?: string;
|
|
32
|
-
|
|
32
|
+
taskSelection?: number; queueSelection?: string;
|
|
33
|
+
actionLock?: { prefix: string[]; id: string; revision: number; state: string; owner?: string; reserved?: string; reviewer?: string; waiting?: boolean };
|
|
34
|
+
confirm?: { type: "permission"; id: string; option: string; peer: string } | { type: "command"; args: string[]; prompt?: string } | { type: "permission_default"; peer: string; source: string };
|
|
33
35
|
optionChoice?: string;
|
|
34
36
|
modeChoice?: string;
|
|
35
37
|
permissionDefaults: PermissionDefault[];
|
|
@@ -319,8 +321,8 @@ const KEYS: [string, string[]][] = [
|
|
|
319
321
|
["Panels", ["1-5 panel", "Left/Right panel", "j/k or Up/Down move", "Enter view", "j/k scroll a view"]],
|
|
320
322
|
["Approvals", ["a allow (then y)", "d deny", "v view", "[ ] select"]],
|
|
321
323
|
["Peers", ["p pause", "r resume", "m permission mode"]],
|
|
322
|
-
["Tasks", ["a assign (then y)", "r review"]],
|
|
323
|
-
["Queue", ["r resolve (reason, then y)"]],
|
|
324
|
+
["Tasks", ["a assign/reviewer (then y)", "u unassign (then y)", "r review (in review)"]],
|
|
325
|
+
["Queue", ["r resolve (reason, then y)", "queued=discard, needs_review=retry"]],
|
|
324
326
|
["Events", ["f peer filter", "g kind filter"]],
|
|
325
327
|
];
|
|
326
328
|
/** The `?` key table: one group per panel, its keys kept whole and packed into the width after the group column. */
|
|
@@ -372,7 +374,7 @@ export function parseConsoleCommand(input: string): string[] {
|
|
|
372
374
|
}
|
|
373
375
|
export function commandNeedsConfirmation(args: string[]): boolean {
|
|
374
376
|
const positional = args.filter(arg => arg !== "--yes");
|
|
375
|
-
return (positional[0] === "permission" && positional[2] === "never-ask") || (args[0] === "task" && args[1]
|
|
377
|
+
return (positional[0] === "permission" && positional[2] === "never-ask") || (args[0] === "task" && ["assign", "unassign"].includes(args[1] ?? "")) || (args[0] === "queue" && args[1] === "resolve") || (args[0] === "permit" && args[2] !== "deny");
|
|
376
378
|
}
|
|
377
379
|
export function panelRows(s: ConsoleState): any[] {
|
|
378
380
|
if (s.panel === 1) return Object.entries(s.peers).map(([id, peer]) => ({ id, ...peer, budget: s.budget[id] }));
|
|
@@ -385,8 +387,38 @@ export function panelRows(s: ConsoleState): any[] {
|
|
|
385
387
|
return s.events.filter(e => (!s.peerFilter || e.peer === s.peerFilter) && (!s.kindFilter || e.kind === s.kindFilter));
|
|
386
388
|
}
|
|
387
389
|
export const selectedApproval = (s: ConsoleState) => s.approvals.find(a => a.id === s.approvalId);
|
|
388
|
-
/** The selected row:
|
|
389
|
-
const selection = (s: ConsoleState) =>
|
|
390
|
+
/** The selected row: approvals and refreshed Tasks/Queue rows are bound by id, never by a replacement row. */
|
|
391
|
+
const selection = (s: ConsoleState) => {
|
|
392
|
+
if (s.panel === 2) return s.approvals.findIndex(a => a.id === s.approvalId);
|
|
393
|
+
if (s.panel === 3 && s.taskSelection !== undefined) return panelRows(s).findIndex(row => row.id === s.taskSelection);
|
|
394
|
+
if (s.panel === 4 && s.queueSelection !== undefined) return panelRows(s).findIndex(row => row.id === s.queueSelection);
|
|
395
|
+
return s.selection;
|
|
396
|
+
};
|
|
397
|
+
export const selectedPanelRow = (s: ConsoleState): any | undefined => panelRows(s)[selection(s)];
|
|
398
|
+
export function captureConsoleSelection(s: ConsoleState, byIndex = false): void {
|
|
399
|
+
const row = byIndex ? panelRows(s)[s.selection] : selectedPanelRow(s);
|
|
400
|
+
if (s.panel === 3 && row) s.taskSelection = Number(row.id);
|
|
401
|
+
if (s.panel === 4 && row) s.queueSelection = String(row.id);
|
|
402
|
+
}
|
|
403
|
+
const commandFlag = (args: string[], name: string) => { const at = args.indexOf(name); return at < 0 ? undefined : args[at + 1]; };
|
|
404
|
+
function validateLockedCommand(lock: NonNullable<ConsoleState["actionLock"]>, args: string[]): void {
|
|
405
|
+
const revisions = args.flatMap((arg, index) => arg === "--revision" ? [args[index + 1]] : []);
|
|
406
|
+
if (args[0] !== lock.prefix[0] || args[1] !== lock.prefix[1] || args[2] !== lock.id || revisions.length !== 1 || revisions[0] !== String(lock.revision)) {
|
|
407
|
+
throw new Error("the selected id and revision cannot be edited; start again from the row");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function lockedCommandPrompt(lock: NonNullable<ConsoleState["actionLock"]>, args: string[]): string {
|
|
411
|
+
if (lock.prefix[0] === "queue") {
|
|
412
|
+
const action = commandFlag(args, "--action") ?? "resolve";
|
|
413
|
+
return `${action} delivery ${lock.id} at revision ${lock.revision}${action === "completed" ? "; this asserts external effects" : action === "discard" ? "; no task or native turn is cancelled" : ""}`;
|
|
414
|
+
}
|
|
415
|
+
const role = commandFlag(args, "--role") ?? "owner";
|
|
416
|
+
if (lock.prefix[1] === "assign") return `assign ${role} on task #${lock.id} at revision ${lock.revision}${lock.waiting && role === "owner" ? " as a reservation until dependencies approve" : ""}`;
|
|
417
|
+
const affected = role === "owner" ? [lock.owner, lock.reserved].filter(Boolean).join(" and ") : lock.reviewer ?? "none";
|
|
418
|
+
const resulting = role === "owner" ? "proposed" : lock.state;
|
|
419
|
+
const guidance = role === "reviewer" && lock.state !== "in_review" ? "; later completion without a reviewer may approve directly" : lock.state === "in_review" ? `; use ahub review ${lock.id}` : "";
|
|
420
|
+
return `unassign ${role} ${affected} on task #${lock.id} at revision ${lock.revision} -> ${resulting}${guidance}; native work and uncertain deliveries are not stopped or resolved`;
|
|
421
|
+
}
|
|
390
422
|
export function reduceConsole(state: ConsoleState, key: string, now = Date.now()): { state: ConsoleState; effects: ConsoleEffect[] } {
|
|
391
423
|
const attentionKeyBlocked = !!(state.confirm || state.optionChoice || state.modeChoice || state.help || state.editing || state.input);
|
|
392
424
|
// A notice answers the key before this one; one that pruning raises now stays.
|
|
@@ -427,24 +459,29 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
427
459
|
}
|
|
428
460
|
if (s.help && s.mode === "panels" && !["?", "\x1b", "q"].includes(key)) return done(); // the key table is modal
|
|
429
461
|
if (s.editing || s.input) {
|
|
430
|
-
if (key === "\x1b") { s.input = ""; s.editing = false; }
|
|
462
|
+
if (key === "\x1b") { s.input = ""; s.editing = false; s.actionLock = undefined; }
|
|
431
463
|
else if (key === "\x7f" || key === "\b") s.input = Array.from(s.input).slice(0, -1).join("");
|
|
432
464
|
else if (key === "\x15") s.input = "";
|
|
433
465
|
else if (key === "\x1b[A" || key === "\x1b[B") {
|
|
434
466
|
s.historyIndex = Math.max(0, Math.min(s.history.length, s.historyIndex + (key === "\x1b[A" ? -1 : 1)));
|
|
435
467
|
s.input = s.history[s.historyIndex] ?? "";
|
|
436
468
|
} else if (key === "\r" || key === "\n") {
|
|
437
|
-
const input = s.input; s.input = ""; s.editing = false;
|
|
469
|
+
const input = s.input, lock = s.actionLock; s.input = ""; s.editing = false; s.actionLock = undefined;
|
|
438
470
|
if (!input.trim()) return done();
|
|
439
471
|
try {
|
|
440
472
|
const parsed = parseConsoleCommand(input);
|
|
441
473
|
const args = parsed[0] === "permission" ? [...parsed.filter(arg => arg !== "--yes"), ...(parsed.includes("--yes") ? ["--yes"] : [])] : parsed;
|
|
442
474
|
if (args[0] === "queue" && args[1] === "resolve") {
|
|
443
|
-
const reason = args.indexOf("--reason");
|
|
475
|
+
const reason = args.indexOf("--reason"), action = commandFlag(args, "--action");
|
|
444
476
|
if (reason < 0 || !args[reason + 1]?.trim()) throw new Error("queue resolve requires --reason");
|
|
477
|
+
if (!["completed", "retry", "discard"].includes(action ?? "")) throw new Error("queue resolve requires --action completed|retry|discard");
|
|
478
|
+
}
|
|
479
|
+
if (lock) {
|
|
480
|
+
validateLockedCommand(lock, args);
|
|
481
|
+
if (lock.prefix[0] === "task" && !["owner", "reviewer"].includes(commandFlag(args, "--role") ?? "owner")) throw new Error("role must be owner or reviewer");
|
|
445
482
|
}
|
|
446
483
|
s.history = [...s.history, input].slice(-100); s.historyIndex = s.history.length;
|
|
447
|
-
if (commandNeedsConfirmation(args)) s.confirm = { type: "command", args };
|
|
484
|
+
if (commandNeedsConfirmation(args)) s.confirm = { type: "command", args, ...(lock ? { prompt: lockedCommandPrompt(lock, args) } : {}) };
|
|
448
485
|
else effects.push({ type: "command", args });
|
|
449
486
|
} catch (error) { notify(s, String((error as Error).message), now); }
|
|
450
487
|
} else if (!key.startsWith("\x1b")) s.input += terminalText(key).replace(/\n/g, "");
|
|
@@ -482,7 +519,7 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
482
519
|
const selected = rows.findIndex(row => row.id === item.id);
|
|
483
520
|
if (selected >= 0) { s.panel = 3; s.selection = selected; navigated = true; }
|
|
484
521
|
}
|
|
485
|
-
if (navigated) { s.detail = undefined; s.requestDetail = undefined; }
|
|
522
|
+
if (navigated) { captureConsoleSelection(s, true); s.detail = undefined; s.requestDetail = undefined; }
|
|
486
523
|
else {
|
|
487
524
|
const command = attentionCommand(item);
|
|
488
525
|
const text = `${item.kind}${item.peer ? ` ${item.peer}` : ""}${item.id ? ` ${item.id}` : ""}: ${command}`;
|
|
@@ -493,7 +530,7 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
493
530
|
}
|
|
494
531
|
if (key === "\t") { s.mode = s.mode === "panels" ? "stream" : "panels"; s.detail = undefined; s.requestDetail = undefined; return done(); }
|
|
495
532
|
if (key === ":") { s.editing = true; return done(); }
|
|
496
|
-
const switchPanel = (panel: number) => { s.panel = panel; s.selection = 0; s.detail = undefined; s.requestDetail = undefined; return done(); };
|
|
533
|
+
const switchPanel = (panel: number) => { s.panel = panel; s.selection = 0; s.taskSelection = undefined; s.queueSelection = undefined; s.detail = undefined; s.requestDetail = undefined; return done(); };
|
|
497
534
|
if (s.mode === "panels" && /^[1-5]$/.test(key) && !s.optionChoice) return switchPanel(Number(key));
|
|
498
535
|
// Right and Left are the digit of the next or previous tab, wrapping at both ends (#309); the same guards as the
|
|
499
536
|
// digits apply (the confirm, mode-choice, key-table and editing branches above returned already).
|
|
@@ -508,6 +545,7 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
508
545
|
if (s.detail !== undefined || s.requestDetail) { s.detailOffset = Math.max(0, s.detailOffset + (["j", "\x1b[B"].includes(key) ? 1 : -1)); return done(); }
|
|
509
546
|
s.selection = Math.max(0, Math.min(panelRows(s).length - 1, selection(s) + (["j", "\x1b[B"].includes(key) ? 1 : -1)));
|
|
510
547
|
if (s.panel === 2) s.approvalId = s.approvals[s.selection]?.id;
|
|
548
|
+
captureConsoleSelection(s, true);
|
|
511
549
|
return done();
|
|
512
550
|
}
|
|
513
551
|
if (key === "[" || key === "]") {
|
|
@@ -537,10 +575,11 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
537
575
|
if (key === "v" && s.mode === "stream") { effects.push({ type: "print", text: permissionText(approval), kind: "permission", tone: "attention" }); return done(); }
|
|
538
576
|
if (key === "v") { s.requestDetail = approval.id; s.detailOffset = 0; return done(); }
|
|
539
577
|
}
|
|
540
|
-
const row =
|
|
578
|
+
const row = selectedPanelRow(s);
|
|
541
579
|
// With a detail open the keys act on the item it shows (its id), never on what a refresh moved under the selection;
|
|
542
580
|
// a detail that is only a message names no item.
|
|
543
|
-
const
|
|
581
|
+
const selected = s.detail === undefined ? row : typeof s.detail === "object" ? s.detail : undefined;
|
|
582
|
+
const item = selected?.id;
|
|
544
583
|
if (s.mode === "panels" && row && s.detail === undefined && (key === "\r" || key === "\n")) {
|
|
545
584
|
if (s.panel === 3 || s.panel === 4) effects.push({ type: "show", panel: s.panel, id: String(row.id) });
|
|
546
585
|
else if (s.panel === 2) { s.requestDetail = row.id; s.detailOffset = 0; } // the row is the selected request
|
|
@@ -557,9 +596,28 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
557
596
|
return done();
|
|
558
597
|
}
|
|
559
598
|
const prefill = s.panel === 1 && key === "p" ? `pause ${item}` : s.panel === 1 && key === "r" ? `resume ${item}` :
|
|
560
|
-
s.panel === 3 && key === "
|
|
561
|
-
s.panel === 4 && key === "r" ? `queue resolve ${item} --action retry --reason ` : undefined;
|
|
599
|
+
s.panel === 3 && key === "r" && selected.state === "in_review" ? `review ${item} ` : undefined;
|
|
562
600
|
if (prefill) { s.input = prefill; s.editing = true; return done(); }
|
|
601
|
+
if (s.panel === 3 && (key === "a" || key === "u")) {
|
|
602
|
+
if (!["proposed", "in_progress", "in_review", "changes_requested"].includes(selected.state)) return done();
|
|
603
|
+
const revision = selected.flowRevision, role = selected.state === "in_review" || key === "u" && !selected.owner && !selected.reserved && selected.reviewer ? "reviewer" : "owner";
|
|
604
|
+
if (!Number.isSafeInteger(revision) || revision < 0) { notify(s, "task row actions need flowRevision; upgrade the hub", now); return done(); }
|
|
605
|
+
if (key === "u" && (role === "owner" && !selected.owner && !selected.reserved || role === "reviewer" && !selected.reviewer)) { notify(s, `task #${item}: no ${role} to unassign`, now); return done(); }
|
|
606
|
+
const listed = s.tasks.find(task => task.id === item), ready = listed?.ready ?? selected.ready;
|
|
607
|
+
const waiting = selected.state === "proposed" && ready !== true && Array.isArray(selected.deps) && selected.deps.length > 0;
|
|
608
|
+
s.actionLock = { prefix: ["task", key === "a" ? "assign" : "unassign"], id: String(item), revision, state: String(selected.state), ...(selected.owner ? { owner: String(selected.owner) } : {}), ...(selected.reserved ? { reserved: String(selected.reserved) } : {}), ...(selected.reviewer ? { reviewer: String(selected.reviewer) } : {}), waiting };
|
|
609
|
+
s.input = key === "a" ? `task assign ${item} ${role === "reviewer" ? "--role reviewer " : ""}--revision ${revision} ` : `task unassign ${item} --role ${role} --revision ${revision}`;
|
|
610
|
+
s.editing = true; return done();
|
|
611
|
+
}
|
|
612
|
+
if (s.panel === 4 && key === "r") {
|
|
613
|
+
if (!["queued", "needs_review"].includes(selected.state)) return done();
|
|
614
|
+
const revision = selected.revision;
|
|
615
|
+
if (!Number.isSafeInteger(revision) || revision < 0) { notify(s, "queue row actions need a delivery revision; upgrade the hub", now); return done(); }
|
|
616
|
+
const action = selected.state === "queued" ? "discard" : "retry";
|
|
617
|
+
s.actionLock = { prefix: ["queue", "resolve"], id: String(item), revision, state: String(selected.state) };
|
|
618
|
+
s.input = `queue resolve ${item} --action ${action} --revision ${revision} --reason `;
|
|
619
|
+
s.editing = true; return done();
|
|
620
|
+
}
|
|
563
621
|
}
|
|
564
622
|
if (s.mode === "panels" && s.panel === 5 && (key === "f" || key === "g")) {
|
|
565
623
|
const field = key === "f" ? "peer" : "kind"; const current = key === "f" ? s.peerFilter : s.kindFilter;
|
|
@@ -646,6 +704,21 @@ export function table(head: string[], rows: Span[][], columns: number, explicitW
|
|
|
646
704
|
return { ...cell, text: text + " ".repeat(Math.max(0, widths[i]! - Bun.stringWidth(text)) + (i === head.length - 1 ? 0 : 2)) };
|
|
647
705
|
}));
|
|
648
706
|
}
|
|
707
|
+
function taskHints(row: any): string[] {
|
|
708
|
+
if (!["proposed", "in_progress", "in_review", "changes_requested"].includes(row.state)) return [];
|
|
709
|
+
const review = row.state === "in_review";
|
|
710
|
+
const hints = [review ? "a reviewer" : "a assign"];
|
|
711
|
+
if (review ? row.reviewer : row.owner || row.reserved) hints.push("u unassign");
|
|
712
|
+
if (review) hints.push("r review");
|
|
713
|
+
return hints;
|
|
714
|
+
}
|
|
715
|
+
function panelContext(s: ConsoleState, row: any): string[] {
|
|
716
|
+
if (!row) return [];
|
|
717
|
+
if (s.panel === 1) return ["p pause", "r resume", "m permission mode"];
|
|
718
|
+
if (s.panel === 3) return taskHints(row);
|
|
719
|
+
if (s.panel === 4) return [row.state === "queued" ? "r discard" : "r resolve"];
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
649
722
|
/** The footer's key hint: only keys that act in this mode, panel and state. */
|
|
650
723
|
function hint(s: ConsoleState, columns: number): string {
|
|
651
724
|
const pending = selectedApproval(s) ? ["a allow", "d deny"] : [];
|
|
@@ -653,9 +726,8 @@ function hint(s: ConsoleState, columns: number): string {
|
|
|
653
726
|
if (s.help) return "? or Esc close q quit";
|
|
654
727
|
const approvals = s.panel === 2 ? pending : [];
|
|
655
728
|
if (s.detail !== undefined || s.requestDetail) return [...approvals, "j/k scroll", "Esc back", "? keys", "q quit"].join(" ");
|
|
656
|
-
const rows = panelRows(s); const row =
|
|
657
|
-
const
|
|
658
|
-
const text = [...approvals, ...context, ...(s.panel === 5 ? ["f peer", "g kind"] : []), ...(rows.length ? ["j/k move"] : []), ...(row ? ["Enter view"] : []), "? keys", ": command", "Tab stream", "q quit"].join(" ");
|
|
729
|
+
const rows = panelRows(s); const row = selectedPanelRow(s);
|
|
730
|
+
const text = [...approvals, ...panelContext(s, row), ...(s.panel === 5 ? ["f peer", "g kind"] : []), ...(rows.length ? ["j/k move"] : []), ...(row ? ["Enter view"] : []), "? keys", ": command", "Tab stream", "q quit"].join(" ");
|
|
659
731
|
return s.panel === 1 && Bun.stringWidth(text) > columns ? text.replace("m permission mode", "m mode").replace("j/k move", "j/k").replace(": command", ": cmd") : text;
|
|
660
732
|
}
|
|
661
733
|
/** The project, then the tabs: the active one in brackets, pending approvals and held deliveries counted. */
|
|
@@ -705,7 +777,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
705
777
|
const permission = selectedApproval(s);
|
|
706
778
|
let prompt = s.editing ? `: ${s.input}` : hint(s, columns);
|
|
707
779
|
if (s.confirm?.type === "permission") prompt = `allow ${quoted(s.confirm.option)} for ${s.confirm.peer} (request ${s.confirm.id})? y/N`;
|
|
708
|
-
if (s.confirm?.type === "command") prompt = `${s.confirm.args.join(" ")}? y/N`;
|
|
780
|
+
if (s.confirm?.type === "command") prompt = `${s.confirm.prompt ?? s.confirm.args.join(" ")}? y/N`;
|
|
709
781
|
if (s.modeChoice) prompt = `permission ${s.modeChoice}: 1 ask 2 ask-when-needed 3 never-ask Esc cancel`;
|
|
710
782
|
const choice = s.optionChoice ? s.approvals.find(a => a.id === s.optionChoice) : undefined;
|
|
711
783
|
if (choice) prompt = `allow request ${choice.id} (${choice.peer}) with: ${allowOptions(choice).map((o, i) => `${i + 1} ${quoted(o.name)}`).join(" ")} Esc cancel`;
|
package/src/cli/console.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
4
4
|
import type { ControlClient } from "../hub/control-client.ts";
|
|
5
5
|
import { contextLine } from "./status-lines.ts";
|
|
6
6
|
import { renderTailEvent } from "./tail-render.ts";
|
|
7
|
-
import { syncPermissionDefaults, permissionDefaultText, approvalFrom, initialConsoleState, keyTable, notify, paint,
|
|
7
|
+
import { syncPermissionDefaults, permissionDefaultText, approvalFrom, initialConsoleState, keyTable, notify, paint, permissionText, plural, pruneApprovals, reduceConsole, renderConsoleLines, resolveColor, stateTone, streamTokens, wrapStreamTokens, terminalText, wrap, captureConsoleSelection, selectedPanelRow } from "./console-state.ts";
|
|
8
8
|
import type { ConsoleEffect, ConsoleEvent, Detail, Tone } from "./console-state.ts";
|
|
9
9
|
import type { BusEvent } from "../hub/bus.ts";
|
|
10
10
|
import { consoleNeeds, attentionFacts } from "./attention.ts";
|
|
@@ -82,7 +82,7 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
82
82
|
let lastAct = 0; let lastBell = -Infinity;
|
|
83
83
|
let columns = options.columns ?? terminal.columns; let rows = options.rows ?? terminal.rows;
|
|
84
84
|
let active = true; let inAlternate = false; let plain = !terminal.isTTY || columns < 80 || rows < 24;
|
|
85
|
-
let polling = false; let childRunning = false; let cancelChild: (() => void) | undefined;
|
|
85
|
+
let polling = false; let refreshPending = false; let childRunning = false; let cancelChild: (() => void) | undefined;
|
|
86
86
|
const priorPush = client.onPush; const priorClose = client.onClose; const priorRaw = !!terminal.isRaw;
|
|
87
87
|
const removers: (() => void)[] = []; const timers: ReturnType<typeof setInterval>[] = [];
|
|
88
88
|
let resolveDone: () => void = () => {}; const done = new Promise<void>(resolve => { resolveDone = resolve; });
|
|
@@ -159,8 +159,20 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
159
159
|
safeWrite(`${reason ? reason + "\n" : ""}Left console; the hub keeps running (ahub kill stops it).\n`);
|
|
160
160
|
resolveDone();
|
|
161
161
|
};
|
|
162
|
+
const reconcileDetail = () => {
|
|
163
|
+
if (state.detail === undefined || typeof state.detail !== "object" || (state.panel !== 3 && state.panel !== 4)) return;
|
|
164
|
+
const id = state.detail.id;
|
|
165
|
+
if (id === undefined || id === null) return;
|
|
166
|
+
const key = state.panel === 3 ? "flowRevision" : "revision", rows = state.panel === 3 ? state.tasks : state.queue;
|
|
167
|
+
const fresh = rows.find(row => String(row.id) === String(id));
|
|
168
|
+
if (!fresh || state.detail[key] !== fresh[key]) {
|
|
169
|
+
state.detail = undefined; state.detailOffset = 0;
|
|
170
|
+
notice(`detail ${id} changed or is no longer available`);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
162
173
|
const refresh = async () => {
|
|
163
|
-
if (polling || !active)
|
|
174
|
+
if (polling || !active) { if (active) refreshPending = true; return; }
|
|
175
|
+
polling = true;
|
|
164
176
|
try {
|
|
165
177
|
const panels = state.mode === "panels" && !plain;
|
|
166
178
|
const replies = await Promise.all([
|
|
@@ -169,6 +181,7 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
169
181
|
]);
|
|
170
182
|
if (!active) return;
|
|
171
183
|
const [status, budget, tasks, queue, ready] = replies;
|
|
184
|
+
captureConsoleSelection(state);
|
|
172
185
|
if (status.status?.tasks && status.ok !== false) state.taskCounts = status.status.tasks;
|
|
173
186
|
if (status.status?.peers) state.peers = Object.fromEntries(Object.entries(status.status.peers).map(([peer, value]) => [peer, { ...(value as object), ...status.status.roleInfo?.[peer] }]));
|
|
174
187
|
if (status.status && status.ok !== false) {
|
|
@@ -184,10 +197,14 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
184
197
|
state.tasksKnown = true;
|
|
185
198
|
} }
|
|
186
199
|
if (queue?.ok && state.mode === "panels" && Array.isArray(queue.deliveries)) state.queue = queue.deliveries;
|
|
200
|
+
reconcileDetail();
|
|
187
201
|
const error = replies.find(reply => reply.ok === false)?.error; if (error) notice(String(error));
|
|
188
202
|
draw();
|
|
189
203
|
} catch (error) { if (active) { notice(String((error as Error).message)); draw(); } }
|
|
190
|
-
finally {
|
|
204
|
+
finally {
|
|
205
|
+
polling = false;
|
|
206
|
+
if (refreshPending) { refreshPending = false; void refresh(); }
|
|
207
|
+
}
|
|
191
208
|
};
|
|
192
209
|
const run = options.runCommand ?? ((args, output, finished, commandColumns) => {
|
|
193
210
|
const child = Bun.spawn([process.execPath, fileURLToPath(new URL("./main.ts", import.meta.url)), "--project", options.cwd, ...(options.liveTest ? ["--live-test"] : []), ...args], {
|
|
@@ -223,7 +240,7 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
223
240
|
const result = await client.request(action.panel === 3 ? { t: "task", op: "task_show", args: { id: action.id } } : { t: "queue", op: "show", id: action.id }, 3000);
|
|
224
241
|
// A late reply is for the panel and row it was asked for: once the person has moved on, it would show one item
|
|
225
242
|
// while the keys act on another, so it is dropped.
|
|
226
|
-
if (!active || state.mode !== "panels" || state.panel !== action.panel || state.help || state.detail !== undefined || String(
|
|
243
|
+
if (!active || state.mode !== "panels" || state.panel !== action.panel || state.help || state.detail !== undefined || String(selectedPanelRow(state)?.id) !== action.id) return;
|
|
227
244
|
state.detailOffset = 0;
|
|
228
245
|
state.detail = result.ok === false ? String(result.error) : action.panel === 3 ? labeled(String(result.text)) : result.delivery ?? "delivery not found";
|
|
229
246
|
draw(); return;
|
package/src/cli/help.ts
CHANGED
|
@@ -65,7 +65,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
65
65
|
["ahub pause|resume <peer>", "hold a peer's deliveries in its queue / release them"],
|
|
66
66
|
["ahub queue list [--peer <id>] [--json] [--color=auto|always|never]", "inspect durable deliveries"],
|
|
67
67
|
["ahub queue show <delivery-id> [--json] [--color=auto|always|never]", "inspect one delivery and revision"],
|
|
68
|
-
["ahub queue resolve <delivery-id> --action completed|retry|discard --reason <text>", "
|
|
68
|
+
["ahub queue resolve <delivery-id> --action completed|retry|discard --reason <text> [--revision N]", "resolve a queued or needs_review delivery; --revision binds the displayed row instead of a fresh show"],
|
|
69
69
|
]],
|
|
70
70
|
["Tasks and review", [
|
|
71
71
|
["ahub board import github --label <label> [--milestone <milestone>] [--repo <owner/repo>]", "person only: read bounded open issues into deduplicated Backlog; never writes GitHub"],
|
|
@@ -78,7 +78,8 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
78
78
|
["ahub backlog pull|return <id>", "person only: pull backlog into To Do or return ownerless proposed work; --backlog on task propose stages unoffered work"],
|
|
79
79
|
["ahub task show|escalate <id>", "full task with history (PII text included) / hand it to the next peer in escalate_to"],
|
|
80
80
|
["ahub task role <id> <role|none>", "person only: change bounded catalog guidance on an open task; routing and authority are unchanged"],
|
|
81
|
-
["ahub task assign <id> <peer> [--role owner|reviewer]", "name the owner (default) or reviewer; a task that still waits reserves its owner"],
|
|
81
|
+
["ahub task assign <id> <peer> [--role owner|reviewer] [--revision N]", "name the owner (default) or reviewer; a task that still waits reserves its owner; --revision refuses stale console actions"],
|
|
82
|
+
["ahub task unassign <id> [--role owner|reviewer] [--revision N]", "person only: remove the owner/reservation or reviewer without routing a replacement or stopping native work"],
|
|
82
83
|
["ahub task label <id> ok|regressed|reverted|incomplete|wrong|abandoned", "your later verdict on an approved task, kept in the research records (research must be on)"],
|
|
83
84
|
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]...", "give a review verdict on a task"],
|
|
84
85
|
["ahub route explain <id>", "why a task went where it went, its reserved owner included"],
|
package/src/cli/identity.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function cliCommandLabel(command: string, args: readonly string[]): strin
|
|
|
77
77
|
if (!["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "claude", "codex", "kimi", "qwen", "jcode", "pi", "local", "stop", "models", "say", "queue", "tail", "watch", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "permission", "settings", "roster", "status", "pet", "logs", "export", "report", "research", "memory", "goal", "bench", "sdlc", "role", "facts", "check-path", "turns", "undo", "kill", "reset", "doctor"].includes(command)) return "unknown";
|
|
78
78
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
79
79
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
80
|
-
if (sub && ["propose", "show", "assign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "dedupe", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
|
|
80
|
+
if (sub && ["propose", "show", "assign", "unassign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "dedupe", "decay", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
|
|
81
81
|
}
|
|
82
82
|
return (/^[a-z][a-z0-9-]*$/.test(command) && !/\s/.test(command)) || ["--help", "--version"].includes(command) ? command : "unknown";
|
|
83
83
|
}
|
package/src/cli/main.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Keep this entrypoint plain JavaScript: Node must fail before loading TypeScript or bun:sqlite.
|
|
3
3
|
const version = /^(\d+)\.(\d+)\.(\d+)$/.exec(process.versions.bun ?? "");
|
|
4
|
-
if (!version || !(Number(version[1]) > 1 || (Number(version[1]) === 1 && Number(version[2])
|
|
5
|
-
console.error("ahub: Bun >=1.
|
|
4
|
+
if (!version || !(Number(version[1]) > 1 || (Number(version[1]) === 1 && (Number(version[2]) > 4 || (Number(version[2]) === 4 && Number(version[3]) >= 2))))) {
|
|
5
|
+
console.error("ahub: Bun >=1.4.2 is required. Install Bun: https://bun.sh");
|
|
6
6
|
process.exit(1);
|
|
7
7
|
}
|
|
8
8
|
|
package/src/cli/main.ts
CHANGED
|
@@ -59,6 +59,8 @@ import { armRuns, benchCompare, benchCsv, benchReport, formatCompare, formatRepo
|
|
|
59
59
|
import { benchPreflight, defaultIo, runBench } from "./bench.ts";
|
|
60
60
|
import { ADDONS, isAddon, type AddonName } from "../hub/tooling.ts";
|
|
61
61
|
import { goalCommand, memoryCommand } from "./memory.ts";
|
|
62
|
+
import { selectedMemoryProfile } from "../hub/memory-profile.ts";
|
|
63
|
+
import { checkMemoryProfiles, formatMemoryProfileCheck } from "../hub/memory-profile-check.ts";
|
|
62
64
|
import { addonReport, appendRecords, formatAddons, formatResearch, LABELS, labelTarget, RESEARCH_SCHEMA, projectKey, readStores, researchReport, taskRecords, toCsv, type Label, type TaskRecord } from "../hub/research.ts";
|
|
63
65
|
import { summarize, summarizeByTask } from "../hub/report.ts";
|
|
64
66
|
import { hasTree, planUndo, repoOf, restore, Turns } from "../hub/snapshots.ts";
|
|
@@ -975,17 +977,24 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
975
977
|
return;
|
|
976
978
|
}
|
|
977
979
|
const [id, ...flags] = rest;
|
|
978
|
-
if (!id || !["show", "resolve"].includes(operation)) fail("usage: ahub queue show <id> | resolve <id> --action completed|retry|discard --reason <text>");
|
|
979
|
-
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
980
|
-
if (!shown.ok) fail(shown.error);
|
|
980
|
+
if (!id || !["show", "resolve"].includes(operation)) fail("usage: ahub queue show <id> | resolve <id> --action completed|retry|discard --reason <text> [--revision N]");
|
|
981
981
|
if (operation === "show") {
|
|
982
982
|
if (flags.length) fail("usage: ahub queue show <id>");
|
|
983
|
+
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
984
|
+
if (!shown.ok) fail(shown.error);
|
|
983
985
|
if (options.json) return console.log(JSON.stringify(shown.delivery, null, 2));
|
|
984
986
|
return printOutput(renderQueueShow(shown.delivery, options.columns, Date.now()), options.color);
|
|
985
987
|
}
|
|
986
|
-
const { one, rest: extra } = takeFlags(
|
|
987
|
-
|
|
988
|
-
|
|
988
|
+
const { one, rest: extra } = takeFlags(flags, ["--action", "--reason", "--revision"], []);
|
|
989
|
+
const rawRevision = one["--revision"], parsedRevision = rawRevision === undefined ? undefined : Number(rawRevision);
|
|
990
|
+
if (extra.length || !["completed", "retry", "discard"].includes(one["--action"] ?? "") || !one["--reason"]?.trim() || (rawRevision !== undefined && (!/^\d+$/.test(rawRevision) || !Number.isSafeInteger(parsedRevision)))) fail("usage: ahub queue resolve <id> --action completed|retry|discard --reason <text> [--revision N]");
|
|
991
|
+
let revision = parsedRevision;
|
|
992
|
+
if (revision === undefined) {
|
|
993
|
+
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
994
|
+
if (!shown.ok) fail(shown.error);
|
|
995
|
+
revision = shown.delivery.revision;
|
|
996
|
+
}
|
|
997
|
+
const result = await hub.request({ t: "queue", op: "resolve", id, revision, action: one["--action"], reason: one["--reason"] });
|
|
989
998
|
if (!result.ok) fail(result.error);
|
|
990
999
|
console.log(JSON.stringify(result.delivery, null, 2));
|
|
991
1000
|
} finally { hub.close(); }
|
|
@@ -1183,10 +1192,16 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1183
1192
|
if (sub === "show") return console.log(await taskOp("task_show", { id: rest[0] }));
|
|
1184
1193
|
if (sub === "escalate") return console.log(await taskOp("task_escalate", { id: rest[0] }));
|
|
1185
1194
|
if (sub === "assign") {
|
|
1186
|
-
const { one, rest: positional } = takeFlags(rest, ["--role"], []);
|
|
1187
|
-
const role = one["--role"] ?? "owner";
|
|
1188
|
-
if (positional.length !== 2 || !["owner", "reviewer"].includes(role)) fail("usage: ahub task assign <id> <peer> [--role owner|reviewer]");
|
|
1189
|
-
return console.log(await taskOp("task_assign", { id: positional[0], peer: positional[1], ...(role === "reviewer" ? { role } : {}) }));
|
|
1195
|
+
const { one, rest: positional } = takeFlags(rest, ["--role", "--revision"], []);
|
|
1196
|
+
const role = one["--role"] ?? "owner", rawRevision = one["--revision"], revision = rawRevision === undefined ? undefined : Number(rawRevision);
|
|
1197
|
+
if (positional.length !== 2 || !["owner", "reviewer"].includes(role) || (rawRevision !== undefined && (!/^\d+$/.test(rawRevision) || !Number.isSafeInteger(revision)))) fail("usage: ahub task assign <id> <peer> [--role owner|reviewer] [--revision N]");
|
|
1198
|
+
return console.log(await taskOp(revision === undefined ? "task_assign" : "task_assign_checked", { id: positional[0], peer: positional[1], ...(role === "reviewer" ? { role } : {}), ...(revision === undefined ? {} : { revision }) }));
|
|
1199
|
+
}
|
|
1200
|
+
if (sub === "unassign") {
|
|
1201
|
+
const { one, rest: positional } = takeFlags(rest, ["--role", "--revision"], []);
|
|
1202
|
+
const role = one["--role"] ?? "owner", rawRevision = one["--revision"], revision = rawRevision === undefined ? undefined : Number(rawRevision);
|
|
1203
|
+
if (positional.length !== 1 || !["owner", "reviewer"].includes(role) || (rawRevision !== undefined && (!/^\d+$/.test(rawRevision) || !Number.isSafeInteger(revision)))) fail("usage: ahub task unassign <id> [--role owner|reviewer] [--revision N]");
|
|
1204
|
+
return console.log(await taskOp("task_unassign", { id: positional[0], role, ...(revision === undefined ? {} : { revision }) }));
|
|
1190
1205
|
}
|
|
1191
1206
|
if (sub === "label") {
|
|
1192
1207
|
// #247: a person's later verdict on an approved task (the identity gate keeps it human-only), for research records.
|
|
@@ -1199,7 +1214,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1199
1214
|
appendRecords(project.id, [{ schema: RESEARCH_SCHEMA, kind: "label", project: projectKey(project.id), task: id, createdAt: record!.createdAt!, label, at: new Date().toISOString() }]);
|
|
1200
1215
|
return console.log(`task #${id} (approved ${record!.approvedAt}) labelled ${label} in the research records`);
|
|
1201
1216
|
}
|
|
1202
|
-
if (sub !== "propose" || rest.length < 1) fail("usage: ahub task propose [<class>] <title...> | show <id> | assign <id> <peer> | escalate <id>");
|
|
1217
|
+
if (sub !== "propose" || rest.length < 1) fail("usage: ahub task propose [<class>] <title...> | show <id> | assign <id> <peer> | unassign <id> | escalate <id>");
|
|
1203
1218
|
// `--class` is the explicit form. A first word that is a class name is still taken as the class (the documented
|
|
1204
1219
|
// short form), but said out loud: "review the auth module" would otherwise be filed as class review, silently.
|
|
1205
1220
|
const urgent = rest.includes("--urgent"), backlog = rest.includes("--backlog");
|
|
@@ -1411,7 +1426,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1411
1426
|
printOutput(renderStatus({ ...status, budget: quota.budget ?? {} }, options.columns, Date.now(), options.full), options.color);
|
|
1412
1427
|
if (status.needs) { const pet = loadPetConfig(cwd); if (pet.enabled) console.log(renderPet(petView(status), false, pet.character)); }
|
|
1413
1428
|
// #258: open long-term tasks and who holds them, as of this machine's last memory sync.
|
|
1414
|
-
if (existsSync(join(hubHome(), "memory", "index.db"))) {
|
|
1429
|
+
if (status.memoryProfile ? status.memoryProfile.state === "ready" : existsSync(join(hubHome(), "memory", "index.db"))) {
|
|
1415
1430
|
try { const lines = await goalCommand(["list"], cwd); if (!lines.startsWith("no open")) console.log(`\nlong-term tasks\n${lines}`); } catch { /* memory unreadable: status goes on */ }
|
|
1416
1431
|
}
|
|
1417
1432
|
} finally { hub.close(); }
|
|
@@ -1487,7 +1502,14 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1487
1502
|
},
|
|
1488
1503
|
memory: async () => {
|
|
1489
1504
|
// #258: the hub's own memory on this machine; reads are anyone's, changes a person's.
|
|
1490
|
-
|
|
1505
|
+
if (args.filter(arg => arg !== "--json")[0] === "profile-check") {
|
|
1506
|
+
if (args.some(arg => arg !== "profile-check" && arg !== "--json")) fail("usage: ahub memory profile-check [--json]");
|
|
1507
|
+
const result = await checkMemoryProfiles(selectedMemoryProfile());
|
|
1508
|
+
console.log(args.includes("--json") ? JSON.stringify(result, null, 2) : formatMemoryProfileCheck(result));
|
|
1509
|
+
if (!result.ready) process.exitCode = 1;
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
try { console.log(await memoryCommand(args, cwd, { person: caller.role === "console" })); } catch (e) { fail((e as Error).message); }
|
|
1491
1513
|
},
|
|
1492
1514
|
goal: async () => {
|
|
1493
1515
|
// #258: long-term tasks; a link goes through the project's hub, so Tasks validates the board task.
|
|
@@ -1496,7 +1518,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1496
1518
|
try { const r = await hub.request({ t: "goal_link", goal, task: Number(task) }); if (!r.ok) throw new Error(String(r.error)); return String(r.text); }
|
|
1497
1519
|
finally { hub.close(); }
|
|
1498
1520
|
};
|
|
1499
|
-
try { console.log(await goalCommand(args, cwd, { link })); } catch (e) { fail((e as Error).message); }
|
|
1521
|
+
try { console.log(await goalCommand(args, cwd, { link, person: caller.role === "console" })); } catch (e) { fail((e as Error).message); }
|
|
1500
1522
|
},
|
|
1501
1523
|
research: () => {
|
|
1502
1524
|
// #247: measures from the opt-in research records; `export` writes them out, `backfill` builds them from events.jsonl.
|