@staix/agent-hub 0.12.44 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +83 -116
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/docs/adding-an-ai-tool.md +102 -14
- package/docs/agent-notes/README.md +15 -0
- package/docs/agent-notes/adapters.md +63 -46
- package/docs/agent-notes/budget.md +7 -2
- package/docs/agent-notes/bus.md +6 -4
- package/docs/agent-notes/daemon.md +15 -8
- package/docs/agent-notes/local-worker.md +6 -4
- package/docs/agent-notes/tasks.md +18 -5
- package/docs/agent-notes/tests.md +11 -9
- package/docs/dogfood.md +92 -0
- package/docs/events.md +14 -1
- package/docs/operations.md +349 -26
- package/docs/profiles.md +2 -2
- package/docs/quickstart.md +12 -2
- package/docs/sdlc.md +61 -13
- package/docs/security.md +24 -12
- package/docs/smoke.md +9 -4
- package/docs/specs/2026-09-19-agent-hub-design.md +873 -13
- package/docs/verification/2026-10-19-reviewer-isolation.md +2 -0
- package/docs/verification/2026-10-21-469-r1-dashboard-tokens.md +43 -0
- package/docs/verification/2026-10-21-469-r2-dashboard-shell.md +54 -0
- package/docs/verification/2026-10-21-469-r3-dashboard-board.md +51 -0
- package/docs/verification/2026-10-21-469-r4-dashboard-views.md +77 -0
- package/docs/verification/2026-10-21-469-r6-dashboard-motion-states.md +82 -0
- package/docs/verification/2026-10-22-0.12.45.md +60 -0
- package/docs/verification/2026-10-22-code-facts.md +78 -0
- package/docs/verification/2026-10-22-codex-reviewer-isolation.md +47 -0
- package/docs/verification/2026-10-22-kimi-qwen-compat.md +79 -0
- package/docs/verification/2026-10-23-0.13.0.md +89 -0
- package/docs/verification/assets/469-r1-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r1-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-agents-768-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1024-light.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r2-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-board-768-light.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-board-390-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-390-dark.png +0 -0
- package/docs/verification/assets/469-r3-plan-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-plan-390-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-memory-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-message-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-390-light.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-light.png +0 -0
- package/docs/verification/assets/469-r6-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-toasts-390-light.png +0 -0
- package/docs/verification/dogfood/2026-10-22.md +3109 -0
- package/docs/verified.json +19 -16
- package/package.json +5 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +10120 -9623
- package/src/adapters/adapter-api.ts +2 -0
- package/src/adapters/agy/definition.ts +3 -1
- package/src/adapters/agy/entry.ts +5 -4
- package/src/adapters/claude-channel.ts +32 -12
- package/src/adapters/codex-appserver.ts +19 -2
- package/src/adapters/codex-review-isolation.ts +12 -5
- package/src/adapters/compat.ts +140 -0
- package/src/adapters/conformance/acp.ts +114 -0
- package/src/adapters/conformance/codex.ts +191 -0
- package/src/adapters/conformance/enrollment.ts +43 -0
- package/src/adapters/conformance/factories.ts +17 -0
- package/src/adapters/conformance/fake-paths.ts +12 -0
- package/src/adapters/conformance/fakes/acp-server.ts +267 -0
- package/src/adapters/conformance/fakes/acp-watchdog-agent.ts +10 -0
- package/src/adapters/conformance/fakes/app-server.ts +119 -0
- package/src/adapters/conformance/fakes/codex-bin.ts +15 -0
- package/src/adapters/conformance/fakes/jcode-agent.ts +91 -0
- package/src/adapters/conformance/fakes/model-server.ts +39 -0
- package/src/adapters/conformance/fakes/pi-rpc.ts +32 -0
- package/src/adapters/conformance/index.ts +11 -0
- package/src/adapters/conformance/kit.ts +50 -0
- package/src/adapters/conformance/local-pi.ts +100 -0
- package/src/adapters/conformance/owned-acp.ts +44 -0
- package/src/adapters/definitions/builtin-metadata.ts +25 -7
- package/src/adapters/definitions/native-cli-doctor.ts +12 -5
- package/src/adapters/definitions/native-cli-entry-jcode.ts +9 -6
- package/src/adapters/definitions/native-cli-entry-kimi.ts +7 -1
- package/src/adapters/definitions/native-cli-entry.ts +7 -19
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +9 -2
- package/src/adapters/definitions/native-cli-preview-common.ts +2 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +8 -2
- package/src/adapters/definitions/native-cli-preview-qwen.ts +2 -1
- package/src/adapters/definitions/native-cli-preview.ts +2 -11
- package/src/adapters/definitions/qwen-native.ts +7 -3
- package/src/adapters/definitions/qwen.ts +5 -1
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-module-scan.ts +126 -0
- package/src/adapters/external-modules.ts +240 -0
- package/src/adapters/jcode.ts +72 -31
- package/src/adapters/kimi-review.ts +6 -3
- package/src/adapters/pi.ts +2 -0
- package/src/adapters/public.ts +7 -0
- package/src/adapters/registry-types.ts +13 -1
- package/src/adapters/registry.ts +52 -15
- package/src/cli/agents.ts +40 -0
- package/src/cli/console-review.ts +54 -0
- package/src/cli/console-state.ts +124 -29
- package/src/cli/console-symbols.ts +74 -0
- package/src/cli/console-usage.ts +89 -0
- package/src/cli/console.ts +26 -1
- package/src/cli/help.ts +36 -21
- package/src/cli/identity.ts +30 -7
- package/src/cli/init.ts +109 -11
- package/src/cli/main.ts +145 -16
- package/src/cli/output.ts +44 -1
- package/src/cli/preview.ts +1 -1
- package/src/cli/sdlc-release-guard.ts +82 -0
- package/src/cli/upgrade-labels.ts +1 -1
- package/src/cli/upgrade-presentation.ts +1 -1
- package/src/cli/upgrade-restore-state.ts +3 -2
- package/src/cli/upgrade-runtime.ts +4 -1
- package/src/hub/action-class.ts +362 -0
- package/src/hub/action-policy.ts +106 -0
- package/src/hub/board.ts +72 -11
- package/src/hub/budget.ts +71 -2
- package/src/hub/bus.ts +43 -3
- package/src/hub/checks.ts +80 -31
- package/src/hub/code-facts.ts +223 -0
- package/src/hub/config-apply.ts +28 -11
- package/src/hub/config-reload.ts +8 -5
- package/src/hub/config-trust.ts +7 -4
- package/src/hub/control-client.ts +4 -1
- package/src/hub/daemon.ts +332 -61
- package/src/hub/dogfood-read.ts +199 -0
- package/src/hub/dogfood.ts +163 -0
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +11 -2
- package/src/hub/facts.ts +76 -13
- package/src/hub/hub-tools.ts +45 -7
- package/src/hub/inbox-polling.ts +25 -0
- package/src/hub/instance-config.ts +17 -5
- package/src/hub/instance-execution.ts +20 -8
- package/src/hub/instances.ts +75 -2
- package/src/hub/peers.ts +9 -0
- package/src/hub/plan-conformance.ts +83 -0
- package/src/hub/profiles.ts +7 -2
- package/src/hub/release-worktree.ts +56 -0
- package/src/hub/review-brief.ts +36 -0
- package/src/hub/review-findings.ts +104 -0
- package/src/hub/roster.ts +31 -5
- package/src/hub/sdlc-queue.ts +48 -11
- package/src/hub/sdlc-release-authorization.ts +173 -0
- package/src/hub/sdlc-release.ts +16 -1
- package/src/hub/sdlc.ts +71 -18
- package/src/hub/settings.ts +10 -3
- package/src/hub/snapshots.ts +101 -14
- package/src/hub/symbol-normalizer.ts +26 -0
- package/src/hub/task-code-symbols.ts +90 -0
- package/src/hub/task-sweep.ts +3 -2
- package/src/hub/tasks.ts +556 -88
- package/src/hub/test-integrity.ts +162 -0
- package/src/hub/ui.ts +2 -2
- package/src/hub/usage-rollup.ts +361 -0
- package/src/local/sandbox.ts +5 -3
- package/src/ui/index.html +560 -78
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +44 -0
- package/templates/AGENTS.block.md +1 -1
- package/templates/profiles/dev.json +6 -1
- package/templates/profiles/general.json +6 -1
- package/templates/profiles/research.json +6 -1
- package/templates/profiles/writing.json +6 -1
- package/templates/release-guard.block.md +6 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { planSymbolMatches } from "../hub/review-brief.ts";
|
|
2
|
+
|
|
3
|
+
// #684 2b: the "changed symbols" block of the console's task detail. Pure data to fields, no terminal code. The hub's own observation of the changed
|
|
4
|
+
// symbols (`codeSymbols`, PR #708) is read as it is; nothing is parsed from code here and nothing is asked of the hub beyond `task_show`.
|
|
5
|
+
// Advisory only: ripwire does not report deleted symbols, so these lists never establish that a plan was or was not followed. The plan names are `plan.symbols`,
|
|
6
|
+
// the same list `withCodeSymbols` reads as `planned` in the review brief (daemon.ts passes `task.plan?.symbols`), not a second source.
|
|
7
|
+
|
|
8
|
+
export interface ChangedSymbol { id: string; path: string; name: string; kind: string }
|
|
9
|
+
export interface SymbolBlock { state: "known" | "unknown"; complete: boolean; head?: string; matching: ChangedSymbol[]; unplanned: ChangedSymbol[]; untouched: string[] }
|
|
10
|
+
|
|
11
|
+
const MAX_LINES = 40;
|
|
12
|
+
const isPrivate = (task: any): boolean =>
|
|
13
|
+
!task || (Array.isArray(task.signals) && task.signals.some((s: unknown) => s === "pii" || s === "pii_path")) || task.title === "[pii]";
|
|
14
|
+
const symbolsOf = (task: any): ChangedSymbol[] | undefined => {
|
|
15
|
+
const received = task?.codeSymbols;
|
|
16
|
+
if (!received || received.state !== "known" || !Array.isArray(received.symbols)) return undefined;
|
|
17
|
+
// Only the public receipt (`publicTaskCodeSymbols`, re-screened on every read) is shown. A stored receipt still carries `repositoryKey` and was never screened for
|
|
18
|
+
// the current policy: a task handed over in that shape reads as unknown, whatever feeds this renderer today (the renderer holds the rule itself).
|
|
19
|
+
if (Object.hasOwn(received, "repositoryKey")) return undefined;
|
|
20
|
+
const out: ChangedSymbol[] = [];
|
|
21
|
+
for (const s of received.symbols) if (s && typeof s.id === "string" && typeof s.path === "string" && typeof s.name === "string" && typeof s.kind === "string") out.push({ id: s.id, path: s.path, name: s.name, kind: s.kind });
|
|
22
|
+
return out;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Each changed symbol as matching the plan or unplanned, and each planned name nothing changed under as untouched. Unknown without code facts. */
|
|
26
|
+
export function classifySymbols(task: any): SymbolBlock {
|
|
27
|
+
const changed = symbolsOf(task);
|
|
28
|
+
if (!changed) return { state: "unknown", complete: false, matching: [], unplanned: [], untouched: [] };
|
|
29
|
+
const plan: string[] = Array.isArray(task?.plan?.symbols) ? task.plan.symbols.filter((v: unknown): v is string => typeof v === "string") : [];
|
|
30
|
+
const matching = changed.filter(s => plan.some(p => planSymbolMatches(p, s.name)));
|
|
31
|
+
const unplanned = changed.filter(s => !plan.some(p => planSymbolMatches(p, s.name)));
|
|
32
|
+
const untouched = plan.filter(p => !changed.some(s => planSymbolMatches(p, s.name)));
|
|
33
|
+
return { state: "known", complete: task.codeSymbols.complete === true, ...(typeof task.codeSymbols.head === "string" ? { head: task.codeSymbols.head.slice(0, 8) } : {}), matching, unplanned, untouched };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The detail fields for a task (`symbols` and `symbols note`), or none for a private one. Values are objects of single words (`{ matching: name, in: path }`) so the console's field
|
|
38
|
+
* renderer quotes every agent-written string: a symbol name cannot pass for a hub label. Capped, with the count of what is left out.
|
|
39
|
+
*/
|
|
40
|
+
export function symbolFields(task: any): [string, unknown][] {
|
|
41
|
+
// Only a task as `task_show` shapes it (a numeric id and a state): any other detail an agent or a test hands the Tasks panel keeps its own layout.
|
|
42
|
+
if (!task || typeof task.id !== "number" || typeof task.state !== "string" || isPrivate(task)) return [];
|
|
43
|
+
const block = classifySymbols(task);
|
|
44
|
+
if (block.state === "unknown") return [["symbols", "changed symbols: unknown (no code facts)"]];
|
|
45
|
+
const items: Record<string, string>[] = [
|
|
46
|
+
...block.matching.map(s => ({ matching: s.name, in: s.path })), ...block.unplanned.map(s => ({ unplanned: s.name, in: s.path })), ...block.untouched.map(name => ({ untouched: name })),
|
|
47
|
+
];
|
|
48
|
+
const shown = items.slice(0, MAX_LINES);
|
|
49
|
+
const note = [`changed symbols: ${block.matching.length} matching, ${block.unplanned.length} unplanned, ${block.untouched.length} untouched${block.head ? ` at ${block.head}` : ""}`,
|
|
50
|
+
block.complete ? "" : "incomplete: some changed symbols were not listed, so an untouched one may have changed",
|
|
51
|
+
"advisory: deleted symbols are not reported"].filter(Boolean).join("; ");
|
|
52
|
+
return [["symbols", shown.length ? shown : "none observed"], ["symbols note", items.length > shown.length ? `${note}; ${items.length - shown.length} more not shown` : note]];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/** Render the hub's read-only pair verdicts; never derive disjointness from a local receipt or plan. */
|
|
57
|
+
export function overlapFields(task: any): [string, unknown][] {
|
|
58
|
+
if (!task || typeof task.id !== "number" || typeof task.state !== "string" || isPrivate(task) || !Array.isArray(task.symbolOverlaps)) return [];
|
|
59
|
+
if (!task.symbolOverlaps.length) return [["overlaps", "none"]];
|
|
60
|
+
const items: Record<string, unknown>[] = [];
|
|
61
|
+
let invalid = 0;
|
|
62
|
+
for (const row of task.symbolOverlaps) {
|
|
63
|
+
if (!row || !Number.isSafeInteger(row.task) || row.task < 1 || !Array.isArray(row.files)
|
|
64
|
+
|| !row.files.every((file: unknown) => typeof file === "string") || typeof row.sameFileDifferentSymbols !== "boolean") { invalid++; continue; }
|
|
65
|
+
items.push({ task: row.task, files: row.files.slice(0, MAX_LINES),
|
|
66
|
+
overlap: row.sameFileDifferentSymbols ? "same file, different symbols" : "overlap",
|
|
67
|
+
...(row.files.length > MAX_LINES ? { omitted: `${row.files.length - MAX_LINES} files not shown` } : {}) });
|
|
68
|
+
}
|
|
69
|
+
if (!items.length) return [["overlaps", "unknown (invalid overlap data)"]];
|
|
70
|
+
const note = ["observed names only, deleted definitions may be missing",
|
|
71
|
+
items.length > MAX_LINES ? `${items.length - MAX_LINES} more not shown` : "",
|
|
72
|
+
invalid ? `${invalid} invalid rows withheld` : ""].filter(Boolean).join("; ");
|
|
73
|
+
return [["overlaps", items.slice(0, MAX_LINES)], ["overlap note", note]];
|
|
74
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Counter, LimitsResult, UsageResult, UsageRow } from "../hub/usage-rollup.ts";
|
|
2
|
+
|
|
3
|
+
// #683 U3: what the console's Usage panel shows, as plain strings. No terminal code and no state import here: console-state.ts renders it.
|
|
4
|
+
// Every number names its source, an unknown counter reads `unknown` (never 0), and a private task is `#id [pii]` with no title.
|
|
5
|
+
|
|
6
|
+
/** What the console keeps from the `usage` op: one query per period, the limits history and, for the task selected in Tasks, its tokens per peer. */
|
|
7
|
+
export interface UsageState {
|
|
8
|
+
today?: UsageResult; week?: UsageResult; life?: UsageResult; limits?: LimitsResult;
|
|
9
|
+
/** The selected task's tokens per peer, with the task id it was asked for. */
|
|
10
|
+
task?: UsageResult; taskId?: number;
|
|
11
|
+
at?: number; problem?: string;
|
|
12
|
+
/** The hub's roll-up was still behind the event log when it answered: the numbers are partial (a later poll catches up). */
|
|
13
|
+
behind?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface UsagePeerRow { id: string }
|
|
16
|
+
|
|
17
|
+
const TAG: Record<Counter["source"], string> = { native: "native", count: "hub", manual: "manual", partial: "at least", unknown: "unknown" };
|
|
18
|
+
const compact = (n: number): string => n >= 1e7 ? `${Math.round(n / 1e6)}M` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e4 ? `${Math.round(n / 1e3)}k` : String(n);
|
|
19
|
+
/** `1.2M native`, `77 hub`, or `unknown`: a counter nobody reported is never shown as a number. */
|
|
20
|
+
export const counterText = (c: Counter | undefined, exact = false): string => !c || c.value === null ? "unknown" : `${exact ? c.value : compact(c.value)} ${TAG[c.source]}`;
|
|
21
|
+
/** A row's tokens: the provider's total when it reported one, otherwise what the hub counted, otherwise unknown. */
|
|
22
|
+
export const rowTokens = (row: UsageRow | undefined): Counter =>
|
|
23
|
+
row && row.total.value !== null ? row.total : row && row.counted.value !== null ? row.counted : { value: null, source: "unknown" };
|
|
24
|
+
const periodText = (row: UsageRow | undefined, exact = false): string => row ? counterText(rowTokens(row), exact) : "no records";
|
|
25
|
+
|
|
26
|
+
const rowOf = (result: UsageResult | undefined, peer: string): UsageRow | undefined => result?.rows.find(row => row.key === peer);
|
|
27
|
+
|
|
28
|
+
/** Every peer the panel can say something about: peers with usage in any period, and peers with a quota reading. */
|
|
29
|
+
export function usagePeers(usage: UsageState | undefined, budget: Record<string, unknown>, peers: Record<string, unknown>): UsagePeerRow[] {
|
|
30
|
+
const ids = new Set<string>([...Object.keys(peers), ...Object.keys(budget)]);
|
|
31
|
+
for (const result of [usage?.today, usage?.week, usage?.life]) for (const row of result?.rows ?? []) ids.add(row.key);
|
|
32
|
+
return [...ids].sort().map(id => ({ id }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Budget { windows?: { id: string; used: number; resetsAt?: number }[]; paused?: { resetsAt: number } }
|
|
36
|
+
const windowText = (w: { id: string; used: number; resetsAt?: number }, rel: (at: number) => string) =>
|
|
37
|
+
`${w.id} ${Math.round(w.used * 100)}%${typeof w.resetsAt === "number" ? ` ${rel(w.resetsAt)}` : ""}`;
|
|
38
|
+
const lastResets = (limits: LimitsResult | undefined, peer: string, count: number) =>
|
|
39
|
+
(limits?.history ?? []).filter(h => h.type === "quota_reset" && h.peer === peer).slice(-count).reverse();
|
|
40
|
+
|
|
41
|
+
/** One table row: PEER, TODAY, 7D, LIFETIME, PAUSE, QUOTA, LAST RESET (PAUSE before QUOTA: a pause must survive a narrow terminal, the long quota text is what gives way). */
|
|
42
|
+
export function usageCells(usage: UsageState | undefined, peer: string, budget: Budget | undefined, rel: (at: number) => string): string[] {
|
|
43
|
+
const reset = lastResets(usage?.limits, peer, 1)[0];
|
|
44
|
+
// The table is tight at 80 columns: a time still to come reads `3h00m`, not `in 3h00m` (the detail view spells it out).
|
|
45
|
+
const ahead = (at: number) => rel(at).replace(/^in /, "");
|
|
46
|
+
return [peer, periodText(rowOf(usage?.today, peer)), periodText(rowOf(usage?.week, peer)), periodText(rowOf(usage?.life, peer)),
|
|
47
|
+
budget?.paused ? ahead(budget.paused.resetsAt) : "-",
|
|
48
|
+
budget?.windows?.length ? budget.windows.map(w => windowText(w, ahead)).join(", ") : "-",
|
|
49
|
+
reset ? `${reset.window} ${rel(Date.parse(reset.at))}` : "-"];
|
|
50
|
+
}
|
|
51
|
+
export const USAGE_HEAD = ["PEER", "TODAY", "7D", "LIFETIME", "PAUSE", "QUOTA", "LAST RESET"];
|
|
52
|
+
/** Only the last reset gives way to width; the others are cut shorter instead, so the 7D column and the pause are never dropped. */
|
|
53
|
+
export const USAGE_DROP = ["LAST RESET"];
|
|
54
|
+
|
|
55
|
+
/** The token split of one period: input, output, cache read, cache write, the total and the hub's own count, each with its source. */
|
|
56
|
+
export function splitText(row: UsageRow | undefined): string {
|
|
57
|
+
if (!row) return "no records";
|
|
58
|
+
return [`total ${counterText(row.total, true)}`, `input ${counterText(row.input, true)}`, `output ${counterText(row.output, true)}`,
|
|
59
|
+
`cache read ${counterText(row.cacheRead, true)}`, `cache write ${counterText(row.cacheWrite, true)}`, `hub count ${counterText(row.counted, true)}`].join(", ");
|
|
60
|
+
}
|
|
61
|
+
export const taskLabel = (usage: UsageState | undefined): string =>
|
|
62
|
+
usage?.taskId === undefined ? "" : `#${usage.taskId}${usage.task?.taskPii ? " [pii]" : ""}`;
|
|
63
|
+
|
|
64
|
+
/** The detail of one peer, as labelled one-line-per-field text. Values are hub-built from ids and numbers only. */
|
|
65
|
+
export function usageDetail(usage: UsageState | undefined, peer: string, budget: Budget | undefined, rel: (at: number) => string): Record<string, string> {
|
|
66
|
+
const resets = lastResets(usage?.limits, peer, 3).map(h => `${h.window} ${rel(Date.parse(h.at))}`);
|
|
67
|
+
const out: Record<string, string> = {
|
|
68
|
+
peer,
|
|
69
|
+
quota: budget?.windows?.length ? budget.windows.map(w => windowText(w, rel)).join(", ") : "no reading",
|
|
70
|
+
pause: budget?.paused ? `budget pause, resumes ${rel(budget.paused.resetsAt)}` : "none",
|
|
71
|
+
"last resets": resets.length ? resets.join(", ") : "none recorded",
|
|
72
|
+
today: splitText(rowOf(usage?.today, peer)),
|
|
73
|
+
"last 7 days": splitText(rowOf(usage?.week, peer)),
|
|
74
|
+
lifetime: splitText(rowOf(usage?.life, peer)),
|
|
75
|
+
};
|
|
76
|
+
if (usage?.taskId !== undefined) out[`task ${taskLabel(usage)}`] = splitText(rowOf(usage.task, peer));
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The lines under the table: the task selected in Tasks and its tokens per peer. */
|
|
81
|
+
export function usageTaskLines(usage: UsageState | undefined, selectedTask: number | undefined): { text: string; tone?: "muted" | "info" | "failure" }[] {
|
|
82
|
+
if (selectedTask === undefined) return [{ text: "task: none selected; pick one in Tasks (3), then open Usage (7)", tone: "muted" }];
|
|
83
|
+
if (usage?.problem) return [];
|
|
84
|
+
if (usage?.taskId !== selectedTask || !usage.task) return [{ text: `task #${selectedTask}: loading...`, tone: "muted" }];
|
|
85
|
+
const rows = usage.task.rows;
|
|
86
|
+
const head = { text: `task ${taskLabel(usage)}: tokens per peer, all time`, tone: "info" as const };
|
|
87
|
+
if (!rows.length) return [head, { text: " no usage recorded for this task", tone: "muted" }];
|
|
88
|
+
return [head, ...rows.map(row => ({ text: ` ${row.key} ${counterText(rowTokens(row))}` }))];
|
|
89
|
+
}
|
package/src/cli/console.ts
CHANGED
|
@@ -4,6 +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 { reviewResult } from "./console-review.ts";
|
|
7
8
|
import { syncPermissionDefaults, permissionDefaultText, approvalFrom, initialConsoleState, keyTable, notify, paint, permissionText, plural, pruneApprovals, reduceConsole, renderConsoleLines, resolveColor, stateTone, streamTokens, wrapStreamTokens, terminalText, wrap, captureConsoleSelection, selectedPanelRow, appendEvent, FIRST_KEYS, LAST_KEYS } from "./console-state.ts";
|
|
8
9
|
import type { ConsoleEffect, ConsoleEvent, Detail, Tone } from "./console-state.ts";
|
|
9
10
|
import type { BusEvent } from "../hub/bus.ts";
|
|
@@ -192,6 +193,15 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
192
193
|
...(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
194
|
...(panels && (state.panel === 1 || state.panel === 6) ? [client.request({ t: "settings_get" }, 3000)] : []),
|
|
194
195
|
]);
|
|
196
|
+
// #683 U3: the Usage panel asks the hub's roll-up only while it is open (UTC day, 7 UTC days, lifetime, the limits history and the selected task).
|
|
197
|
+
// The task is read at the time of the request, so a reply for a task that is no longer selected is dropped below.
|
|
198
|
+
const usageTask = panels && state.panel === 7 ? state.usageTask : undefined;
|
|
199
|
+
const usageReplies = panels && state.panel === 7 ? await (async () => {
|
|
200
|
+
const startOfDay = Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate());
|
|
201
|
+
return Promise.all([client.request({ t: "usage", by: "peer", since: startOfDay }, 5000), client.request({ t: "usage", by: "peer", since: startOfDay - 6 * 86_400_000 }, 5000),
|
|
202
|
+
client.request({ t: "usage", by: "peer" }, 5000), client.request({ t: "usage", op: "limits", limit: 1000 }, 5000),
|
|
203
|
+
...(usageTask !== undefined ? [client.request({ t: "usage", by: "peer", task: usageTask }, 5000)] : [])]);
|
|
204
|
+
})() : undefined;
|
|
195
205
|
if (!active) return;
|
|
196
206
|
const [status, budget, tasks, queue, ready, settings] = replies;
|
|
197
207
|
captureConsoleSelection(state);
|
|
@@ -217,6 +227,12 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
217
227
|
}
|
|
218
228
|
if (syncPermissionDefaults(state, status.status?.permissionDefaults) && state.confirm?.type === "permission_default") stream({ text: permissionDefaultText(state.confirm), kind: "permission_default", peer: state.confirm.peer, tone: "attention" });
|
|
219
229
|
if (budget.budget) state.budget = budget.budget;
|
|
230
|
+
if (usageReplies) {
|
|
231
|
+
const [today, week, life, limits, task] = usageReplies, failed = usageReplies.find(reply => reply?.ok === false);
|
|
232
|
+
if (failed || !today?.usage || !week?.usage || !life?.usage || !limits?.limits) state.usage = { ...state.usage, problem: String(failed?.error ?? "usage is unavailable") };
|
|
233
|
+
else state.usage = { today: today.usage, week: week.usage, life: life.usage, limits: limits.limits, at: Date.now(), ...(usageReplies.some(reply => reply?.behind) ? { behind: true } : {}),
|
|
234
|
+
...(usageTask !== undefined && task?.usage && state.usageTask === usageTask ? { task: task.usage, taskId: usageTask } : {}) };
|
|
235
|
+
}
|
|
220
236
|
if (tasks?.ok) { const parsed = JSON.parse(tasks.text); if (Array.isArray(parsed)) {
|
|
221
237
|
const readyIds = new Set(ready?.ok ? JSON.parse(ready.text).map((task: any) => task.id) : []);
|
|
222
238
|
state.tasks = parsed.map(task => ({ ...task, ready: readyIds.has(task.id) }));
|
|
@@ -304,7 +320,16 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
304
320
|
let diagnostic = "";
|
|
305
321
|
// 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.
|
|
306
322
|
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) => {
|
|
323
|
+
cancelChild = run(action.args, output, (code = 0) => {
|
|
324
|
+
childRunning = false; cancelChild = undefined;
|
|
325
|
+
if (active) {
|
|
326
|
+
// #711: a review's result is a notice in panels mode too (where the stream is not on screen): `#372 approved`, or the CLI's own refusal verbatim.
|
|
327
|
+
const review = reviewResult(action.args, code, diagnostic.trim());
|
|
328
|
+
if (review) notify(state, review.text, Date.now(), review.tone === "success" ? "success" : "failure");
|
|
329
|
+
else if (code) notice(diagnostic.trim() || `command exited ${code}`);
|
|
330
|
+
void refresh(); draw();
|
|
331
|
+
}
|
|
332
|
+
}, Math.max(1, columns - 4));
|
|
308
333
|
} catch (error) { childRunning = false; notice(String((error as Error).message)); draw(); }
|
|
309
334
|
};
|
|
310
335
|
const handle = (key: string) => {
|
package/src/cli/help.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ADAPTERS, deriveAdapterLists, type AdapterDefinition } from "../adapters/registry.ts";
|
|
1
2
|
import { VERSION } from "../version.ts";
|
|
2
3
|
import { paint, wrap, type Span } from "./console-state.ts";
|
|
3
4
|
|
|
@@ -10,7 +11,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
10
11
|
["ahub projects [--json] [--color=auto|always|never]", "list registered projects and live status"],
|
|
11
12
|
["ahub projects remove <id>", "forget a stopped registration (keeps project files)"],
|
|
12
13
|
["ahub setup [--yes]", "install or update the Claude Code channel plugin from this package, then run doctor"],
|
|
13
|
-
["ahub init [--profile dev|research|writing|general] [--shadow] [--dry-run --json]", "preview or write .agenthub/config.json and the AGENTS.md marker block (drops a legacy CLAUDE.md block)"],
|
|
14
|
+
["ahub init [--profile dev|research|writing|general] [--shadow] [--release-guard] [--dry-run --json]", "preview or write .agenthub/config.json and the AGENTS.md marker block (drops a legacy CLAUDE.md block)"],
|
|
14
15
|
["ahub --live-test [--project <path|id>] <command> [args...]", "user-authorized operational console delegation; ordinary calls retain peer identity; person-authored content refuses"],
|
|
15
16
|
["ahub help [command]", "this help, or one command's entries; also --help and -h"],
|
|
16
17
|
["ahub version | --version", "print the installed version"],
|
|
@@ -25,17 +26,23 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
25
26
|
["ahub pet --palette <blue|teal|violet|ochre>", "save the floating window's base color to pet.palette in .agenthub/config.local.json"],
|
|
26
27
|
["ahub sdlc status [task] | explain <task> | export <task> | record <task> <receipt-json>", "read head-bound lifecycle gates and the next actor/action (person only)"],
|
|
27
28
|
["ahub sdlc queue <owner/repo> | release <owner/repo> [live-issue...]", "read-only merge and release evidence (person only)"],
|
|
29
|
+
["ahub sdlc authorize-release <version>", "approve this daemon checkout commit, version and fresh docs (person only; restart revokes)"],
|
|
30
|
+
["ahub sdlc release-guard -- <literal git push|gh release create|npm publish argv>", "read-only authorization preflight; init --release-guard installs the tag pre-push hook"],
|
|
31
|
+
["ahub sdlc release-guard-hook <remote-name> <remote-url>", "internal pre-push readback; validates version tags and forwards exact stdin on success"],
|
|
28
32
|
["ahub sdlc accept <task> --issue <n> [--repo owner/repo]", "accept the task's spec issue at its current body; build entry needs it in enforce (person only)"],
|
|
29
|
-
["ahub sdlc
|
|
33
|
+
["ahub sdlc accept-departure <task> <head> <reason...>", "accept current plan departures on that exact PR head (person only)"],
|
|
34
|
+
["ahub sdlc plan-review <task> <revision> <reason...> [--review full|delta|none --reviewers N] [--ci full|focused|reuse]", "record planner review, scope and host-read issue metadata"],
|
|
30
35
|
["ahub sdlc train report|configure|cut <release-id> [revision] [json]", "plan a release and freeze its approved scope after resource/quota admission"],
|
|
31
|
-
["ahub sdlc release-assign <task> <revision> <release-id|none> <reason
|
|
36
|
+
["ahub sdlc release-assign <task> <revision> <release-id|none> <reason...>", "assign approved/in-flight work without changing execution"],
|
|
32
37
|
["ahub sdlc costs [owner/repo]", "read bounded planner cost metrics for linked merged PRs"],
|
|
33
38
|
["ahub backlog groom | return <task> <revision> <reason...>", "read grooming findings or return accepted untouched work to backlog (planner/person)"],
|
|
34
39
|
["ahub status [--json] [--full] [--color=auto|always|never]", "the hub, its peers, model backends and task counts"],
|
|
35
40
|
["ahub status --all [--json] [--color=auto|always|never]", "show every registered project"],
|
|
36
41
|
["ahub logs [-f]", "the last 100 lines of hub.log; -f shows the last 10 and follows it"],
|
|
37
|
-
["ahub doctor [--json] [--color=auto|always|never]", "check the tools, daemon, plugin, gateway, models and memory worker this project uses"],
|
|
42
|
+
["ahub doctor [--json] [--full] [--color=auto|always|never]", "check the tools, daemon, plugin, gateway, models and memory worker this project uses"],
|
|
38
43
|
["ahub doctor --orphans [--kill] [--json] [--color=auto|always|never]", "list registrations whose project root is gone; --kill stops their daemons (SIGTERM, then SIGKILL) only after the process identity checks out"],
|
|
44
|
+
["ahub agents [--json]", "show each gated agent CLI's installed version and verdict (verified, in-line, trusted, refused or unknown)"],
|
|
45
|
+
["ahub agents trust <id> <version> [--revoke]", "accept an untested agent CLI version on this machine (person only; runtime proofs still run)"],
|
|
39
46
|
["ahub kill", "stop this project's hub"],
|
|
40
47
|
["ahub reset [--all] [--yes]", "list, then with --yes stop the hub and discard every queued and needs_review delivery, clear holds and pauses and drop session pointers; --all moves the state directory to .agenthub/archive instead"],
|
|
41
48
|
["ahub restart [--dry-run] [--yes] [--json] [--verbose] [--color=auto|always|never]", "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"],
|
|
@@ -49,15 +56,6 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
49
56
|
]],
|
|
50
57
|
["Agents", [
|
|
51
58
|
["ahub <configured-kind> [--mode headless] [--as <instance>] [--session-id <id>] [--print-command | --dry-run]", "start a trusted config.local.json ACP kind (default off roster); native argv is withheld in previews"],
|
|
52
|
-
["ahub claude [--print-command] [--unattended] [args...]", "launch Claude Code with the hub channel"],
|
|
53
|
-
["ahub codex [--print-command] [--unattended] [args...]", "start the Codex adapter and attach the TUI"],
|
|
54
|
-
["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"],
|
|
55
|
-
["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"],
|
|
56
|
-
["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"],
|
|
57
|
-
["ahub qwen [--mode headless] [--model <alias>] [--session-id <id>] [--print-command]", "start optional Qwen Code 0.25.0 ACP (roster.qwen, default off)"],
|
|
58
|
-
["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"],
|
|
59
|
-
["ahub local [--route <id> | --model <id>]", "start the hub-native worker on the self-hosted models (routing.toml)"],
|
|
60
|
-
["ahub claude|codex|kimi|qwen|pi|local --as <instance> [options...]", "start an extra instance of that kind declared under instances in .agenthub/config.local.json"],
|
|
61
59
|
["ahub role list [--group <group>] | show <id>", "read bounded catalog guidance with layer and content hash"],
|
|
62
60
|
["ahub role assign <peer> <id> | clear <peer> [<id>]", "person only: add or clear catalog guidance, applied at the next peer start"],
|
|
63
61
|
["ahub stop <peer>", "human only: stop a hub-owned headless peer; end TUI agents in their terminals"],
|
|
@@ -71,17 +69,18 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
71
69
|
["ahub permission [<peer> [ask|ask-when-needed|never-ask]] [--yes]", "human only: inspect or set a running peer mode; never-ask requires --yes; config never-ask waits for console y"],
|
|
72
70
|
["ahub permit <id> <option>", "answer a permission request shown by tail (\"deny\" cancels)"],
|
|
73
71
|
["ahub roster [--json] [--color=auto|always|never]", "human only: every known peer, participation, start mode and retained work"],
|
|
74
|
-
["ahub roster add <peer> | remove <peer> [--yes] [--now]", "human only: change live participation through settings; removal previews retained messages and task handoff"],
|
|
72
|
+
["ahub roster add <peer> | remove <peer> [--yes] [--now]", "human only: change live participation through settings; removal previews retained messages and task handoff; remove also deletes the roster key of an instance no longer declared"],
|
|
75
73
|
["ahub settings [list] [--json] | get <key> [--json]", "human only: the settings the dashboard can change, each with its value, the file it comes from and when it applies"],
|
|
76
74
|
["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"],
|
|
77
75
|
["ahub pause|resume <peer>", "hold a peer's deliveries in its queue / release them"],
|
|
78
76
|
["ahub queue list [--peer <id>] [--json] [--color=auto|always|never]", "inspect durable deliveries"],
|
|
79
77
|
["ahub queue show <delivery-id> [--json] [--color=auto|always|never]", "inspect one delivery and revision"],
|
|
80
|
-
["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"],
|
|
78
|
+
["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; a queued delivery can be discarded while its peer is busy, and a refusal names its cause"],
|
|
79
|
+
["ahub queue discard --peer <id> --reason <text> [--state queued|needs_review] [--yes] [--json]", "person only: without --yes count one peer's queued (default) deliveries; with --yes discard them all with the reason, one recorded discard each; other peers' queues are untouched"],
|
|
81
80
|
]],
|
|
82
81
|
["Tasks and review", [
|
|
83
82
|
["ahub board import github --label <label> [--milestone <milestone>] [--repo <owner/repo>]", "person only: read bounded open issues into deduplicated Backlog; never writes GitHub"],
|
|
84
|
-
["ahub board [state | --ready] [--columns] [--release <name> | --release-id <id>] [--json] [--color=auto|always|never]", "the task board; --ready: proposed tasks with nothing left to wait for"],
|
|
83
|
+
["ahub board [state | --ready] [--columns] [--release <name> | --release-id <id>] [--json] [--full] [--color=auto|always|never]", "the task board; --ready: proposed tasks with nothing left to wait for"],
|
|
85
84
|
["ahub task propose [--class <c> | <class>] <title...> [--owner <peer>] [--path <p>]... [--after <id>]... [--urgent] [--backlog] [--detail <text>]", "put a task on the board; a first word that names a class is taken as the class; while a task named by --after is not approved yet, --owner reserves the task for that peer, offered to it first once it is ready; otherwise --owner assigns it now"],
|
|
86
85
|
["ahub backlog list | add <class> <title...> | plan <id> <revision> <fields-json> <reason...> | pin <id> <revision> true|false <reason...> | undo <id> <revision>", "person only: stage and plan work, preserve pins/frozen scope and refuse stale edits"],
|
|
87
86
|
["ahub release new <name> <goal> <order> | list | show <name> | plan <name> <revision> <dates-json> | activate|freeze|ship|drop <name> <revision>", "person only: planned UTC millisecond dates and phased scope; ship records a time after approval, never tags or publishes"],
|
|
@@ -94,7 +93,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
94
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"],
|
|
95
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"],
|
|
96
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)"],
|
|
97
|
-
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]... [--head <sha>]", "give a review verdict on a task"],
|
|
96
|
+
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]... [--head <sha>] [--findings <json>]", "give a review verdict on a task; the person can do the same in ahub console: Tasks, select the task, r, then a (approve) or c (request changes, with a note)"],
|
|
98
97
|
["ahub review brief <id>", "print the review brief of a task's commit: reviewable and excluded files, rules, coverage to account for"],
|
|
99
98
|
["ahub route explain <id>", "why a task went where it went, its reserved owner included"],
|
|
100
99
|
["ahub route explain --class <c> <title...>", "what would happen to such a task now"],
|
|
@@ -102,7 +101,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
102
101
|
["ahub remember <text...>", "save a note to the memory all agents share"],
|
|
103
102
|
]],
|
|
104
103
|
["Budget", [
|
|
105
|
-
["ahub budget [--json] [--color=auto|always|never]", "quota windows per peer, and who is paused until when"],
|
|
104
|
+
["ahub budget [--json] [--full] [--color=auto|always|never]", "quota windows per peer, and who is paused until when"],
|
|
106
105
|
["ahub budget set <peer> <0..1> [--resets-in 30m] [--window 5h|week]", "feed a reading by hand (also: test the relay)"],
|
|
107
106
|
["ahub budget resume <peer>", "override a budget pause; readings are ignored for that peer until the window resets"],
|
|
108
107
|
["ahub budget execution status [id] [--json] [--color=auto|always|never]", "read execution counters and limits"],
|
|
@@ -112,6 +111,10 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
112
111
|
["ahub turns [peer] [--limit N] [--json] [--color=auto|always|never]", "recent turns and the files each changed (a git work tree only)"],
|
|
113
112
|
["ahub undo <turn> [--yes] [--context]", "put back the files a turn changed; refuses files changed since. Without --yes it only lists them; --context also drops a Codex turn from its conversation"],
|
|
114
113
|
["ahub report [--since 7d|<iso>] [--by task] [--json] [--color=auto|always|never]", "turns, tokens, messages, overlaps and task events per period"],
|
|
114
|
+
["ahub report --dogfood [--since 7d|<iso>] [--json]", "read-only scorecard: flow, quality, cost, delivery, performance and adoption; missing evidence is unknown"],
|
|
115
|
+
["ahub usage [--since 7d|<iso>] [--by peer|task|issue|pr|release|day|model] [--json] [--color=auto|always|never]", "tokens per group from the incremental roll-up: the provider's total with and without cache reads, input, output, reasoning and the hub's own count; each number names its source, unknown is never 0"],
|
|
116
|
+
["ahub usage limits [--peer <id>] [--limit <n>] [--json] [--color=auto|always|never]", "current quota windows and the history of pauses, resumes and resets"],
|
|
117
|
+
["ahub usage total [--json] [--color=auto|always|never]", "lifetime development tokens since the first event, per peer and overall"],
|
|
115
118
|
["ahub export [--since 7d|<iso>]", "structured events (events.jsonl) as JSON lines; never message bodies"],
|
|
116
119
|
["ahub research [--since 30d] [--all] [--json]", "success, first-pass, rework and check-failure rates, tokens and wall time per approved task, from the opt-in research records"],
|
|
117
120
|
["ahub research addons [--since 30d] [--all] [--json]", "approved tasks with each add-on (claude-mem, ponytail, ripwire, ...) on against off, with intervals; association, not cause"],
|
|
@@ -145,7 +148,18 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
145
148
|
["ahub manager", "internal: the dashboard manager process that ahub ui --all starts"],
|
|
146
149
|
]],
|
|
147
150
|
];
|
|
148
|
-
export
|
|
151
|
+
export function helpEntries(definitions: readonly AdapterDefinition[] = ADAPTERS): readonly HelpEntry[] {
|
|
152
|
+
const byKind = new Map(definitions.map(definition => [definition.kind, definition]));
|
|
153
|
+
const order = deriveAdapterLists(definitions).HELP_ORDER;
|
|
154
|
+
const native = order.flatMap(kind => byKind.get(kind)?.cli.usage ?? []);
|
|
155
|
+
const instanceKinds = order.filter(kind => byKind.get(kind)?.instances !== undefined);
|
|
156
|
+
const instances: readonly [string, string] = [`ahub ${instanceKinds.join("|")} --as <instance> [options...]`, "start an extra instance of that kind declared under instances in .agenthub/config.local.json"];
|
|
157
|
+
return SECTIONS.flatMap(([section, rows]) => {
|
|
158
|
+
const entries: readonly (readonly [string, string])[] = section === "Agents" ? [rows[0]!, ...native, ...(instanceKinds.length ? [instances] : []), ...rows.slice(1)] : rows;
|
|
159
|
+
return entries.map(([usage, description]) => ({ section, usage, description }));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export const HELP = helpEntries();
|
|
149
163
|
|
|
150
164
|
const COMMAND = /^ahub (-{0,2}[a-z][a-z-]*(?: ?\| ?-{0,2}[a-z][a-z-]*)*)/;
|
|
151
165
|
/** The command words a usage documents: `ahub pause|resume <peer>` documents pause and resume. */
|
|
@@ -159,9 +173,10 @@ const DESCRIPTION = 34;
|
|
|
159
173
|
* (80 to 100 columns). A usage too long for that column puts its description on the next line.
|
|
160
174
|
* With a command, only its entries; an unknown command renders nothing.
|
|
161
175
|
*/
|
|
162
|
-
export function renderHelp(columns: number, color: boolean, command?: string): string {
|
|
176
|
+
export function renderHelp(columns: number, color: boolean, command?: string, definitions?: readonly AdapterDefinition[]): string {
|
|
163
177
|
const width = Math.min(100, Math.max(80, columns || 80));
|
|
164
|
-
const
|
|
178
|
+
const model = definitions === undefined ? HELP : helpEntries(definitions);
|
|
179
|
+
const entries = command === undefined ? model : model.filter(entry => helpCommands(entry).includes(command));
|
|
165
180
|
const lines: Span[][] = command === undefined ? wrap(`agent-hub ${VERSION}: Claude Code, Codex and Kimi as peers in one project directory`, width, 0).map(text => [{ text }]) : [];
|
|
166
181
|
let section = "";
|
|
167
182
|
for (const entry of entries) {
|
package/src/cli/identity.ts
CHANGED
|
@@ -14,15 +14,17 @@ export const LIVE_TEST_PERSON_REFUSAL = "live-test cannot write person-attribute
|
|
|
14
14
|
|
|
15
15
|
/** Person verdicts and user-authored workflow content are never delegated. */
|
|
16
16
|
export function liveTestRefusal(command: string, args: readonly string[], context: { repairedFresh?: boolean } = {}): boolean {
|
|
17
|
-
if (command === "sdlc" && (args[0] === "plan-review" || args[0] === "release-assign" || args[0] === "train" && args[1] !== "report")) return true;
|
|
17
|
+
if (command === "sdlc" && (args[0] === "authorize-release" || args[0] === "plan-review" || args[0] === "release-assign" || args[0] === "train" && args[1] !== "report")) return true;
|
|
18
18
|
if (args.some(arg => arg === "--as-user" || arg.startsWith("--as-user="))) return true;
|
|
19
19
|
const sub = command === "recovery" ? recoveryAction(args) : args.find(arg => arg !== "--json");
|
|
20
20
|
return ["task", "role", "goal"].includes(command)
|
|
21
21
|
|| (command === "backlog" && sub !== "list") || (command === "release" && !["list", "show"].includes(sub ?? ""))
|
|
22
22
|
|| (command === "memory" && sub !== "graph")
|
|
23
23
|
|| (command === "recovery" && (sub === "reattach" || sub === "dispose" && context.repairedFresh === true))
|
|
24
|
-
|| (command === "board" && args[0] === "import") || (command === "sdlc" && (args[0] === "record" || args[0] === "accept"))
|
|
25
|
-
|| (command === "bench" && args.find(arg => arg !== "--json") === "run") || command === "pet"
|
|
24
|
+
|| (command === "board" && args[0] === "import") || (command === "sdlc" && (args[0] === "record" || args[0] === "accept" || args[0] === "accept-departure"))
|
|
25
|
+
|| (command === "bench" && args.find(arg => arg !== "--json") === "run") || command === "pet"
|
|
26
|
+
// #691: trusting an untested agent CLI version is the person's consent, never a delegated live-test operation.
|
|
27
|
+
|| (command === "agents" && !args.every(arg => arg === "--json"));
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export type CliIdentity =
|
|
@@ -59,6 +61,7 @@ export function classifyPeerCommand(command: string, args: readonly string[], ca
|
|
|
59
61
|
if (command === "pet" && args.includes("--window")) return "console";
|
|
60
62
|
if (command === "permission" || command === "stop" || command === "watch") return "console";
|
|
61
63
|
if (args.some((arg) => arg === "--as-user" || arg.startsWith("--as-user="))) return "console";
|
|
64
|
+
if (command === "sdlc" && ["release-guard", "release-guard-hook"].includes(args[0] ?? "")) return "allowed";
|
|
62
65
|
if (command === "role") return ["list", "show"].includes(args[0] ?? "") ? "allowed" : "console";
|
|
63
66
|
if (command === "task") {
|
|
64
67
|
if (["propose", "show"].includes(args[0] ?? "")) return "allowed";
|
|
@@ -66,6 +69,8 @@ export function classifyPeerCommand(command: string, args: readonly string[], ca
|
|
|
66
69
|
return "console";
|
|
67
70
|
}
|
|
68
71
|
if (command === "route") return args[0] === "explain" ? "allowed" : "console";
|
|
72
|
+
// #691: reading agent CLI versions is harmless; trusting an untested version on this machine is a person's.
|
|
73
|
+
if (command === "agents") return args.every(arg => arg === "--json") ? "allowed" : "console";
|
|
69
74
|
// #247: reading the research measures and records is harmless (ids and counts only); writing them is a person's.
|
|
70
75
|
if (command === "research") return args[0] === "backfill" ? "console" : "allowed";
|
|
71
76
|
// #258: reading hub memory is harmless; promote, demote, forget, supersede and rebuild are a person's.
|
|
@@ -78,7 +83,7 @@ export function classifyPeerCommand(command: string, args: readonly string[], ca
|
|
|
78
83
|
if (native?.cli.authority === "person") return "console";
|
|
79
84
|
if (native || ["pause", "resume"].includes(command)) return "conductor";
|
|
80
85
|
if (command === "board" && args[0] === "import") return "console";
|
|
81
|
-
if (["help", "version", "--version", "completion", "say", "status", "pet", "board", "report", "turns", "review", "remember", "facts", "check-path"].includes(command)) return "allowed";
|
|
86
|
+
if (["help", "version", "--version", "completion", "say", "status", "pet", "board", "report", "usage", "turns", "review", "remember", "facts", "check-path"].includes(command)) return "allowed";
|
|
82
87
|
return "console";
|
|
83
88
|
}
|
|
84
89
|
|
|
@@ -87,14 +92,32 @@ export function cliCommandLabel(command: string, args: readonly string[], catalo
|
|
|
87
92
|
if (command === "backlog") return ["list", "add", "pull", "pull-ready", "split", "return", "plan", "pin", "undo"].includes(args[0] ?? "") ? `backlog ${args[0]}` : "backlog";
|
|
88
93
|
if (command === "board" && args[0] === "import") return "board import";
|
|
89
94
|
if (command === "release") return ["new", "list", "show", "plan", "activate", "freeze", "ship", "drop"].includes(args[0] ?? "") ? `release ${args[0]}` : "release";
|
|
90
|
-
if (!(catalog?.definitionOf(command) ?? 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";
|
|
91
|
-
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
95
|
+
if (!(catalog?.definitionOf(command) ?? 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", "agents"].includes(command)) return "unknown";
|
|
96
|
+
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench", "agents"].includes(command)) {
|
|
92
97
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
93
|
-
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}`;
|
|
98
|
+
if (sub && ["propose", "show", "assign", "unassign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "reattach", "setup", "start", "stop", "remove", "discard", "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", "trust"].includes(sub)) return `${command} ${sub}`;
|
|
94
99
|
}
|
|
95
100
|
return (/^[a-z][a-z0-9-]*$/.test(command) && !/\s/.test(command)) || ["--help", "--version"].includes(command) ? command : "unknown";
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
/**
|
|
104
|
+
* #711: a hub refusal that only the person or the task's own reviewer could have avoided ("only its reviewer (user) or the console user can do that",
|
|
105
|
+
* "only the person may ..."). The wording is the hub's, so this reads it and never invents it: anything else gets no extra line.
|
|
106
|
+
*/
|
|
107
|
+
export const personOnlyRefusal = (error: string): boolean => /\bonly\b[^.]*\b(?:console user|the person)\b/i.test(error);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* #711: the extra stderr line for an agent shell that was refused something only the person may do. The refusal itself and the authority are unchanged: this only
|
|
111
|
+
* says why the shell was treated as an agent and where to act as the person. `undefined` for the console identity and for any refusal that is not person-only.
|
|
112
|
+
*/
|
|
113
|
+
export function agentShellCause(identity: CliIdentity, error: string, env: Readonly<Record<string, string | undefined>> = process.env): string | undefined {
|
|
114
|
+
if (identity.role !== "tools" || !personOnlyRefusal(error)) return undefined;
|
|
115
|
+
const markers = ["AGENTHUB_PEER_ID", "CLAUDECODE", "CLAUDE_CODE_SESSION_ID", "CODEX_THREAD_ID"].filter(name => env[name] !== undefined);
|
|
116
|
+
const set = markers.length ? `${markers.join(" and ")} ${markers.length === 1 ? "is" : "are"} set` : "an agent marker is set";
|
|
117
|
+
const task = /\btask #(\d+)\b/.exec(error)?.[1];
|
|
118
|
+
return `This shell runs as agent "${identity.peer}" (${set}), not as you. Run the command in your own terminal, or use the ahub console: ${task ? `Tasks, select #${task}, r` : "Tasks, select the task, r"}.`;
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
export function peerCommandRefusal(peer: string, command: string, env: Readonly<Record<string, string | undefined>> = process.env): string {
|
|
99
122
|
const markers = ["CLAUDECODE", "CLAUDE_CODE_SESSION_ID", "AGENTHUB_PEER_ID", "CODEX_THREAD_ID"].filter(name => env[name] !== undefined);
|
|
100
123
|
return `${peer} cannot run ahub ${command}; the person runs it in ahub console or a terminal. Refused because inherited agent markers${markers.length ? ` (${markers.join(", ")})` : ""} identify this shell as an agent. A ! command in an agent session runs as that agent; use a separate terminal outside the agent session`;
|