@staix/agent-hub 0.12.41 → 0.12.43
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 +26 -0
- package/README.md +7 -4
- package/docs/agent-notes/adapters.md +4 -2
- package/docs/agent-notes/bus.md +2 -0
- package/docs/agent-notes/daemon.md +7 -2
- package/docs/agent-notes/tasks.md +1 -0
- package/docs/agent-notes/tests.md +4 -2
- package/docs/operations.md +98 -24
- package/docs/quickstart.md +11 -3
- package/docs/sdlc.md +1 -1
- package/docs/security.md +3 -3
- package/docs/smoke.md +1 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +51 -9
- package/docs/verification/2026-10-20-0.12.42.md +48 -0
- package/docs/verification/2026-10-21-0.12.43.md +50 -0
- package/docs/verification/2026-10-21-agy-1.4.3-t0.md +65 -0
- package/docs/verification/2026-10-21-pii-screen.md +14 -0
- package/docs/verified.json +13 -13
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +79 -29
- package/src/adapters/agy/entry.ts +11 -1
- package/src/adapters/agy/hooks.ts +7 -2
- package/src/adapters/pi.ts +46 -16
- package/src/cli/completion.ts +173 -0
- package/src/cli/console-state.ts +107 -20
- package/src/cli/console.ts +9 -6
- package/src/cli/help.ts +3 -2
- package/src/cli/identity.ts +2 -2
- package/src/cli/main.js +5 -1
- package/src/cli/main.ts +6 -0
- package/src/cli/terminal-recovery.ts +18 -9
- package/src/hub/daemon.ts +138 -33
- package/src/hub/hub-tools.ts +1 -1
- package/src/hub/main-checkout.ts +2 -2
- package/src/hub/manager.ts +10 -2
- package/src/hub/settings.ts +1 -1
- package/src/hub/snapshots.ts +252 -17
- package/src/hub/tasks.ts +16 -2
package/src/cli/console-state.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface PermissionDefault { peer: string; mode: "never-ask"; source: st
|
|
|
16
16
|
export interface ConsoleEvent { text: string; peer?: string; kind?: string; tone?: Tone }
|
|
17
17
|
export interface SettingsEdit {
|
|
18
18
|
token: number; key?: string; undo: boolean; phase: "value" | "preview" | "reason" | "confirm";
|
|
19
|
+
/** The existing editor can also be opened by the Peers WIP shortcut. */
|
|
20
|
+
panel?: 1 | 6;
|
|
19
21
|
text: string; value?: SettingValue; reason?: string; lines?: string[]; confirm?: string; undoDigest?: string;
|
|
20
22
|
}
|
|
21
23
|
export interface ConsoleState {
|
|
@@ -38,11 +40,13 @@ export interface ConsoleState {
|
|
|
38
40
|
autoSelected?: boolean;
|
|
39
41
|
input: string; editing: boolean; history: string[]; historyIndex: number;
|
|
40
42
|
approvals: Approval[]; events: ConsoleEvent[]; peers: Record<string, any>; budget: Record<string, any>;
|
|
43
|
+
taskWip?: Record<string, { wip: number; wipLimit: number }>;
|
|
44
|
+
backlogView?: boolean;
|
|
41
45
|
tasks: any[]; tasksKnown?: boolean; taskCounts?: Record<string, number>; queue: any[]; detail?: Detail; detailOffset: number; help: boolean; notice: string; noticeAt?: number; noticeTone?: Tone;
|
|
42
46
|
peerFilter?: string; kindFilter?: string; project?: string;
|
|
43
47
|
taskSelection?: number; queueSelection?: string;
|
|
44
|
-
actionLock?: { prefix: string[]; id: string; revision: number; state: string; owner?: string; reserved?: string; reviewer?: string; waiting?: boolean };
|
|
45
|
-
confirm?: { type: "permission"; id: string; option: string; peer: string } | { type: "command"; args: string[]; prompt?: string } | { type: "permission_default"; peer: string; source: string };
|
|
48
|
+
actionLock?: { prefix: string[]; id: string; revision: number; state: string; owner?: string; reserved?: string; reviewer?: string; waiting?: boolean; syntax?: "planning" | "ownerless-return"; revisionField?: "planningRevision" | "flowRevision" };
|
|
49
|
+
confirm?: { type: "permission"; id: string; option: string; peer: string } | { type: "command"; args: string[]; prompt?: string; lock?: ConsoleState["actionLock"] } | { type: "permission_default"; peer: string; source: string };
|
|
46
50
|
optionChoice?: string;
|
|
47
51
|
modeChoice?: string;
|
|
48
52
|
permissionDefaults: PermissionDefault[];
|
|
@@ -334,10 +338,10 @@ export const LAST_KEYS = [">", "\x1b[F", "\x1bOF", "\x1b[4~", "\x1b[8~"];
|
|
|
334
338
|
const KEYS: [string, string[]][] = [
|
|
335
339
|
["Everywhere", ["Tab stream/panels", ": command", "? keys", "Esc back", "q quit"]],
|
|
336
340
|
["Command", ["Enter run", "Esc cancel", "Up/Down history", "Ctrl-U clear"]],
|
|
337
|
-
["Panels", ["1-5 panel", "Left/Right panel", "j/k or Up/Down move", "Home/< first, End/> last", "Enter view", "j/k scroll a view", "w web UI", "o pet window"]],
|
|
341
|
+
["Panels", ["1-5 panel", "Left/Right panel", "j/k or Up/Down move", "Home/< first, End/> last", "Enter view", "j/k scroll a view", "w web UI (outside Peers)", "o pet window"]],
|
|
338
342
|
["Approvals", ["a allow (then y)", "d deny", "v view", "[ ] select"]],
|
|
339
|
-
["Peers", ["p pause", "r resume", "m permission mode", "l make planner (then y)"]],
|
|
340
|
-
["Tasks", ["a assign/
|
|
343
|
+
["Peers", ["p pause", "r resume", "m permission mode", "w WIP limit", "l make planner (then y)"]],
|
|
344
|
+
["Tasks", ["b backlog/open", "n add backlog", "a assign/reserve (then y)", "u unassign (then y)", "r review (in review)", "p pull, e plan (backlog)", "R return to backlog", "c cancel (then y)"]],
|
|
341
345
|
["Queue", ["r resolve (reason, then y)", "queued=discard, needs_review=retry"]],
|
|
342
346
|
["Events", ["f peer filter", "g kind filter"]],
|
|
343
347
|
["Settings", ["6 Settings panel", "e edit", "u undo", "preview, reason, then y"]],
|
|
@@ -371,7 +375,7 @@ function bound(s: ConsoleState): ConsoleState {
|
|
|
371
375
|
if (s.requestDetail && s.requestDetail !== s.approvalId) { s.requestDetail = s.approvalId; s.detailOffset = 0; }
|
|
372
376
|
return s;
|
|
373
377
|
}
|
|
374
|
-
const COMMANDS = new Set(["status", "board", "task", "review", "say", "pause", "resume", "budget", "queue", "permit", "permission", "ask", "remember", "route", "sdlc", "turns", "undo", "check-path", "report", "ui", "pet"]);
|
|
378
|
+
const COMMANDS = new Set(["status", "board", "task", "backlog", "review", "say", "pause", "resume", "budget", "queue", "permit", "permission", "ask", "remember", "route", "sdlc", "turns", "undo", "check-path", "report", "ui", "pet"]);
|
|
375
379
|
/** A tiny argv parser, never a shell. Quotes group arguments; backslash escapes one character. */
|
|
376
380
|
export function parseConsoleCommand(input: string): string[] {
|
|
377
381
|
const words: string[] = []; let word = ""; let quote = ""; let escaped = false; let started = false;
|
|
@@ -391,12 +395,13 @@ export function parseConsoleCommand(input: string): string[] {
|
|
|
391
395
|
}
|
|
392
396
|
export function commandNeedsConfirmation(args: string[]): boolean {
|
|
393
397
|
const positional = args.filter(arg => arg !== "--yes");
|
|
394
|
-
return (positional[0] === "permission" && positional[2] === "never-ask") || (args[0] === "task" && ["assign", "unassign", "cancel"].includes(args[1] ?? "")) || (args[0] === "queue" && args[1] === "resolve") || (args[0] === "permit" && args[2] !== "deny");
|
|
398
|
+
return (args[0] === "backlog" && args[1] !== "list") || (args[0] === "task" && args[1] === "propose" && args.includes("--backlog")) || (positional[0] === "permission" && positional[2] === "never-ask") || (args[0] === "task" && ["assign", "unassign", "cancel"].includes(args[1] ?? "")) || (args[0] === "queue" && args[1] === "resolve") || (args[0] === "permit" && args[2] !== "deny");
|
|
395
399
|
}
|
|
396
400
|
export function panelRows(s: ConsoleState): any[] {
|
|
397
401
|
if (s.panel === 1) return Object.entries(s.peers).map(([id, peer]) => ({ id, ...peer, budget: s.budget[id] }));
|
|
398
402
|
if (s.panel === 2) return s.approvals;
|
|
399
403
|
if (s.panel === 3) {
|
|
404
|
+
if (s.backlogView) return s.tasks.filter(t => t.state === "backlog").sort((a, b) => (a.rank ?? Number.MAX_SAFE_INTEGER) - (b.rank ?? Number.MAX_SAFE_INTEGER) || a.id - b.id);
|
|
400
405
|
const states = ["proposed", "in_progress", "in_review", "changes_requested"];
|
|
401
406
|
return s.tasks.filter(t => states.includes(t.state)).sort((a, b) => states.indexOf(a.state) - states.indexOf(b.state));
|
|
402
407
|
}
|
|
@@ -430,11 +435,20 @@ export function captureConsoleSelection(s: ConsoleState, byIndex = false): void
|
|
|
430
435
|
const commandFlag = (args: string[], name: string) => { const at = args.indexOf(name); return at < 0 ? undefined : args[at + 1]; };
|
|
431
436
|
function validateLockedCommand(lock: NonNullable<ConsoleState["actionLock"]>, args: string[]): void {
|
|
432
437
|
const revisions = args.flatMap((arg, index) => arg === "--revision" ? [args[index + 1]] : []);
|
|
433
|
-
|
|
438
|
+
const matches = lock.syntax === "planning" ? args[3] === String(lock.revision) : lock.syntax === "ownerless-return" ? args.length === 3 : revisions.length === 1 && revisions[0] === String(lock.revision);
|
|
439
|
+
if (args[0] !== lock.prefix[0] || args[1] !== lock.prefix[1] || args[2] !== lock.id || !matches) {
|
|
434
440
|
throw new Error("the selected id and revision cannot be edited; start again from the row");
|
|
435
441
|
}
|
|
436
442
|
}
|
|
443
|
+
function validateLockedRow(s: ConsoleState, lock: NonNullable<ConsoleState["actionLock"]>): void {
|
|
444
|
+
if (!lock.revisionField) return;
|
|
445
|
+
const row = s.tasks.find(row => String(row.id) === lock.id);
|
|
446
|
+
// Board rows omit a zero planning revision; flow revisions are always explicit in the console projection.
|
|
447
|
+
const revision = row && (lock.revisionField === "planningRevision" ? row.planningRevision ?? 0 : row.flowRevision);
|
|
448
|
+
if (!row || row.state !== lock.state || revision !== lock.revision) throw new Error("the selected task revision changed; reload and start again from the row");
|
|
449
|
+
}
|
|
437
450
|
function lockedCommandPrompt(lock: NonNullable<ConsoleState["actionLock"]>, args: string[]): string {
|
|
451
|
+
if (lock.prefix[0] === "backlog") return `${lock.prefix[1]} task #${lock.id} at ${lock.revisionField === "planningRevision" ? "planning " : ""}revision ${lock.revision}`;
|
|
438
452
|
if (lock.prefix[0] === "queue") {
|
|
439
453
|
const action = commandFlag(args, "--action") ?? "resolve";
|
|
440
454
|
return `${action} delivery ${lock.id} at revision ${lock.revision}${action === "completed" ? "; this asserts external effects" : action === "discard" ? "; no task or native turn is cancelled" : ""}`;
|
|
@@ -493,6 +507,9 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
493
507
|
return done();
|
|
494
508
|
}
|
|
495
509
|
if (key === "y") {
|
|
510
|
+
if (confirm.type === "command" && confirm.lock) {
|
|
511
|
+
try { validateLockedRow(s, confirm.lock); } catch (error) { notify(s, (error as Error).message, now); return done(); }
|
|
512
|
+
}
|
|
496
513
|
if (confirm.type === "command") effects.push({ type: "command", args: confirm.args[0] === "permission" && confirm.args[2] === "never-ask" ? [...confirm.args.filter(arg => arg !== "--yes"), "--yes"] : confirm.args });
|
|
497
514
|
else if (s.approvals.some(a => a.id === confirm.id && allowOptions(a).some(o => o.optionId === confirm.option))) {
|
|
498
515
|
effects.push({ type: "permit", id: confirm.id, option: confirm.option });
|
|
@@ -535,15 +552,23 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
535
552
|
}
|
|
536
553
|
if (lock) {
|
|
537
554
|
validateLockedCommand(lock, args);
|
|
555
|
+
validateLockedRow(s, lock);
|
|
556
|
+
if (lock.syntax === "planning" && args.length < (lock.prefix[1] === "plan" ? 6 : 5)) throw new Error("backlog row actions require a reason");
|
|
538
557
|
if (lock.prefix[0] === "task" && !["owner", "reviewer"].includes(commandFlag(args, "--role") ?? "owner")) throw new Error("role must be owner or reviewer");
|
|
539
558
|
}
|
|
540
559
|
s.history = [...s.history, input].slice(-100); s.historyIndex = s.history.length;
|
|
541
|
-
if (commandNeedsConfirmation(args)) s.confirm = { type: "command", args, ...(lock ? { prompt: lockedCommandPrompt(lock, args) } : {}) };
|
|
560
|
+
if (commandNeedsConfirmation(args)) s.confirm = { type: "command", args, ...(lock ? { prompt: lockedCommandPrompt(lock, args), ...(lock.revisionField ? { lock } : {}) } : {}) };
|
|
542
561
|
else effects.push({ type: "command", args });
|
|
543
562
|
} catch (error) { notify(s, String((error as Error).message), now); }
|
|
544
563
|
} else if (!key.startsWith("\x1b")) s.input += terminalText(key).replace(/\n/g, "");
|
|
545
564
|
return done();
|
|
546
565
|
}
|
|
566
|
+
if (s.mode === "panels" && s.panel === 3 && key === "b") {
|
|
567
|
+
s.backlogView = !s.backlogView; s.selection = 0; s.taskSelection = undefined; s.detail = undefined; s.detailOffset = 0; return done();
|
|
568
|
+
}
|
|
569
|
+
if (s.mode === "panels" && s.panel === 3 && key === "n" && s.detail === undefined) {
|
|
570
|
+
s.input = "backlog add implement "; s.editing = true; return done();
|
|
571
|
+
}
|
|
547
572
|
if ((key === "n" || key === "P") && attentionKeyBlocked) return done();
|
|
548
573
|
if (key === "q") { effects.push({ type: "exit" }); return done(); }
|
|
549
574
|
if (key === "P" && !s.optionChoice && !s.help) {
|
|
@@ -572,9 +597,9 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
572
597
|
const selected = item.id === undefined ? -1 : rows.findIndex(row => row.id === item.id);
|
|
573
598
|
if (selected >= 0) { s.panel = 4; s.selection = selected; navigated = true; }
|
|
574
599
|
} else if (item.kind === "review" && s.mode === "panels") {
|
|
575
|
-
const rows = panelRows({ ...s, panel: 3 });
|
|
600
|
+
const rows = panelRows({ ...s, panel: 3, backlogView: false });
|
|
576
601
|
const selected = rows.findIndex(row => row.id === item.id);
|
|
577
|
-
if (selected >= 0) { s.panel = 3; s.selection = selected; navigated = true; }
|
|
602
|
+
if (selected >= 0) { s.panel = 3; s.backlogView = false; s.selection = selected; navigated = true; }
|
|
578
603
|
}
|
|
579
604
|
if (navigated) { captureConsoleSelection(s, true); s.detail = undefined; s.requestDetail = undefined; }
|
|
580
605
|
else {
|
|
@@ -592,6 +617,17 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
592
617
|
// Right and Left are the digit of the next or previous tab, wrapping at both ends (#309); the same guards as the
|
|
593
618
|
// digits apply (the confirm, mode-choice, key-table and editing branches above returned already).
|
|
594
619
|
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);
|
|
620
|
+
if (s.mode === "panels" && s.panel === 1 && key === "w") {
|
|
621
|
+
const peer = selectedPanelRow(s)?.id;
|
|
622
|
+
if (!peer || s.detail !== undefined) { notify(s, "select a peer row to edit its planner WIP limit", now); return done(); }
|
|
623
|
+
const wip = s.taskWip?.[peer];
|
|
624
|
+
if (!wip || s.peers[peer]?.rostered === false) { notify(s, `planner WIP is unavailable for ${peer}`, now); return done(); }
|
|
625
|
+
const row = s.settingsRows?.find(row => row.key === `tasks.wip_limits.${peer}`);
|
|
626
|
+
if (!row || s.settingsProblem) { notify(s, s.settingsProblem ?? "WIP setting is unavailable; refresh settings and try again", now); return done(); }
|
|
627
|
+
const token = (s.settingsSequence ?? 0) + 1; s.settingsSequence = token;
|
|
628
|
+
s.settingsEdit = { token, panel: 1, key: row.key, undo: false, phase: "value", text: String(wip.wipLimit) };
|
|
629
|
+
return done();
|
|
630
|
+
}
|
|
595
631
|
if (s.mode === "panels" && s.panel === 6 && s.detail === undefined && (key === "e" || key === "u")) {
|
|
596
632
|
const row = selectedPanelRow(s);
|
|
597
633
|
if (key === "u" && !s.settingsUndo) { notify(s, "nothing to undo", now); return done(); }
|
|
@@ -666,6 +702,16 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
666
702
|
return done();
|
|
667
703
|
}
|
|
668
704
|
if (s.mode === "panels" && item !== undefined && item !== null) {
|
|
705
|
+
if (s.panel === 3 && (selected.state === "backlog" && ["p", "a", "e"].includes(key) || key === "R" && ["proposed", "in_progress"].includes(selected.state))) {
|
|
706
|
+
const ownerless = key === "R" && selected.state === "proposed" && !selected.owner;
|
|
707
|
+
const revision = ownerless ? selected.flowRevision : selected.planningRevision ?? 0;
|
|
708
|
+
if (!Number.isSafeInteger(revision) || revision < 0) { notify(s, "backlog row actions need a valid revision; refresh the board", now); return done(); }
|
|
709
|
+
const sub = key === "R" ? "return" : key === "p" ? "pull-ready" : "plan";
|
|
710
|
+
s.actionLock = { prefix: ["backlog", sub], id: String(item), revision, state: String(selected.state), syntax: ownerless ? "ownerless-return" : "planning", revisionField: ownerless ? "flowRevision" : "planningRevision" };
|
|
711
|
+
const fields = key === "a" ? { reserved: selected.reserved ?? "" } : { priority: selected.priority ?? null, rank: selected.rank ?? null, deps: selected.deps ?? [] };
|
|
712
|
+
s.input = ownerless ? `backlog return ${item}` : `backlog ${sub} ${item} ${revision} ${sub === "plan" ? `'${JSON.stringify(fields)}' ` : ""}`;
|
|
713
|
+
s.editing = true; return done();
|
|
714
|
+
}
|
|
669
715
|
if (s.panel === 1 && key === "m") {
|
|
670
716
|
const peer = String(item);
|
|
671
717
|
const info = s.peers[peer];
|
|
@@ -690,7 +736,7 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
690
736
|
const revision = selected.flowRevision;
|
|
691
737
|
if (!["proposed", "in_progress", "in_review", "changes_requested", "backlog"].includes(selected.state)) return done();
|
|
692
738
|
if (!Number.isSafeInteger(revision) || revision < 0) { notify(s, "task row actions need flowRevision; upgrade the hub", now); return done(); }
|
|
693
|
-
s.actionLock = { prefix: ["task", "cancel"], id: String(item), revision, state: String(selected.state) };
|
|
739
|
+
s.actionLock = { prefix: ["task", "cancel"], id: String(item), revision, state: String(selected.state), ...(selected.state === "backlog" ? { revisionField: "flowRevision" as const } : {}) };
|
|
694
740
|
s.input = `task cancel ${item} --revision ${revision} --reason `; s.editing = true; return done();
|
|
695
741
|
}
|
|
696
742
|
if (s.panel === 3 && (key === "a" || key === "u")) {
|
|
@@ -732,7 +778,7 @@ export function activeNotice(s: Pick<ConsoleState, "notice" | "noticeAt">, now =
|
|
|
732
778
|
return !!s.notice && now - (s.noticeAt ?? now) < NOTICE_MS;
|
|
733
779
|
}
|
|
734
780
|
export const TABLES: Record<number, string[]> = {
|
|
735
|
-
1: ["PEER", "STATE", "LINK", "Q", "!", "REVIEW", "PAUSE", "QUOTA", "MODEL"],
|
|
781
|
+
1: ["PEER", "STATE", "LINK", "Q", "!", "WIP", "REVIEW", "PAUSE", "QUOTA", "MODEL"],
|
|
736
782
|
2: ["ID", "PEER", "LEFT", "TITLE"],
|
|
737
783
|
3: ["ID", "STATE", "OWNER", "REVIEWER", "CLASS", "AGE", "TITLE", "STAGE"],
|
|
738
784
|
4: ["ID", "PEER", "STATE", "REV", "AGE"],
|
|
@@ -740,11 +786,14 @@ export const TABLES: Record<number, string[]> = {
|
|
|
740
786
|
};
|
|
741
787
|
const showPermissionModes = (s: ConsoleState) => Object.values(s.peers).some(peer => peer.permissionMode && peer.permissionMode !== "ask");
|
|
742
788
|
const count = (n: unknown) => typeof n === "number" && n ? String(n) : "-";
|
|
789
|
+
const BACKLOG_COLUMNS = ["ID", "PRI", "RESERVED", "DEPS", "CLASS", "AGE", "TITLE"];
|
|
790
|
+
const waitingDeps = (s: ConsoleState, row: any): number[] => (row.deps ?? []).filter((id: number) => !s.tasks.some(task => task.id === id && task.state === "approved"));
|
|
743
791
|
/** One span per column of a panel row; zero counters and unknown values read `-`. */
|
|
744
792
|
export function cells(s: ConsoleState, row: any, selected: boolean, now: number, stages?: Map<number, ProgressStage>): Span[] {
|
|
745
793
|
const id = (text: unknown, tone: Tone | undefined) => span(text, tone && (selected ? "strong" : tone));
|
|
746
794
|
if (s.panel === 1) {
|
|
747
795
|
const b = s.budget[row.id];
|
|
796
|
+
const wip = s.taskWip?.[row.id];
|
|
748
797
|
const paused = b?.paused ? `budget ${relative(b.paused.resetsAt, now)}` : row.paused && typeof row.paused === "object"
|
|
749
798
|
? `${row.paused.by ?? "paused"}${typeof row.paused.at === "number" ? ` ${duration(now - row.paused.at)}` : ""}`
|
|
750
799
|
: row.paused === "manual" ? "user" : row.paused ? String(row.paused).split(":")[0] : "-";
|
|
@@ -752,11 +801,16 @@ export function cells(s: ConsoleState, row: any, selected: boolean, now: number,
|
|
|
752
801
|
...(showPermissionModes(s) ? [span(row.permissionMode && row.permissionMode !== "ask" ? row.permissionMode : "-", row.permissionMode === "never-ask" ? "attention" : undefined)] : []),
|
|
753
802
|
span(row.pullOnly && definitionOf(row.id)?.attach?.pullOnlyForms?.includes("tui") ? "pull-only" : row.toolsOnly ? "tools-only" : row.attached === false ? "detached" : "attached", row.toolsOnly ? "attention" : undefined),
|
|
754
803
|
span(count(row.queued)), span(count(row.queuedImportant), row.queuedImportant ? "attention" : undefined),
|
|
804
|
+
span(wip ? `${wip.wip}/${wip.wipLimit}` : "-", wip && wip.wip >= wip.wipLimit ? "attention" : undefined),
|
|
755
805
|
span(count(row.needsReview), row.needsReview ? "failure" : undefined), span(paused, paused === "-" ? undefined : "attention"),
|
|
756
806
|
span(b?.windows?.length ? b.windows.map((w: any) => `${w.id} ${Math.round(w.used * 100)}%${typeof w.resetsAt === "number" ? ` ${relative(w.resetsAt, now)}` : ""}`).join(", ") : "-"),
|
|
757
807
|
span(row.servedBy ?? row.requestedModel ?? "-")];
|
|
758
808
|
}
|
|
759
809
|
if (s.panel === 2) return [id(row.id, "attention"), span(row.peer, "attention"), span(duration(row.expiresAt - now), "muted"), span(row.title)];
|
|
810
|
+
if (s.panel === 3 && s.backlogView) {
|
|
811
|
+
const waits = waitingDeps(s, row);
|
|
812
|
+
return [id(`#${row.id}`, "info"), span(`${row.priority ?? "-"}/${row.rank ?? "-"}`), span(row.reserved ?? "-"), span(waits.length ? waits.map(id => `#${id}`).join(",") : "ready", waits.length ? "attention" : "success"), span(row.class ?? "-"), span(typeof row.created === "number" ? duration(now - row.created) : "-", "muted"), span(row.pii ? "[pii]" : row.title)];
|
|
813
|
+
}
|
|
760
814
|
if (s.panel === 3) {
|
|
761
815
|
const last = row.history?.at(-1);
|
|
762
816
|
const at = last?.at ?? row.updated ?? row.created;
|
|
@@ -801,20 +855,41 @@ export function table(head: string[], rows: Span[][], columns: number, explicitW
|
|
|
801
855
|
return { ...cell, text: text + " ".repeat(Math.max(0, widths[i]! - Bun.stringWidth(text)) + (i === head.length - 1 ? 0 : 2)) };
|
|
802
856
|
}));
|
|
803
857
|
}
|
|
858
|
+
/** Keep the status/quota columns visible instead of clipping them behind optional peer metadata. */
|
|
859
|
+
function optionalTable(head: string[], rows: Span[][], columns: number, drop = ["MODEL", "WIP", "PAUSE", "REVIEW", "LINK", "Q", "!", "MODE"]): Span[][] {
|
|
860
|
+
const cap = Math.max(24, Math.floor(columns / 3));
|
|
861
|
+
const widths = head.map((name, i) => Math.min(cap, rows.reduce((max, row) => Math.max(max, Bun.stringWidth(flat(row[i]?.text ?? ""))), name.length)));
|
|
862
|
+
const keep = head.map((_, i) => i);
|
|
863
|
+
const total = () => keep.reduce((sum, i) => sum + widths[i]!, 0) + 2 * (keep.length - 1);
|
|
864
|
+
for (const name of drop) {
|
|
865
|
+
if (total() <= columns - 2) break;
|
|
866
|
+
const at = keep.findIndex(i => head[i] === name);
|
|
867
|
+
if (at >= 0) keep.splice(at, 1);
|
|
868
|
+
}
|
|
869
|
+
// Very long peer/state/quota values still fit, with their headers intact.
|
|
870
|
+
while (total() > columns - 2) {
|
|
871
|
+
const largest = keep.reduce((best, i) => widths[i]! - head[i]!.length > widths[best]! - head[best]!.length ? i : best, keep[0]!);
|
|
872
|
+
if (widths[largest]! <= head[largest]!.length) break;
|
|
873
|
+
widths[largest] = widths[largest]! - 1;
|
|
874
|
+
}
|
|
875
|
+
return table(keep.map(i => head[i]!), rows.map(row => keep.map(i => row[i]!)), columns, keep.map(i => widths[i]!));
|
|
876
|
+
}
|
|
804
877
|
function taskHints(row: any): string[] {
|
|
878
|
+
if (row.state === "backlog") return ["p pull", "a reserve", "e plan"];
|
|
805
879
|
if (!["proposed", "in_progress", "in_review", "changes_requested"].includes(row.state)) return [];
|
|
806
880
|
const review = row.state === "in_review";
|
|
807
881
|
const hints = [review ? "a reviewer" : "a assign"];
|
|
808
882
|
if (review ? row.reviewer : row.owner || row.reserved) hints.push("u unassign");
|
|
809
883
|
if (review) hints.push("r review");
|
|
884
|
+
if (["proposed", "in_progress"].includes(row.state)) hints.push("R return");
|
|
810
885
|
return hints;
|
|
811
886
|
}
|
|
812
887
|
const settingValueText = (value: unknown): string => value === null || value === undefined ? "inherit" : Array.isArray(value) ? value.join(",") : String(value);
|
|
813
888
|
function panelContext(s: ConsoleState, row: any): string[] {
|
|
889
|
+
if (s.panel === 3) return [...(row ? taskHints(row) : []), ...(row && s.features?.includes("task_cancel") ? ["c cancel"] : []), s.backlogView ? "b open" : "b backlog", "n add"];
|
|
814
890
|
if (!row) return [];
|
|
815
891
|
if (s.panel === 6) return ["e edit", ...(s.settingsUndo ? ["u undo"] : [])];
|
|
816
|
-
if (s.panel === 1) return ["p pause", "r resume", "m permission mode"];
|
|
817
|
-
if (s.panel === 3) return [...taskHints(row), ...(s.features?.includes("task_cancel") ? ["c cancel"] : [])];
|
|
892
|
+
if (s.panel === 1) return ["p pause", "r resume", "m permission mode", "w WIP limit"];
|
|
818
893
|
if (s.panel === 4) return [row.state === "queued" ? "r discard" : "r resolve"];
|
|
819
894
|
return [];
|
|
820
895
|
}
|
|
@@ -827,7 +902,14 @@ function hint(s: ConsoleState, columns: number): string {
|
|
|
827
902
|
if (s.detail !== undefined || s.requestDetail) return [...approvals, "j/k scroll", "Esc back", "? keys", "q quit"].join(" ");
|
|
828
903
|
const rows = panelRows(s); const row = selectedPanelRow(s);
|
|
829
904
|
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(" ");
|
|
830
|
-
|
|
905
|
+
if (s.panel === 3 && Bun.stringWidth(text) > columns) {
|
|
906
|
+
const compact = [...panelContext(s, row), ...(rows.length ? ["j/k"] : []), "? keys", "q quit"];
|
|
907
|
+
if (Bun.stringWidth(compact.join(" ")) > columns) return compact.filter(key => key !== "j/k").join(" ");
|
|
908
|
+
return compact.join(" ");
|
|
909
|
+
}
|
|
910
|
+
if (s.panel !== 1 || Bun.stringWidth(text) <= columns) return text;
|
|
911
|
+
const compact = text.replace("m permission mode", "m mode").replace("w WIP limit", "w WIP").replace("j/k move", "j/k").replace(": command", ": cmd");
|
|
912
|
+
return Bun.stringWidth(compact) > columns ? compact.replace("Enter view", "Enter") : compact;
|
|
831
913
|
}
|
|
832
914
|
/** The project, then the tabs: the active one in brackets, pending approvals and held deliveries counted. */
|
|
833
915
|
function header(s: ConsoleState, columns: number): Span[] {
|
|
@@ -927,7 +1009,9 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
927
1009
|
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());
|
|
928
1010
|
else if (s.settingsEdit) {
|
|
929
1011
|
const edit = s.settingsEdit;
|
|
930
|
-
|
|
1012
|
+
const row = s.settingsRows?.find(row => row.key === edit.key);
|
|
1013
|
+
const definition: [string, unknown][] = row ? [["type", row.type], ...(row.min !== undefined || row.max !== undefined ? [["range", `${row.min ?? "-"}..${row.max ?? "-"}`] as [string, unknown]] : []), ["risk", row.risk], ["applies", row.applies]] : [];
|
|
1014
|
+
lines.push(...fieldLines([["setting", edit.key ?? s.settingsUndo ?? "last write"], ...definition, ["preview", (edit.lines ?? []).join("\n")], ["decision", "Esc cancels; saving needs a reason and explicit y"]], columns, now).flat());
|
|
931
1015
|
}
|
|
932
1016
|
else if (s.help) lines.push(...keyTable(columns, !!s.petEnabled || !!s.petChoices?.length, !!s.petChoices?.length));
|
|
933
1017
|
else if (s.detail !== undefined || (s.requestDetail && permission)) {
|
|
@@ -935,7 +1019,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
935
1019
|
lines.push(...detail.slice(Math.min(s.detailOffset, Math.max(0, detail.length - height))));
|
|
936
1020
|
}
|
|
937
1021
|
else {
|
|
938
|
-
const data = panelRows(s); const head = s.panel === 1 && showPermissionModes(s) ? ["PEER", "STATE", "MODE", ...TABLES[1]!.slice(2)] : TABLES[s.panel]; const at = selection(s);
|
|
1022
|
+
const data = panelRows(s); const head = s.panel === 3 && s.backlogView ? BACKLOG_COLUMNS : s.panel === 1 && showPermissionModes(s) ? ["PEER", "STATE", "MODE", ...TABLES[1]!.slice(2)] : TABLES[s.panel]; const at = selection(s);
|
|
939
1023
|
// Below the Approvals table: the selected request's title, its allow options and deny, one line each. Deny always
|
|
940
1024
|
// shows; options that do not fit are counted, rows give way to the rest, and the title gets what is left.
|
|
941
1025
|
const [title = [], allow = [], deny = []] = s.panel === 2 && permission ? fieldLines(approvalFields(permission).slice(3), columns, now) : [];
|
|
@@ -944,7 +1028,8 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
944
1028
|
const tail = [...allow, ...deny];
|
|
945
1029
|
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);
|
|
946
1030
|
const stages = new Map(progress.stages.map(stage => [stage.id, stage]));
|
|
947
|
-
const
|
|
1031
|
+
const cellRows = head ? data.map((row, i) => cells(s, row, i === at, now, stages)) : [];
|
|
1032
|
+
const rowsOf = head ? s.panel === 1 ? optionalTable(head, cellRows, columns) : s.panel === 3 && s.backlogView ? optionalTable(head, cellRows, columns, ["AGE", "CLASS", "DEPS", "PRI"]) : table(head, cellRows, columns) : undefined;
|
|
948
1033
|
if (s.panel === 6) {
|
|
949
1034
|
const row = selectedPanelRow(s);
|
|
950
1035
|
const info = s.settingsProblem ?? (!s.settingsKnown ? "settings loading..." : row?.restartReason ?? row?.note ?? row?.label ?? "Settings");
|
|
@@ -955,7 +1040,9 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
955
1040
|
for (const pending of s.retainedConfig) lines.push(fitLine([span(`${pending.key}: ${pending.reason}`, "attention")], columns));
|
|
956
1041
|
}
|
|
957
1042
|
}
|
|
958
|
-
if (s.panel === 3) {
|
|
1043
|
+
if (s.panel === 3 && s.backlogView) {
|
|
1044
|
+
lines.push(fitLine([span(s.tasksKnown === false ? "backlog loading..." : `backlog ${data.length}`, s.tasksKnown === false ? "attention" : "info")], columns));
|
|
1045
|
+
} else if (s.panel === 3) {
|
|
959
1046
|
const done = progress.total ? Math.floor(progress.counts.approved * 20 / progress.total) : 0;
|
|
960
1047
|
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}%`;
|
|
961
1048
|
const known = s.tasksKnown !== false || progress.total > 0;
|
package/src/cli/console.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface ConsoleOptions {
|
|
|
31
31
|
/** Explicit delegation inherited only by this console's ahub subprocesses. */
|
|
32
32
|
liveTest?: boolean;
|
|
33
33
|
/** Injected command runner must use argv, closed stdin, and return a cancellation handle. */
|
|
34
|
-
runCommand?: (args: string[], output: (text: string) => void, done: () => void, columns?: number) => (() => void);
|
|
34
|
+
runCommand?: (args: string[], output: (text: string) => void, done: (code?: number) => void, columns?: number) => (() => void);
|
|
35
35
|
pollMs?: number;
|
|
36
36
|
}
|
|
37
37
|
/** Exact navigation sequences only (#309, #441); anything else in a multi-character read is a paste. */
|
|
@@ -190,7 +190,7 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
190
190
|
const replies = await Promise.all([
|
|
191
191
|
client.request({ t: "status" }, 3000), client.request({ t: "budget" }, 3000),
|
|
192
192
|
...(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)] : []),
|
|
193
|
-
...(panels && state.panel === 6 ? [client.request({ t: "settings_get" }, 3000)] : []),
|
|
193
|
+
...(panels && (state.panel === 1 || state.panel === 6) ? [client.request({ t: "settings_get" }, 3000)] : []),
|
|
194
194
|
]);
|
|
195
195
|
if (!active) return;
|
|
196
196
|
const [status, budget, tasks, queue, ready, settings] = replies;
|
|
@@ -210,6 +210,7 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
210
210
|
if (status.status) state.features = Array.isArray(status.status.features) ? status.status.features : [];
|
|
211
211
|
if (status.status?.peers) state.peers = Object.fromEntries(Object.entries(status.status.peers).map(([peer, value]) => [peer, { ...(value as object), ...status.status.roleInfo?.[peer] }]));
|
|
212
212
|
if (status.status && status.ok !== false) {
|
|
213
|
+
state.taskWip = status.status.taskWip ?? {};
|
|
213
214
|
state.attentionFacts = attentionFacts(status.status.attentionItems);
|
|
214
215
|
state.storageBlocked = !!status.status.deliveryError;
|
|
215
216
|
state.recoveryNeed = status.status.recovery ? { id: status.status.recovery.operationId, running: true } : undefined;
|
|
@@ -249,7 +250,8 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
249
250
|
try { while (true) { const { value, done } = await reader.read(); if (done) break; output(decoder.decode(value, { stream: true })); } const end = decoder.decode(); if (end) output(end); }
|
|
250
251
|
finally { reader.releaseLock(); }
|
|
251
252
|
};
|
|
252
|
-
|
|
253
|
+
let exitCode = 0;
|
|
254
|
+
void Promise.all([read(child.stdout), read(child.stderr), child.exited]).then(([, , code]) => { exitCode = code; if (code) output(`command exited ${code}`); }).catch(error => { exitCode = 1; output(String(error.message)); }).finally(() => finished(exitCode));
|
|
253
255
|
return () => { child.kill(); };
|
|
254
256
|
});
|
|
255
257
|
const effect = async (action: ConsoleEffect) => {
|
|
@@ -257,7 +259,7 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
257
259
|
if (action.type === "settings_preview") {
|
|
258
260
|
const response = await client.request(action.undo ? { t: "settings_preview", undo: true } : { t: "settings_preview", key: action.key, value: action.value }, 3000);
|
|
259
261
|
const edit = state.settingsEdit;
|
|
260
|
-
if (!active || state.mode !== "panels" || state.panel !==
|
|
262
|
+
if (!active || state.mode !== "panels" || !edit || state.panel !== (edit.panel ?? 6) || 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;
|
|
261
263
|
if (response.ok === false) { state.settingsEdit = undefined; notice(String(response.error ?? "setting preview refused")); }
|
|
262
264
|
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))) {
|
|
263
265
|
state.settingsEdit = undefined; notice("undo preview is unavailable or changed; reload settings and review it again");
|
|
@@ -299,9 +301,10 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
299
301
|
childRunning = true;
|
|
300
302
|
stream({ text: `> ${action.args.join(" ")}`, kind: "command" });
|
|
301
303
|
try {
|
|
304
|
+
let diagnostic = "";
|
|
302
305
|
// Output lines sit at column 4, as message bodies do: what a command prints can carry peer text, and columns 0 and 2 are the hub's.
|
|
303
|
-
const output = (text: string) => { if (active) { stream({ text: terminalText(text).replace(/\t/g, " ").replace(/\n$/, "").replace(/^/gm, " "), kind: "command" }); draw(); } };
|
|
304
|
-
cancelChild = run(action.args, output, () => { childRunning = false; cancelChild = undefined; if (active) void refresh(); }, Math.max(1, columns - 4));
|
|
306
|
+
const output = (text: string) => { if (active) { diagnostic = (diagnostic + terminalText(text)).slice(-2000); stream({ text: terminalText(text).replace(/\t/g, " ").replace(/\n$/, "").replace(/^/gm, " "), kind: "command" }); draw(); } };
|
|
307
|
+
cancelChild = run(action.args, output, (code = 0) => { childRunning = false; cancelChild = undefined; if (active) { if (code) notice(diagnostic.trim() || `command exited ${code}`); void refresh(); } }, Math.max(1, columns - 4));
|
|
305
308
|
} catch (error) { childRunning = false; notice(String((error as Error).message)); draw(); }
|
|
306
309
|
};
|
|
307
310
|
const handle = (key: string) => {
|
package/src/cli/help.ts
CHANGED
|
@@ -14,6 +14,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
14
14
|
["ahub --live-test [--project <path|id>] <command> [args...]", "user-authorized operational console delegation; ordinary calls retain peer identity; person-authored content refuses"],
|
|
15
15
|
["ahub help [command]", "this help, or one command's entries; also --help and -h"],
|
|
16
16
|
["ahub version | --version", "print the installed version"],
|
|
17
|
+
["ahub completion zsh|bash|fish", "print a shell completion script; its candidates come from these help entries"],
|
|
17
18
|
]],
|
|
18
19
|
["Daemon and runtime", [
|
|
19
20
|
["ahub up [--unattended] [--no-console]", "start the daemon; interactive terminals enter console"],
|
|
@@ -51,7 +52,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
51
52
|
["ahub codex [--print-command] [--unattended] [args...]", "start the Codex adapter and attach the TUI"],
|
|
52
53
|
["ahub kimi [--print-command] [--mode tui|headless] [--model <alias>]", "default: Kimi's own TUI, pull-only via hub_inbox; no push, steer, watchdog, console approvals or automatic quota handoff. Select headless for ACP"],
|
|
53
54
|
["ahub jcode [--mode tui|headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>] [--print-command]", "optional Jcode (roster.jcode, default off): its TUI on the hub-owned ACP session, opened once that session is saved; --mode headless for ACP only; native policy unmanaged, no hub sandbox"],
|
|
54
|
-
["ahub agy [--print-command | --dry-run] [args...]", "optional Antigravity CLI 1.4.
|
|
55
|
+
["ahub agy [--print-command | --dry-run] [args...]", "optional Antigravity CLI 1.4.x TUI (1.4.2 verified) in this terminal (roster.agy, default off): pull-only via hub_inbox, implementer/verifier only; native permissions unmanaged, no hub sandbox"],
|
|
55
56
|
["ahub qwen [--mode headless] [--model <alias>] [--session-id <id>] [--print-command]", "start optional Qwen Code 0.25.0 ACP (roster.qwen, default off)"],
|
|
56
57
|
["ahub pi [--print-command] [--mode headless|tui | --headless] [--backend auto|dgx|mlx] [--model <alias>] [--session-id <id>] [--session-file <path>] [--replace-session]", "start Pi: its TUI in this terminal unless --headless or the start mode setting (peers.pi.start_mode) says headless; without a terminal the hub opens one or says how"],
|
|
57
58
|
["ahub local [--route <id> | --model <id>]", "start the hub-native worker on the self-hosted models (routing.toml)"],
|
|
@@ -88,7 +89,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
88
89
|
["ahub backlog pull|return <id>", "person only: pull backlog into To Do or return ownerless proposed work; --backlog on task propose stages unoffered work"],
|
|
89
90
|
["ahub task show|escalate <id>", "full task with history (PII text included) / hand it to the next peer in escalate_to"],
|
|
90
91
|
["ahub task role <id> <role|none>", "person only: change bounded catalog guidance on an open task; routing and authority are unchanged"],
|
|
91
|
-
["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"],
|
|
92
|
+
["ahub task assign <id> <peer> [--role owner|reviewer] [--revision N]", "name the owner (default) or reviewer; a task that still waits reserves its owner; a peer that cannot take it is refused with the reason; --revision refuses stale console actions"],
|
|
92
93
|
["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"],
|
|
93
94
|
["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"],
|
|
94
95
|
["ahub task label <id> ok|regressed|reverted|incomplete|wrong|abandoned [--created <time>]", "your later verdict on an approved task, kept in the research records (research must be on)"],
|
package/src/cli/identity.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function classifyPeerCommand(command: string, args: readonly string[]): P
|
|
|
77
77
|
if (native?.cli.authority === "person") return "console";
|
|
78
78
|
if (native || ["pause", "resume"].includes(command)) return "conductor";
|
|
79
79
|
if (command === "board" && args[0] === "import") return "console";
|
|
80
|
-
if (["help", "version", "--version", "say", "status", "pet", "board", "report", "turns", "review", "remember", "facts", "check-path"].includes(command)) return "allowed";
|
|
80
|
+
if (["help", "version", "--version", "completion", "say", "status", "pet", "board", "report", "turns", "review", "remember", "facts", "check-path"].includes(command)) return "allowed";
|
|
81
81
|
return "console";
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -86,7 +86,7 @@ export function cliCommandLabel(command: string, args: readonly string[]): strin
|
|
|
86
86
|
if (command === "backlog") return ["list", "add", "pull", "pull-ready", "split", "return", "plan", "pin", "undo"].includes(args[0] ?? "") ? `backlog ${args[0]}` : "backlog";
|
|
87
87
|
if (command === "board" && args[0] === "import") return "board import";
|
|
88
88
|
if (command === "release") return ["new", "list", "show", "plan", "activate", "freeze", "ship", "drop"].includes(args[0] ?? "") ? `release ${args[0]}` : "release";
|
|
89
|
-
if (!definitionOf(command) && !["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "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";
|
|
89
|
+
if (!definitionOf(command) && !["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "completion", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "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";
|
|
90
90
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
91
91
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
92
92
|
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}`;
|
package/src/cli/main.js
CHANGED
|
@@ -6,4 +6,8 @@ if (!version || !(Number(version[1]) > 1 || (Number(version[1]) === 1 && (Number
|
|
|
6
6
|
process.exit(1);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Shell completion asks on every Tab: answer from the help entries without loading the CLI (#603).
|
|
10
|
+
if (process.argv[2] === "__complete") {
|
|
11
|
+
const { completeWords } = await import("./completion.ts");
|
|
12
|
+
console.log(completeWords(process.argv.slice(process.argv[3] === "--" ? 4 : 3)).join("\n"));
|
|
13
|
+
} else await import("./main.ts");
|
package/src/cli/main.ts
CHANGED
|
@@ -73,6 +73,7 @@ import { addonReport, appendRecords, formatAddons, formatResearch, LABELS, label
|
|
|
73
73
|
import { summarize, summarizeByTask } from "../hub/report.ts";
|
|
74
74
|
import { hasTree, planUndo, repoOf, restore, Turns } from "../hub/snapshots.ts";
|
|
75
75
|
import { pathWarnings } from "../hub/conflicts.ts";
|
|
76
|
+
import { COMPLETION_SHELLS, completionScript } from "./completion.ts";
|
|
76
77
|
import { classifyPeerCommand, cliCommandLabel, detectCliIdentity, LIVE_TEST_PERSON_REFUSAL, liveTestRefusal, peerCommandRefusal } from "./identity.ts";
|
|
77
78
|
import { recordCliAudit, recordDelegatedCliAudit } from "./identity-audit.ts";
|
|
78
79
|
import { runConsole } from "./console.ts";
|
|
@@ -765,6 +766,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
765
766
|
if (!text) fail(`unknown command "${topic}"; run ahub help`);
|
|
766
767
|
console.log(text);
|
|
767
768
|
},
|
|
769
|
+
completion: () => {
|
|
770
|
+
const script = completionScript(args[0]);
|
|
771
|
+
if (args.length !== 1 || !script) fail(`usage: ahub completion ${COMPLETION_SHELLS.join("|")}`);
|
|
772
|
+
process.stdout.write(script!);
|
|
773
|
+
},
|
|
768
774
|
"--version": () => console.log(VERSION),
|
|
769
775
|
version: () => console.log(VERSION),
|
|
770
776
|
|
|
@@ -179,6 +179,8 @@ const MAX_WAIT_MS = 10 * 60 * 1000;
|
|
|
179
179
|
export const MAX_COMMAND_MS = 30_000;
|
|
180
180
|
/** #215: a replacement's readiness is awaited in slices this long, checking between them whether its launcher still runs. */
|
|
181
181
|
const EXIT_SLICE_MS = 5_000;
|
|
182
|
+
/** #599: after the replacement reads idle, its session mapping is re-read this often until the deadline. */
|
|
183
|
+
const MAP_POLL_MS = 1_000;
|
|
182
184
|
const ALLOWED_ENV = ["CODEX_HOME", "CLAUDE_CONFIG_DIR"] as const;
|
|
183
185
|
type AllowedEnvName = (typeof ALLOWED_ENV)[number];
|
|
184
186
|
|
|
@@ -909,16 +911,23 @@ export async function createTerminal(binding: TerminalBinding, options?: Command
|
|
|
909
911
|
idle = await waitForIdle(replacement, slice(), config);
|
|
910
912
|
}
|
|
911
913
|
if (!idle.satisfied) return { created: true, ready: false, manualRequired: true, blockers: idle.blockers };
|
|
912
|
-
// An idle terminal may be the login shell the launcher returned to.
|
|
913
|
-
const launch = await launches();
|
|
914
|
-
if (launch.gone) return exited("exited");
|
|
915
|
-
const shown = await showBinding(replacement, config);
|
|
916
|
-
const identity = identityFrom(shown);
|
|
917
|
-
const session = sessionFrom(shown);
|
|
918
|
-
const recorded = launch.live;
|
|
919
914
|
const same = (value: string | undefined) => anySession || value === binding.sessionId;
|
|
920
|
-
|
|
921
|
-
|
|
915
|
+
let session: string | undefined;
|
|
916
|
+
// An idle terminal may be the login shell the launcher returned to, or a TUI that has not yet recorded its
|
|
917
|
+
// launch or been identified by Orca (#599): poll until it maps, contradicts, or the deadline passes.
|
|
918
|
+
for (;;) {
|
|
919
|
+
const launch = await launches();
|
|
920
|
+
if (launch.gone) return exited("exited");
|
|
921
|
+
const shown = await showBinding(replacement, config);
|
|
922
|
+
const identity = identityFrom(shown);
|
|
923
|
+
session = sessionFrom(shown);
|
|
924
|
+
const recorded = launch.live;
|
|
925
|
+
if ((identity === binding.peer && same(session)) || (recorded && (identity === undefined || identity === binding.peer) && (session === undefined || same(session)))) break;
|
|
926
|
+
const contradicts = (identity !== undefined && identity !== binding.peer) || (session !== undefined && !same(session));
|
|
927
|
+
if (contradicts || Date.now() >= deadline) {
|
|
928
|
+
return { created: true, ready: true, manualRequired: true, blockers: [blocker("ambiguous-create", `created terminal ${createdHandle} cannot be mapped to the original ${binding.peer} session`, binding.peer, createdHandle)] };
|
|
929
|
+
}
|
|
930
|
+
await Bun.sleep(Math.max(1, Math.min(MAP_POLL_MS, deadline - Date.now())));
|
|
922
931
|
}
|
|
923
932
|
if (anySession && session) replacement = { ...replacement, sessionId: session };
|
|
924
933
|
return { created: true, ready: true, manualRequired: false, binding: replacement, newBinding: replacement, blockers: [] };
|