@staix/agent-hub 0.12.31 → 0.12.32
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 +13 -1
- package/README.md +13 -7
- package/docs/agent-notes/adapters.md +7 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +10 -7
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/tasks.md +3 -2
- package/docs/agent-notes/tests.md +1 -0
- package/docs/cooperbench.md +1 -1
- package/docs/gsd-collaboration-analysis.md +1 -1
- package/docs/memory.md +1 -1
- package/docs/operations.md +114 -22
- package/docs/quickstart.md +10 -6
- package/docs/security.md +12 -8
- package/docs/smoke.md +34 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +204 -0
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +191 -0
- package/docs/verification/2026-10-18-0.12.32.md +54 -0
- package/docs/verified.json +20 -15
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1296 -26
- package/src/adapters/acp.ts +5 -6
- package/src/adapters/definitions/builtin-metadata.ts +121 -0
- package/src/adapters/definitions/kimi-native.ts +4 -0
- package/src/adapters/definitions/qwen-native.ts +24 -0
- package/src/adapters/definitions/qwen.ts +71 -0
- package/src/adapters/pi.ts +6 -2
- package/src/adapters/registry-types.ts +174 -0
- package/src/adapters/registry.ts +36 -0
- package/src/cli/console-state.ts +74 -5
- package/src/cli/console.ts +35 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/identity.ts +13 -3
- package/src/cli/launch.ts +1 -6
- package/src/cli/main.ts +200 -39
- package/src/cli/output.ts +2 -0
- package/src/cli/preview.ts +8 -5
- package/src/cli/recovery-guidance.ts +137 -0
- package/src/cli/recovery-presentation-options.ts +8 -0
- package/src/cli/recovery-session-file.ts +18 -0
- package/src/cli/terminal-recovery.ts +48 -0
- package/src/cli/upgrade-interactive.ts +129 -75
- package/src/cli/upgrade-labels.ts +30 -0
- package/src/cli/upgrade-presentation-data.ts +41 -0
- package/src/cli/upgrade-presentation-flags.ts +31 -0
- package/src/cli/upgrade-presentation.ts +217 -0
- package/src/cli/upgrade-restore-state.ts +6 -2
- package/src/cli/upgrade-runtime.ts +345 -194
- package/src/cli/upgrade.ts +276 -25
- package/src/hub/bus.ts +11 -0
- package/src/hub/cohorts.ts +2 -2
- package/src/hub/config-apply.ts +242 -0
- package/src/hub/config-reload.ts +120 -0
- package/src/hub/config-trust.ts +11 -2
- package/src/hub/crash.ts +8 -6
- package/src/hub/daemon.ts +559 -170
- package/src/hub/events.ts +2 -2
- package/src/hub/manager.ts +1 -1
- package/src/hub/peer-catalog.ts +10 -13
- package/src/hub/peers.ts +12 -2
- package/src/hub/permission-mode.ts +5 -3
- package/src/hub/recovery-continuity.ts +152 -0
- package/src/hub/recovery-store.ts +10 -4
- package/src/hub/sdlc.ts +5 -1
- package/src/hub/settings.ts +50 -36
- package/src/hub/settled-roles.ts +35 -0
- package/src/hub/supervision.ts +3 -2
- package/src/hub/tasks.ts +28 -8
- package/src/hub/ui.ts +1 -1
- package/src/memory/brief.ts +6 -5
- package/src/pi/session.ts +23 -4
- package/src/ui/index.html +23 -7
package/src/cli/console-state.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { PLANNER_PEERS } from "../hub/peer-catalog.ts";
|
|
|
6
6
|
import { taskProgress } from "../ui/task-progress.ts";
|
|
7
7
|
import type { ProgressStage } from "../ui/task-progress.ts";
|
|
8
8
|
import { sanitize } from "../hub/envelope.ts";
|
|
9
|
+
import type { SettingRow, SettingValue } from "../hub/settings.ts";
|
|
9
10
|
|
|
10
11
|
export interface Approval {
|
|
11
12
|
id: string; peer: string; title: string; expiresAt: number;
|
|
@@ -13,7 +14,14 @@ export interface Approval {
|
|
|
13
14
|
}
|
|
14
15
|
export interface PermissionDefault { peer: string; mode: "never-ask"; source: string }
|
|
15
16
|
export interface ConsoleEvent { text: string; peer?: string; kind?: string; tone?: Tone }
|
|
17
|
+
export interface SettingsEdit {
|
|
18
|
+
token: number; key?: string; undo: boolean; phase: "value" | "preview" | "reason" | "confirm";
|
|
19
|
+
text: string; value?: SettingValue; reason?: string; lines?: string[]; confirm?: string; undoDigest?: string;
|
|
20
|
+
}
|
|
16
21
|
export interface ConsoleState {
|
|
22
|
+
settingsRows?: SettingRow[]; settingsKnown?: boolean; settingsUndo?: string; settingsProblem?: string;
|
|
23
|
+
settingsEdit?: SettingsEdit; settingsSequence?: number;
|
|
24
|
+
pendingRestart?: { key: string; reason: string }[];
|
|
17
25
|
features?: readonly string[];
|
|
18
26
|
/** Older embedding callers omit choices and retain the original STAI/off toggle. */
|
|
19
27
|
petChoices?: readonly CharacterId[]; petCharacter?: CharacterId; petSize?: PetSize; petGlyphs?: "ascii" | "blocks";
|
|
@@ -42,6 +50,8 @@ export interface ConsoleState {
|
|
|
42
50
|
/** A detail view: text as the stream shows it, or labeled fields. */
|
|
43
51
|
export type Detail = string | Record<string, unknown>;
|
|
44
52
|
export type ConsoleEffect = { type: "exit" } | { type: "permit"; id: string; option?: string } |
|
|
53
|
+
{ type: "settings_preview"; token: number; key?: string; value?: SettingValue; undo: boolean } |
|
|
54
|
+
{ type: "settings_write"; key?: string; value?: SettingValue; undo: boolean; reason: string; confirm?: string; undoDigest?: string } |
|
|
45
55
|
{ type: "permission_default"; peer: string; confirmed: boolean } | { type: "command"; args: string[] } | { type: "show"; panel: number; id: string } | { type: "print"; text: string; kind: string; tone?: Tone } | { type: "keys" };
|
|
46
56
|
export function initialConsoleState(panels = false): ConsoleState {
|
|
47
57
|
return { mode: panels ? "panels" : "stream", panel: 1, selection: 0,
|
|
@@ -69,7 +79,7 @@ export function syncPermissionDefaults(s: ConsoleState, rows: unknown): boolean
|
|
|
69
79
|
s.permissionDefaultsHandled = s.permissionDefaultsHandled.filter(fingerprint => current.has(fingerprint));
|
|
70
80
|
const pending = s.confirm;
|
|
71
81
|
if (pending?.type === "permission_default" && !s.permissionDefaults.some(row => row.peer === pending.peer && row.source === pending.source)) s.confirm = undefined;
|
|
72
|
-
if (s.confirm || s.editing || s.input || s.modeChoice || s.optionChoice || s.help) return false;
|
|
82
|
+
if (s.settingsEdit || s.confirm || s.editing || s.input || s.modeChoice || s.optionChoice || s.help) return false;
|
|
73
83
|
const next = s.permissionDefaults.find(row => !s.permissionDefaultsHandled.includes(permissionDefaultFingerprint(s, row)));
|
|
74
84
|
if (!next) return false;
|
|
75
85
|
s.confirm = { type: "permission_default", peer: next.peer, source: next.source };
|
|
@@ -329,6 +339,7 @@ const KEYS: [string, string[]][] = [
|
|
|
329
339
|
["Tasks", ["a assign/reviewer (then y)", "u unassign (then y)", "r review (in review)"]],
|
|
330
340
|
["Queue", ["r resolve (reason, then y)", "queued=discard, needs_review=retry"]],
|
|
331
341
|
["Events", ["f peer filter", "g kind filter"]],
|
|
342
|
+
["Settings", ["6 Settings panel", "e edit", "u undo", "preview, reason, then y"]],
|
|
332
343
|
];
|
|
333
344
|
/** The `?` key table: one group per panel, its keys kept whole and packed into the width after the group column. */
|
|
334
345
|
export function keyTable(columns: number, attention = false, characters = false): Span[][] {
|
|
@@ -388,6 +399,7 @@ export function panelRows(s: ConsoleState): any[] {
|
|
|
388
399
|
const states = ["proposed", "in_progress", "in_review", "changes_requested"];
|
|
389
400
|
return s.tasks.filter(t => states.includes(t.state)).sort((a, b) => states.indexOf(a.state) - states.indexOf(b.state));
|
|
390
401
|
}
|
|
402
|
+
if (s.panel === 6) return [...(s.settingsRows ?? [])].sort((a, b) => a.group.localeCompare(b.group) || a.key.localeCompare(b.key));
|
|
391
403
|
if (s.panel === 4) return s.queue.filter(q => q.state === "needs_review" || q.state === "queued");
|
|
392
404
|
return s.events.filter(e => (!s.peerFilter || e.peer === s.peerFilter) && (!s.kindFilter || e.kind === s.kindFilter));
|
|
393
405
|
}
|
|
@@ -435,12 +447,41 @@ function lockedCommandPrompt(lock: NonNullable<ConsoleState["actionLock"]>, args
|
|
|
435
447
|
return `unassign ${role} ${affected} on task #${lock.id} at revision ${lock.revision} -> ${resulting}${guidance}; native work and uncertain deliveries are not stopped or resolved`;
|
|
436
448
|
}
|
|
437
449
|
export function reduceConsole(state: ConsoleState, key: string, now = Date.now()): { state: ConsoleState; effects: ConsoleEffect[] } {
|
|
438
|
-
const attentionKeyBlocked = !!(state.confirm || state.optionChoice || state.modeChoice || state.help || state.editing || state.input);
|
|
450
|
+
const attentionKeyBlocked = !!(state.settingsEdit || state.confirm || state.optionChoice || state.modeChoice || state.help || state.editing || state.input);
|
|
439
451
|
// A notice answers the key before this one; one that pruning raises now stays.
|
|
440
452
|
let s = pruneApprovals({ ...state, notice: "" }, now); const effects: ConsoleEffect[] = [];
|
|
441
453
|
const done = () => ({ state: bound(s), effects });
|
|
442
454
|
const answered = (id: string) => { s.approvals = s.approvals.filter(a => a.id !== id); if (s.approvalId === id) s.approvalId = undefined; };
|
|
443
455
|
if (key === "\x03") { effects.push({ type: "exit" }); return done(); }
|
|
456
|
+
if (s.settingsEdit) {
|
|
457
|
+
const edit = { ...s.settingsEdit }; s.settingsEdit = edit;
|
|
458
|
+
if (key === "\x1b") { s.settingsEdit = undefined; return done(); }
|
|
459
|
+
if (edit.phase === "preview") return done();
|
|
460
|
+
if (edit.phase === "confirm") {
|
|
461
|
+
s.settingsEdit = undefined;
|
|
462
|
+
if (key === "y") {
|
|
463
|
+
if (edit.undo && (!edit.key || !edit.undoDigest)) { notify(s, "undo preview changed; reload settings and review it again", now); return done(); }
|
|
464
|
+
effects.push({ type: "settings_write", undo: edit.undo, ...(edit.key ? { key: edit.key } : {}), ...(!edit.undo ? { value: edit.value } : { undoDigest: edit.undoDigest }), reason: edit.reason!, ...(edit.confirm ? { confirm: edit.confirm } : {}) });
|
|
465
|
+
}
|
|
466
|
+
return done();
|
|
467
|
+
}
|
|
468
|
+
if (key === "\x7f" || key === "\b") edit.text = Array.from(edit.text).slice(0, -1).join("");
|
|
469
|
+
else if (key === "\x15") edit.text = "";
|
|
470
|
+
else if (key === "\r" || key === "\n") {
|
|
471
|
+
if (edit.phase === "reason") {
|
|
472
|
+
if (!edit.text.trim() || Buffer.byteLength(edit.text.trim()) > 400) { notify(s, "give a reason from 1 to 400 UTF-8 bytes", now); return done(); }
|
|
473
|
+
edit.reason = edit.text.trim(); edit.text = ""; edit.phase = "confirm";
|
|
474
|
+
} else {
|
|
475
|
+
const row = s.settingsRows?.find(row => row.key === edit.key);
|
|
476
|
+
if (!row) { s.settingsEdit = undefined; notify(s, "setting is no longer available", now); return done(); }
|
|
477
|
+
const text = edit.text;
|
|
478
|
+
edit.value = text === "inherit" ? null : row.type === "boolean" ? text === "true" ? true : text === "false" ? false : text : row.type === "number" ? text.trim() && Number.isFinite(Number(text)) ? Number(text) : text : row.type === "integer" ? /^(0|[1-9]\d*)$/.test(text) ? Number(text) : text : row.type === "peers" || row.type === "roles" ? text.split(",").map(value => value.trim()).filter(Boolean) : text;
|
|
479
|
+
edit.phase = "preview"; edit.text = "";
|
|
480
|
+
effects.push({ type: "settings_preview", token: edit.token, key: edit.key, value: edit.value, undo: false });
|
|
481
|
+
}
|
|
482
|
+
} else if (!key.startsWith("\x1b")) edit.text += terminalText(key).replace(/\n/g, "").slice(0, Math.max(0, 1000 - edit.text.length));
|
|
483
|
+
return done();
|
|
484
|
+
}
|
|
444
485
|
if (s.confirm) {
|
|
445
486
|
const confirm = s.confirm; s.confirm = undefined;
|
|
446
487
|
if (confirm.type === "permission_default") {
|
|
@@ -546,10 +587,19 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
546
587
|
if (key === "\t") { s.mode = s.mode === "panels" ? "stream" : "panels"; s.detail = undefined; s.requestDetail = undefined; return done(); }
|
|
547
588
|
if (key === ":") { s.editing = true; return done(); }
|
|
548
589
|
const switchPanel = (panel: number) => { s.panel = panel; s.selection = panel === 5 ? newestEvent(s) : 0; s.taskSelection = undefined; s.queueSelection = undefined; s.detail = undefined; s.requestDetail = undefined; return done(); };
|
|
549
|
-
if (s.mode === "panels" && /^[1-
|
|
590
|
+
if (s.mode === "panels" && /^[1-6]$/.test(key) && !s.optionChoice) return switchPanel(Number(key));
|
|
550
591
|
// Right and Left are the digit of the next or previous tab, wrapping at both ends (#309); the same guards as the
|
|
551
592
|
// digits apply (the confirm, mode-choice, key-table and editing branches above returned already).
|
|
552
|
-
if (s.mode === "panels" && (key === "\x1b[C" || key === "\x1b[D") && !s.optionChoice) return switchPanel((s.panel + (key === "\x1b[C" ? 0 : 3)) % 5 + 1);
|
|
593
|
+
if (s.mode === "panels" && (key === "\x1b[C" || key === "\x1b[D") && !s.optionChoice) return switchPanel(s.panel === 6 ? key === "\x1b[C" ? 1 : 5 : (s.panel + (key === "\x1b[C" ? 0 : 3)) % 5 + 1);
|
|
594
|
+
if (s.mode === "panels" && s.panel === 6 && s.detail === undefined && (key === "e" || key === "u")) {
|
|
595
|
+
const row = selectedPanelRow(s);
|
|
596
|
+
if (key === "u" && !s.settingsUndo) { notify(s, "nothing to undo", now); return done(); }
|
|
597
|
+
if (key === "e" && !row) return done();
|
|
598
|
+
const token = (s.settingsSequence ?? 0) + 1; s.settingsSequence = token;
|
|
599
|
+
s.settingsEdit = { token, undo: key === "u", ...(key === "e" ? { key: row.key } : {}), phase: key === "u" ? "preview" : "value", text: key === "e" ? settingValueText(row.fileValue) : "" };
|
|
600
|
+
if (key === "u") effects.push({ type: "settings_preview", token, undo: true });
|
|
601
|
+
return done();
|
|
602
|
+
}
|
|
553
603
|
if (key === "?") {
|
|
554
604
|
if (s.mode === "stream") effects.push({ type: "keys" });
|
|
555
605
|
else s.help = !s.help;
|
|
@@ -685,6 +735,7 @@ export const TABLES: Record<number, string[]> = {
|
|
|
685
735
|
2: ["ID", "PEER", "LEFT", "TITLE"],
|
|
686
736
|
3: ["ID", "STATE", "OWNER", "REVIEWER", "CLASS", "AGE", "TITLE", "STAGE"],
|
|
687
737
|
4: ["ID", "PEER", "STATE", "REV", "AGE"],
|
|
738
|
+
6: ["GROUP", "KEY", "VALUE", "SOURCE", "APPLIES"],
|
|
688
739
|
};
|
|
689
740
|
const showPermissionModes = (s: ConsoleState) => Object.values(s.peers).some(peer => peer.permissionMode && peer.permissionMode !== "ask");
|
|
690
741
|
const count = (n: unknown) => typeof n === "number" && n ? String(n) : "-";
|
|
@@ -714,6 +765,7 @@ export function cells(s: ConsoleState, row: any, selected: boolean, now: number,
|
|
|
714
765
|
return [id(`#${row.id}`, "info"), span(`${row.state}${s.attentionFacts?.some(fact => fact.kind === "stall" && fact.id === row.id) ? " stalled" : ""}${failed && row.state !== last.event ? ` ${last.event}` : ""}${row.ready ? " ready" : ""}${stage?.waiting ? " waiting" : ""}`, failed ? "failure" : row.ready || stage?.waiting ? "attention" : stateTone(row.state)),
|
|
715
766
|
span(row.owner ?? "-"), span(row.reviewer ?? "-"), span(row.profileClass ? `${row.profileClass} (base ${row.class})` : row.class ?? "-"), span(typeof at === "number" ? duration(now - at) : "-", "muted"), span(row.title), span(row.lifecycle ? `${meter} ${row.lifecycle.workflow ? row.lifecycle.workflow + " " : ""}SDLC ${row.lifecycle.stage}${row.lifecycle.definition ? ` (base ${row.lifecycle.definition.base}) / next: ${row.lifecycle.actor} / entry: ${row.lifecycle.entry?.length ? "blocked" : "ready"} / exit: ${row.lifecycle.exit?.length ? "blocked" : "ready"}` : ""}: ${row.lifecycle.next}` : meter, stage?.cancelled ? "muted" : stage?.changes ? "failure" : stateTone(row.state))];
|
|
716
767
|
}
|
|
768
|
+
if (s.panel === 6) return [span(row.group), span(row.key, selected ? "strong" : "info"), span(settingValueText(row.value)), span(row.source), span(`${row.applies}${row.pending !== undefined ? ` (file ${settingValueText(row.fileValue)})` : ""}`, row.pending !== undefined ? "attention" : undefined)];
|
|
717
769
|
if (s.panel === 4) return [id(row.id, "info"), span(row.peer), span(row.rostered === false ? `${row.state} off roster` : row.state, stateTone(row.state)), span(row.revision ?? "-"), span(typeof row.createdAt === "number" ? duration(now - row.createdAt) : "-", "muted")];
|
|
718
770
|
const [header, ...body] = String(row.text).split("\n");
|
|
719
771
|
// The header goes through the stream's own tokenizer with the event's tone and kind, on the same tab-flattened
|
|
@@ -756,8 +808,10 @@ function taskHints(row: any): string[] {
|
|
|
756
808
|
if (review) hints.push("r review");
|
|
757
809
|
return hints;
|
|
758
810
|
}
|
|
811
|
+
const settingValueText = (value: unknown): string => value === null || value === undefined ? "inherit" : Array.isArray(value) ? value.join(",") : String(value);
|
|
759
812
|
function panelContext(s: ConsoleState, row: any): string[] {
|
|
760
813
|
if (!row) return [];
|
|
814
|
+
if (s.panel === 6) return ["e edit", ...(s.settingsUndo ? ["u undo"] : [])];
|
|
761
815
|
if (s.panel === 1) return ["p pause", "r resume", "m permission mode"];
|
|
762
816
|
if (s.panel === 3) return [...taskHints(row), ...(s.features?.includes("task_cancel") ? ["c cancel"] : [])];
|
|
763
817
|
if (s.panel === 4) return [row.state === "queued" ? "r discard" : "r resolve"];
|
|
@@ -778,7 +832,7 @@ function hint(s: ConsoleState, columns: number): string {
|
|
|
778
832
|
function header(s: ConsoleState, columns: number): Span[] {
|
|
779
833
|
const counts = [0, s.approvals.length, 0, s.queue.filter(q => q.state === "needs_review").length, 0];
|
|
780
834
|
const tabs: Span[] = [];
|
|
781
|
-
for (const [i, name] of ["Peers", "Approvals", "Tasks", "Queue", "Events"].entries()) {
|
|
835
|
+
for (const [i, name] of ["Peers", "Approvals", "Tasks", "Queue", "Events", "Settings"].entries()) {
|
|
782
836
|
const active = i + 1 === s.panel; const tone = active ? "strong" : "info";
|
|
783
837
|
tabs.push(span(active ? " [" : " ", tone), span(`${i + 1} ${name}`, tone));
|
|
784
838
|
if (counts[i]) tabs.push(span(` ${counts[i]}`, i === 1 ? "attention" : "failure"));
|
|
@@ -822,6 +876,10 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
822
876
|
let prompt = s.editing ? `: ${s.input}` : hint(s, columns);
|
|
823
877
|
if (s.confirm?.type === "permission") prompt = `allow ${quoted(s.confirm.option)} for ${s.confirm.peer} (request ${s.confirm.id})? y/N`;
|
|
824
878
|
if (s.confirm?.type === "command") prompt = `${s.confirm.prompt ?? s.confirm.args.join(" ")}? y/N`;
|
|
879
|
+
if (s.settingsEdit) {
|
|
880
|
+
const edit = s.settingsEdit;
|
|
881
|
+
prompt = edit.phase === "value" ? `value for ${edit.key} (inherit removes): ${edit.text}` : edit.phase === "preview" ? "checking setting preview; Esc cancels" : edit.phase === "reason" ? `reason: ${edit.text}` : `${edit.undo ? "undo last setting" : `set ${edit.key}`} (${edit.reason})? y/N`;
|
|
882
|
+
}
|
|
825
883
|
if (s.modeChoice) prompt = `permission ${s.modeChoice}: 1 ask 2 ask-when-needed 3 never-ask Esc cancel`;
|
|
826
884
|
const choice = s.optionChoice ? s.approvals.find(a => a.id === s.optionChoice) : undefined;
|
|
827
885
|
if (choice) prompt = `allow request ${choice.id} (${choice.peer}) with: ${allowOptions(choice).map((o, i) => `${i + 1} ${quoted(o.name)}`).join(" ")} Esc cancel`;
|
|
@@ -834,6 +892,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
834
892
|
if (p.paused) summary.push(span(" paused", "attention"));
|
|
835
893
|
if (p.toolsOnly) summary.push(span(id === "kimi" ? ` pull-only, read ${typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never"}` : " tools-only: ahub claude", "attention"));
|
|
836
894
|
}
|
|
895
|
+
if (s.pendingRestart?.length) summary.push(span(`${summary.length ? " | " : ""}restart pending: ${s.pendingRestart.map(row => row.key).join(", ")}`, "attention"));
|
|
837
896
|
const quotas = Object.entries(s.budget).map(([id, b]) => `${id}:${b.windows?.map((w: any) => `${w.id} ${Math.round(w.used * 100)}%`).join("/") ?? "?"}`).join(" ");
|
|
838
897
|
if (quotas) summary.push(span(`${summary.length ? " | " : ""}${quotas}`));
|
|
839
898
|
const approvals = [span(plural(s.approvals.length, "approval"), s.approvals.length ? "attention" : undefined)];
|
|
@@ -863,6 +922,10 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
863
922
|
const height = bodyHeight - noticeLines.length;
|
|
864
923
|
const lines: Span[][] = [];
|
|
865
924
|
if (s.confirm?.type === "permission_default") lines.push(...fieldLines([["default", `${s.confirm.peer} never-ask`], ["source", s.confirm.source], ["bounds", permissionBoundary(s.confirm.peer)], ["decision", "y enables; n cancels for this hub; Esc defers; until then ask"]], columns, now).flat());
|
|
925
|
+
else if (s.settingsEdit) {
|
|
926
|
+
const edit = s.settingsEdit;
|
|
927
|
+
lines.push(...fieldLines([["setting", edit.key ?? s.settingsUndo ?? "last write"], ["preview", (edit.lines ?? []).join("\n")], ["decision", "Esc cancels; saving needs a reason and explicit y"]], columns, now).flat());
|
|
928
|
+
}
|
|
866
929
|
else if (s.help) lines.push(...keyTable(columns, !!s.petEnabled || !!s.petChoices?.length, !!s.petChoices?.length));
|
|
867
930
|
else if (s.detail !== undefined || (s.requestDetail && permission)) {
|
|
868
931
|
const detail = s.detail !== undefined ? detailLines(s.detail, columns, now) : fieldLines(approvalFields(permission!), columns, now).flat();
|
|
@@ -879,6 +942,12 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
879
942
|
const capacity = Math.max(1, s.panel === 2 ? Math.min(Math.floor(height / 2) - 1, height - 3 - tail.length) : height - (s.panel === 3 ? 2 : 1)); const start = Math.max(0, at - capacity + 1);
|
|
880
943
|
const stages = new Map(progress.stages.map(stage => [stage.id, stage]));
|
|
881
944
|
const rowsOf = head ? table(head, data.map((row, i) => cells(s, row, i === at, now, stages)), columns) : undefined;
|
|
945
|
+
if (s.panel === 6) {
|
|
946
|
+
const row = selectedPanelRow(s);
|
|
947
|
+
const info = s.settingsProblem ?? (!s.settingsKnown ? "settings loading..." : row?.restartReason ?? row?.note ?? row?.label ?? "Settings");
|
|
948
|
+
lines.push(...wrap(info, columns).slice(0, 2).map(text => [span(text, s.settingsProblem ? "failure" : "info")]));
|
|
949
|
+
if (s.pendingRestart?.length) lines.push(fitLine([span(`pending restart: ${s.pendingRestart.map(row => `${row.key}: ${row.reason}`).join("; ")}`, "attention")], columns));
|
|
950
|
+
}
|
|
882
951
|
if (s.panel === 3) {
|
|
883
952
|
const done = progress.total ? Math.floor(progress.counts.approved * 20 / progress.total) : 0;
|
|
884
953
|
const main = s.tasksKnown === false && !progress.total ? "tasks loading..." : `${progress.counts.approved}/${progress.total} approved [${"#".repeat(done)}${".".repeat(20 - done)}] ${progress.total ? Math.floor(progress.counts.approved * 100 / progress.total) : 0}%`;
|
package/src/cli/console.ts
CHANGED
|
@@ -189,11 +189,13 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
189
189
|
const replies = await Promise.all([
|
|
190
190
|
client.request({ t: "status" }, 3000), client.request({ t: "budget" }, 3000),
|
|
191
191
|
...(panels ? [client.request({ t: "task", op: "hub_task_list", args: {} }, 3000), client.request({ t: "queue", op: "list" }, 3000), client.request({ t: "task", op: "hub_task_list", args: { ready: true } }, 3000)] : []),
|
|
192
|
+
...(panels && state.panel === 6 ? [client.request({ t: "settings_get" }, 3000)] : []),
|
|
192
193
|
]);
|
|
193
194
|
if (!active) return;
|
|
194
|
-
const [status, budget, tasks, queue, ready] = replies;
|
|
195
|
+
const [status, budget, tasks, queue, ready, settings] = replies;
|
|
195
196
|
captureConsoleSelection(state);
|
|
196
197
|
if (status.status?.tasks && status.ok !== false) state.taskCounts = status.status.tasks;
|
|
198
|
+
if (status.status && status.ok !== false) state.pendingRestart = Array.isArray(status.status.pendingRestart) ? status.status.pendingRestart : [];
|
|
197
199
|
if (status.status) state.features = Array.isArray(status.status.features) ? status.status.features : [];
|
|
198
200
|
if (status.status?.peers) state.peers = Object.fromEntries(Object.entries(status.status.peers).map(([peer, value]) => [peer, { ...(value as object), ...status.status.roleInfo?.[peer] }]));
|
|
199
201
|
if (status.status && status.ok !== false) {
|
|
@@ -209,6 +211,13 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
209
211
|
state.tasksKnown = true;
|
|
210
212
|
} }
|
|
211
213
|
if (queue?.ok && state.mode === "panels" && Array.isArray(queue.deliveries)) state.queue = queue.deliveries;
|
|
214
|
+
if (settings) {
|
|
215
|
+
if (settings.ok && Array.isArray(settings.rows)) {
|
|
216
|
+
state.settingsRows = settings.rows; state.settingsKnown = true; state.settingsProblem = undefined;
|
|
217
|
+
state.settingsUndo = typeof settings.undo === "string" ? settings.undo : undefined;
|
|
218
|
+
state.pendingRestart = Array.isArray(settings.pendingRestart) ? settings.pendingRestart : [];
|
|
219
|
+
} else state.settingsProblem = String(settings.error ?? "settings are unavailable");
|
|
220
|
+
}
|
|
212
221
|
reconcileDetail();
|
|
213
222
|
const error = replies.find(reply => reply.ok === false)?.error; if (error) notice(String(error));
|
|
214
223
|
draw();
|
|
@@ -233,6 +242,23 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
233
242
|
});
|
|
234
243
|
const effect = async (action: ConsoleEffect) => {
|
|
235
244
|
if (action.type === "exit") return stop();
|
|
245
|
+
if (action.type === "settings_preview") {
|
|
246
|
+
const response = await client.request(action.undo ? { t: "settings_preview", undo: true } : { t: "settings_preview", key: action.key, value: action.value }, 3000);
|
|
247
|
+
const edit = state.settingsEdit;
|
|
248
|
+
if (!active || state.mode !== "panels" || state.panel !== 6 || !edit || edit.phase !== "preview" || edit.token !== action.token || edit.undo !== action.undo || edit.key !== action.key || (!action.undo && JSON.stringify(edit.value) !== JSON.stringify(action.value))) return;
|
|
249
|
+
if (response.ok === false) { state.settingsEdit = undefined; notice(String(response.error ?? "setting preview refused")); }
|
|
250
|
+
else if (action.undo && (typeof response.key !== "string" || !response.key || response.key.trim() !== response.key || response.key.length > 160 || typeof response.undoDigest !== "string" || !/^[a-f0-9]{64}$/i.test(response.undoDigest))) {
|
|
251
|
+
state.settingsEdit = undefined; notice("undo preview is unavailable or changed; reload settings and review it again");
|
|
252
|
+
} else state.settingsEdit = { ...edit, ...(action.undo ? { key: response.key, undoDigest: response.undoDigest } : {}), phase: "reason", text: "", lines: [...(Array.isArray(response.lines) ? response.lines.map(String) : []), ...(Array.isArray(response.tasks) ? response.tasks.flatMap((task: any) => [String(task.before?.join("\n") ?? ""), String(task.after?.join("\n") ?? "")]) : []), ...(response.hidden ? [`${response.hidden} more open tasks are not shown`] : [])], ...(typeof response.confirm === "string" ? { confirm: response.confirm } : {}) };
|
|
253
|
+
draw(); return;
|
|
254
|
+
}
|
|
255
|
+
if (action.type === "settings_write") {
|
|
256
|
+
const result = await client.request({ t: action.undo ? "settings_undo" : "settings_set", key: action.key, ...(!action.undo ? { value: action.value } : { undoDigest: action.undoDigest }), reason: action.reason, ...(action.confirm ? { confirm: action.confirm } : {}) }, 35_000);
|
|
257
|
+
if (!active) return;
|
|
258
|
+
if (result.ok === false) notice(String(result.error ?? "setting write refused"));
|
|
259
|
+
else stream({ text: String(result.text ?? "setting saved"), kind: "settings" });
|
|
260
|
+
await refresh(); draw(); return;
|
|
261
|
+
}
|
|
236
262
|
if (action.type === "permission_default") {
|
|
237
263
|
const result = await client.request({ t: "permission_default", peer: action.peer, confirmed: action.confirmed }, 35_000);
|
|
238
264
|
if (!active) return;
|
|
@@ -268,12 +294,13 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
268
294
|
};
|
|
269
295
|
const handle = (key: string) => {
|
|
270
296
|
if (!active) return;
|
|
297
|
+
const priorPanel = state.panel;
|
|
271
298
|
const result = reduceConsole(state, key); state = result.state;
|
|
272
299
|
state.attentionMessages = []; state.attentionSelection = key === "n" ? state.attentionSelection : undefined;
|
|
273
300
|
if (state.mode === "panels" && (plain || columns < 80 || rows < 24)) { state.mode = "stream"; notice("panels need at least 80x24"); }
|
|
274
301
|
for (const action of result.effects) void effect(action).catch(error => stop(`Console error: ${String(error.message)}`));
|
|
275
302
|
draw();
|
|
276
|
-
if (key === "\t") void refresh();
|
|
303
|
+
if (key === "\t" || state.panel !== priorPanel) void refresh();
|
|
277
304
|
};
|
|
278
305
|
client.onPush = msg => {
|
|
279
306
|
if (!active) return;
|
|
@@ -320,6 +347,12 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
320
347
|
state.attentionMessages = []; state.attentionSelection = undefined;
|
|
321
348
|
if (state.help && state.mode === "panels") { draw(); return; }
|
|
322
349
|
if (state.confirm?.type === "permission_default") { draw(); return; }
|
|
350
|
+
if (state.settingsEdit) {
|
|
351
|
+
const edit = state.settingsEdit;
|
|
352
|
+
if (edit.phase === "value" || edit.phase === "reason") state.settingsEdit = { ...edit, text: (edit.text + terminalText(text.replace(/\x1b\[20[01]~/g, "")).replace(/[\r\n]/g, " ")).slice(0, 1000) };
|
|
353
|
+
else state.settingsEdit = undefined;
|
|
354
|
+
draw(); return;
|
|
355
|
+
}
|
|
323
356
|
state.confirm = undefined; state.optionChoice = undefined; state.editing = true;
|
|
324
357
|
state.input += terminalText(text.replace(/\x1b\[20[01]~/g, "")).replace(/\n/g, " "); draw();
|
|
325
358
|
}
|
package/src/cli/help.ts
CHANGED
|
@@ -33,6 +33,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
33
33
|
["ahub restart [--dry-run] [--yes]", "recover this project's runtime; on a terminal without a flag it reviews the plan with you, follows the progress and offers the next steps"],
|
|
34
34
|
["ahub upgrade [--to <version>] [--dry-run] [--yes]", "review and upgrade running projects; --to defaults to the latest release (--yes needs it), and a newer release's own coordinator takes over; on a terminal without --dry-run or --yes: review, end agents first if you choose, apply, follow, and resume, cancel, end or reset on the spot"],
|
|
35
35
|
["ahub recovery [status|resume|abort] [<operation-id>]", "inspect, resume or cancel a preflight; status names the next actions; without an id, the operation that holds the lock, and with nothing else on a terminal its screen"],
|
|
36
|
+
["ahub recovery reattach <operation-id> codex|claude|pi", "person only: repair that blocked peer with its recorded session; leaves the rest for recovery resume"],
|
|
36
37
|
["ahub recovery dispose [<operation-id>] --fresh-session codex|claude --reason <text>", "end a partial operation by starting that peer as a new session, recording the lost conversation"],
|
|
37
38
|
["ahub recovery dispose [<operation-id>] --stop-and-archive --reason <text>", "end a partial operation: stop its own targets, archive its snapshot and release the lock; the upgrade is abandoned, not completed"],
|
|
38
39
|
["ahub daemon [--unattended]", "internal: the detached hub process that ahub up or the dashboard manager starts"],
|
|
@@ -61,7 +62,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
61
62
|
["ahub roster [--json] [--color=auto|always|never]", "human only: every known peer, participation, start mode and retained work"],
|
|
62
63
|
["ahub roster add <peer> | remove <peer> [--yes] [--now]", "human only: change live participation through settings; removal previews retained messages and task handoff"],
|
|
63
64
|
["ahub settings [list] [--json] | get <key>", "human only: the settings the dashboard can change, each with its value, the file it comes from and when it applies"],
|
|
64
|
-
["ahub settings set <key> <value|inherit> [--yes] [--preview] [--now] | undo", "human only: change
|
|
65
|
+
["ahub settings set <key> <value|inherit> [--yes] [--preview] [--now] | undo | reload", "human only: change or reload validated settings, written to .agenthub/config.local.json or routing.local.toml (never a tracked file); --preview shows the effect and saves nothing; undo puts back the last write"],
|
|
65
66
|
["ahub pause|resume <peer>", "hold a peer's deliveries in its queue / release them"],
|
|
66
67
|
["ahub queue list [--peer <id>] [--json] [--color=auto|always|never]", "inspect durable deliveries"],
|
|
67
68
|
["ahub queue show <delivery-id> [--json] [--color=auto|always|never]", "inspect one delivery and revision"],
|
|
@@ -80,6 +81,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
80
81
|
["ahub task role <id> <role|none>", "person only: change bounded catalog guidance on an open task; routing and authority are unchanged"],
|
|
81
82
|
["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
83
|
["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"],
|
|
84
|
+
["ahub task cancel <id> --reason <text> [--superseded-by <ref>] [--revision N]", "person only: close obsolete work with a reason; retain its history without stopping native work or releasing dependents"],
|
|
83
85
|
["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)"],
|
|
84
86
|
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]...", "give a review verdict on a task"],
|
|
85
87
|
["ahub route explain <id>", "why a task went where it went, its reserved owner included"],
|
package/src/cli/identity.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
+
import { recoveryPresentationFlag } from "./recovery-presentation-options.ts";
|
|
1
2
|
import { isSupportedPeer } from "../hub/peer-catalog.ts";
|
|
2
3
|
|
|
3
4
|
/** Recovery reads this declaration before delegating to another package's CLI. */
|
|
4
5
|
export const LIVE_TEST_SUPPORTED = true;
|
|
5
6
|
|
|
7
|
+
/** Presentation prefixes cannot hide a person's recovery action from authorization. */
|
|
8
|
+
function recoveryAction(args: readonly string[]): string | undefined {
|
|
9
|
+
return args.find(arg => !recoveryPresentationFlag(arg));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const LIVE_TEST_PERSON_REFUSAL = "live-test cannot write person-attributed content; use peer tools or a person's terminal";
|
|
13
|
+
|
|
6
14
|
/** Person verdicts and user-authored workflow content are never delegated. */
|
|
7
|
-
export function liveTestRefusal(command: string, args: readonly string[]): boolean {
|
|
15
|
+
export function liveTestRefusal(command: string, args: readonly string[], context: { repairedFresh?: boolean } = {}): boolean {
|
|
8
16
|
if (args.some(arg => arg === "--as-user" || arg.startsWith("--as-user="))) return true;
|
|
9
|
-
const sub = args.find(arg => arg !== "--json");
|
|
17
|
+
const sub = command === "recovery" ? recoveryAction(args) : args.find(arg => arg !== "--json");
|
|
10
18
|
return ["task", "role", "goal"].includes(command)
|
|
11
19
|
|| (command === "backlog" && sub !== "list") || (command === "release" && !["list", "show"].includes(sub ?? ""))
|
|
12
20
|
|| (command === "memory" && sub !== "graph")
|
|
21
|
+
|| (command === "recovery" && (sub === "reattach" || sub === "dispose" && context.repairedFresh === true))
|
|
13
22
|
|| (command === "board" && args[0] === "import") || (command === "sdlc" && args[0] === "record")
|
|
14
23
|
|| (command === "bench" && args.find(arg => arg !== "--json") === "run") || command === "pet";
|
|
15
24
|
}
|
|
@@ -42,6 +51,7 @@ export type PeerCommandAccess = "allowed" | "conductor" | "console";
|
|
|
42
51
|
|
|
43
52
|
/** A closed list: unknown commands cannot acquire console authority through an agent shell. */
|
|
44
53
|
export function classifyPeerCommand(command: string, args: readonly string[]): PeerCommandAccess {
|
|
54
|
+
if (command === "recovery" && recoveryAction(args) === "reattach") return "console";
|
|
45
55
|
if (command === "pi" && args.includes("--replace-session")) return "console";
|
|
46
56
|
if (command === "pet" && args.includes("--window")) return "console";
|
|
47
57
|
if (command === "permission" || command === "stop" || command === "watch") return "console";
|
|
@@ -77,7 +87,7 @@ export function cliCommandLabel(command: string, args: readonly string[]): strin
|
|
|
77
87
|
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
88
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
79
89
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
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}`;
|
|
90
|
+
if (sub && ["propose", "show", "assign", "unassign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "reattach", "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
91
|
}
|
|
82
92
|
return (/^[a-z][a-z0-9-]*$/.test(command) && !/\s/.test(command)) || ["--help", "--version"].includes(command) ? command : "unknown";
|
|
83
93
|
}
|
package/src/cli/launch.ts
CHANGED
|
@@ -393,12 +393,7 @@ export function buildKimiLaunch(command: string[], model?: string): Launch {
|
|
|
393
393
|
return { cmd: cmd!, args: model ? ["--model", model, ...args] : args };
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
|
|
397
|
-
export function buildQwenLaunch(command: string[], model?: string): Launch {
|
|
398
|
-
const [cmd, ...args] = command;
|
|
399
|
-
if (!cmd) throw new Error("qwen_cmd must name an executable");
|
|
400
|
-
return { cmd, args: [...args, ...(model ? ["--model", model] : [])] };
|
|
401
|
-
}
|
|
396
|
+
export { buildQwenLaunch } from "../adapters/definitions/qwen-native.ts";
|
|
402
397
|
|
|
403
398
|
/** The configured ACP argv has a TUI form only when its exact final word is the ACP subcommand. */
|
|
404
399
|
export function buildKimiTuiLaunch(command: string[], model?: string): Launch {
|