@staix/agent-hub 0.12.28 → 0.12.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +79 -219
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +1 -1
- package/docs/agent-notes/bus.md +3 -1
- package/docs/agent-notes/daemon.md +13 -0
- package/docs/agent-notes/local-worker.md +2 -2
- package/docs/agent-notes/tasks.md +9 -0
- package/docs/agent-notes/tests.md +1 -1
- package/docs/memory.md +70 -152
- package/docs/operations.md +237 -14
- package/docs/plans/2026-10-16-amem-failure-registry.md +101 -0
- package/docs/plans/2026-10-16-amem-p0-baseline.md +110 -0
- package/docs/plans/2026-10-16-amem-p2-read-tools.md +28 -0
- package/docs/plans/amem-cases.json +482 -0
- package/docs/quickstart.md +9 -2
- package/docs/security.md +39 -2
- package/docs/smoke.md +65 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +113 -5
- package/docs/verification/2026-10-16-0.12.29.md +25 -0
- package/docs/verification/2026-10-16-273-live-verification.md +150 -0
- package/docs/verification/2026-10-17-0.12.30.md +70 -0
- package/docs/verified.json +13 -13
- package/package.json +5 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +64 -44
- package/src/cli/console-state.ts +92 -20
- package/src/cli/console.ts +25 -6
- package/src/cli/help.ts +4 -2
- package/src/cli/identity-audit.ts +2 -2
- package/src/cli/identity.ts +15 -1
- package/src/cli/main.js +2 -2
- package/src/cli/main.ts +78 -39
- package/src/cli/memory.ts +87 -187
- package/src/cli/output.ts +6 -0
- package/src/cli/recovery-package.ts +59 -10
- package/src/cli/upgrade-runtime.ts +60 -18
- package/src/cli/upgrade.ts +8 -0
- package/src/hub/board.ts +2 -1
- package/src/hub/bus.ts +33 -12
- package/src/hub/daemon.ts +141 -31
- package/src/hub/envelope.ts +30 -0
- package/src/hub/events.ts +1 -1
- package/src/hub/git.ts +2 -38
- package/src/hub/hub-tools.ts +5 -1
- package/src/hub/lifecycle.ts +2 -2
- package/src/hub/manager.ts +15 -11
- package/src/hub/memory-candidates.ts +103 -0
- package/src/hub/memory-fail.ts +25 -0
- package/src/hub/memory-observations.ts +47 -0
- package/src/hub/memory-plan-guard.ts +33 -0
- package/src/hub/memory-policy.ts +42 -0
- package/src/hub/memory-profile-check.ts +170 -0
- package/src/hub/memory-profile.ts +192 -0
- package/src/hub/memory-staleness.ts +55 -0
- package/src/hub/path-policy.ts +8 -0
- package/src/hub/project.ts +4 -37
- package/src/hub/read-tools.ts +127 -0
- package/src/hub/tasks.ts +201 -41
- package/src/hub/ui.ts +10 -7
- package/src/local/deny.ts +4 -17
- package/src/memory/brief.ts +8 -5
- package/src/memory/capture.ts +2 -1
- package/src/memory/recall.ts +50 -110
- package/src/pi/process-signature.ts +2 -42
- package/src/memory/import.ts +0 -90
- package/src/memory/store.ts +0 -805
- package/src/memory/sync.ts +0 -145
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { cliCommandLabel } from "./identity.ts";
|
|
4
4
|
|
|
5
5
|
export interface CliAudit {
|
|
6
|
-
v: 1; id: string; at: number; peer: string; command: string; outcome: "run" | "refused" | "invalid";
|
|
6
|
+
v: 1; id: string; at: number; peer: string; command: string; outcome: "run" | "refused" | "invalid" | "delegated";
|
|
7
7
|
}
|
|
8
8
|
const MAX_FILES = 256;
|
|
9
9
|
const MAX_BYTES = 64 * 1024;
|
|
@@ -14,7 +14,7 @@ const valid = (row: any): row is CliAudit => row?.v === 1 && typeof row.id === "
|
|
|
14
14
|
&& typeof row.at === "number" && Number.isFinite(row.at) && row.at > 0
|
|
15
15
|
&& typeof row.peer === "string" && /^[a-z][a-z0-9-]{0,31}$/.test(row.peer) && !/\s/.test(row.peer)
|
|
16
16
|
&& typeof row.command === "string" && row.command.length <= 64 && row.command === cliCommandLabel(...splitLabel(row.command))
|
|
17
|
-
&& ["run", "refused", "invalid"].includes(row.outcome);
|
|
17
|
+
&& ["run", "refused", "invalid", "delegated"].includes(row.outcome);
|
|
18
18
|
const splitLabel = (label: string): [string, string[]] => { const [command = "unknown", ...args] = label.split(" "); return [command, args]; };
|
|
19
19
|
|
|
20
20
|
/**
|
package/src/cli/identity.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { isSupportedPeer } from "../hub/peer-catalog.ts";
|
|
2
2
|
|
|
3
|
+
/** Recovery reads this declaration before delegating to another package's CLI. */
|
|
4
|
+
export const LIVE_TEST_SUPPORTED = true;
|
|
5
|
+
|
|
6
|
+
/** Person verdicts and user-authored workflow content are never delegated. */
|
|
7
|
+
export function liveTestRefusal(command: string, args: readonly string[]): boolean {
|
|
8
|
+
if (args.some(arg => arg === "--as-user" || arg.startsWith("--as-user="))) return true;
|
|
9
|
+
const sub = args.find(arg => arg !== "--json");
|
|
10
|
+
return ["task", "role", "goal"].includes(command)
|
|
11
|
+
|| (command === "backlog" && sub !== "list") || (command === "release" && !["list", "show"].includes(sub ?? ""))
|
|
12
|
+
|| (command === "memory" && sub !== "graph")
|
|
13
|
+
|| (command === "board" && args[0] === "import") || (command === "sdlc" && args[0] === "record")
|
|
14
|
+
|| (command === "bench" && args.find(arg => arg !== "--json") === "run") || command === "pet";
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
export type CliIdentity =
|
|
4
18
|
| { role: "console" }
|
|
5
19
|
| { role: "tools"; peer: string }
|
|
@@ -63,7 +77,7 @@ export function cliCommandLabel(command: string, args: readonly string[]): strin
|
|
|
63
77
|
if (!["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "claude", "codex", "kimi", "qwen", "jcode", "pi", "local", "stop", "models", "say", "queue", "tail", "watch", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "permission", "settings", "roster", "status", "pet", "logs", "export", "report", "research", "memory", "goal", "bench", "sdlc", "role", "facts", "check-path", "turns", "undo", "kill", "reset", "doctor"].includes(command)) return "unknown";
|
|
64
78
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
65
79
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
66
|
-
if (sub && ["propose", "show", "assign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "dedupe", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
|
|
80
|
+
if (sub && ["propose", "show", "assign", "unassign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "dedupe", "decay", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
|
|
67
81
|
}
|
|
68
82
|
return (/^[a-z][a-z0-9-]*$/.test(command) && !/\s/.test(command)) || ["--help", "--version"].includes(command) ? command : "unknown";
|
|
69
83
|
}
|
package/src/cli/main.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Keep this entrypoint plain JavaScript: Node must fail before loading TypeScript or bun:sqlite.
|
|
3
3
|
const version = /^(\d+)\.(\d+)\.(\d+)$/.exec(process.versions.bun ?? "");
|
|
4
|
-
if (!version || !(Number(version[1]) > 1 || (Number(version[1]) === 1 && Number(version[2])
|
|
5
|
-
console.error("ahub: Bun >=1.
|
|
4
|
+
if (!version || !(Number(version[1]) > 1 || (Number(version[1]) === 1 && (Number(version[2]) > 4 || (Number(version[2]) === 4 && Number(version[3]) >= 2))))) {
|
|
5
|
+
console.error("ahub: Bun >=1.4.2 is required. Install Bun: https://bun.sh");
|
|
6
6
|
process.exit(1);
|
|
7
7
|
}
|
|
8
8
|
|
package/src/cli/main.ts
CHANGED
|
@@ -59,11 +59,13 @@ import { armRuns, benchCompare, benchCsv, benchReport, formatCompare, formatRepo
|
|
|
59
59
|
import { benchPreflight, defaultIo, runBench } from "./bench.ts";
|
|
60
60
|
import { ADDONS, isAddon, type AddonName } from "../hub/tooling.ts";
|
|
61
61
|
import { goalCommand, memoryCommand } from "./memory.ts";
|
|
62
|
+
import { selectedMemoryProfile } from "../hub/memory-profile.ts";
|
|
63
|
+
import { checkMemoryProfiles, formatMemoryProfileCheck } from "../hub/memory-profile-check.ts";
|
|
62
64
|
import { addonReport, appendRecords, formatAddons, formatResearch, LABELS, labelTarget, RESEARCH_SCHEMA, projectKey, readStores, researchReport, taskRecords, toCsv, type Label, type TaskRecord } from "../hub/research.ts";
|
|
63
65
|
import { summarize, summarizeByTask } from "../hub/report.ts";
|
|
64
66
|
import { hasTree, planUndo, repoOf, restore, Turns } from "../hub/snapshots.ts";
|
|
65
67
|
import { pathWarnings } from "../hub/conflicts.ts";
|
|
66
|
-
import { classifyPeerCommand, cliCommandLabel, detectCliIdentity, peerCommandRefusal } from "./identity.ts";
|
|
68
|
+
import { classifyPeerCommand, cliCommandLabel, detectCliIdentity, liveTestRefusal, peerCommandRefusal } from "./identity.ts";
|
|
67
69
|
import { recordCliAudit } from "./identity-audit.ts";
|
|
68
70
|
import { runConsole } from "./console.ts";
|
|
69
71
|
import { paint, permissionBoundary, resolveColor, wrap, type Span } from "./console-state.ts";
|
|
@@ -83,10 +85,16 @@ const since = (): number => {
|
|
|
83
85
|
|
|
84
86
|
const argv = process.argv.slice(2);
|
|
85
87
|
let selector: string | undefined;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
let liveTest = false;
|
|
89
|
+
while (argv[0] === "--project" || argv[0] === "--live-test") {
|
|
90
|
+
if (argv.shift() === "--live-test") {
|
|
91
|
+
if (liveTest) fail("--live-test may appear only once");
|
|
92
|
+
liveTest = true;
|
|
93
|
+
} else {
|
|
94
|
+
if (selector !== undefined) fail("--project may appear only once");
|
|
95
|
+
selector = argv.shift();
|
|
96
|
+
if (!selector || selector.startsWith("--")) fail("--project needs a path or project ID");
|
|
97
|
+
}
|
|
90
98
|
}
|
|
91
99
|
const [given = "help", ...rest] = argv;
|
|
92
100
|
// `ahub <command> -h` alone asks for that command's help instead of running it. Elsewhere `-h` may be message text
|
|
@@ -114,17 +122,27 @@ const projectConfig = () => {
|
|
|
114
122
|
return config;
|
|
115
123
|
};
|
|
116
124
|
const stateDir = selected.stateDir;
|
|
117
|
-
const
|
|
125
|
+
const caller = detectCliIdentity(process.env);
|
|
118
126
|
const commandLabel = cliCommandLabel(cmd, args);
|
|
119
127
|
const commandAccess = classifyPeerCommand(cmd, args);
|
|
128
|
+
// Agent-authored content keeps its peer. Operational starts/holds may borrow console authority for live verification.
|
|
129
|
+
const delegationRefused = liveTest && caller.role === "tools" && commandAccess !== "allowed" && liveTestRefusal(cmd, args);
|
|
130
|
+
const delegatedPeer = liveTest && caller.role === "tools" && commandAccess !== "allowed" && !delegationRefused ? caller.peer : undefined;
|
|
131
|
+
const identity = delegatedPeer ? { role: "console" as const } : caller;
|
|
120
132
|
function audit(outcome: "run" | "refused" | "invalid"): void {
|
|
121
|
-
if (
|
|
122
|
-
try { recordCliAudit(stateDir,
|
|
123
|
-
catch {
|
|
133
|
+
if (caller.role === "console") return;
|
|
134
|
+
try { recordCliAudit(stateDir, caller.role === "tools" ? caller.peer : "unknown", commandLabel, delegatedPeer && outcome === "run" ? "delegated" : outcome); }
|
|
135
|
+
catch {
|
|
136
|
+
if (delegatedPeer) fail("live-test delegation audit unavailable; nothing was executed");
|
|
137
|
+
console.error("ahub: CLI audit could not be recorded (outbox unavailable or full)");
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
if (identity.role === "invalid") { audit("invalid"); fail(`${identity.reason}; use ahub console or a terminal with no agent markers`); }
|
|
141
|
+
if (delegationRefused) { audit("refused"); fail("live-test cannot write person-attributed content; use peer tools or a person's terminal"); }
|
|
126
142
|
if (identity.role === "tools" && commandAccess === "console") { audit("refused"); fail(peerCommandRefusal(identity.peer, commandLabel)); }
|
|
127
|
-
if (identity.role === "tools") audit("run");
|
|
143
|
+
if (identity.role === "tools" || delegatedPeer) audit("run");
|
|
144
|
+
// Forward only to ahub's own continuation commands, never to native agent argv or an environment flag.
|
|
145
|
+
const liveTestArgs = liveTest ? ["--live-test"] : [];
|
|
128
146
|
const invokedFrom = process.cwd(); // relative paths a person types (a bench suite) are theirs, not the project's
|
|
129
147
|
try { process.chdir(cwd); } catch { fail(`project directory is unavailable: ${cwd}`); }
|
|
130
148
|
/** One-shot output flags do not alter the fetched JSON document. */
|
|
@@ -381,7 +399,7 @@ async function hold(t: "pause" | "resume"): Promise<void> {
|
|
|
381
399
|
}
|
|
382
400
|
|
|
383
401
|
function spawnRecovery(operation: RecoveryOperation, quiet = false): void {
|
|
384
|
-
const child = spawn(process.execPath, [join(operation.sourceRoot, "src/cli/main.js"), "recovery-run", operation.id], {
|
|
402
|
+
const child = spawn(process.execPath, [join(operation.sourceRoot, "src/cli/main.js"), ...liveTestArgs, "recovery-run", operation.id], {
|
|
385
403
|
cwd, detached: true, stdio: "ignore", env: { ...process.env, AGENTHUB_RECOVERY_OPERATION: operation.id },
|
|
386
404
|
});
|
|
387
405
|
child.on("error", () => console.error(`runner launch failed; use ${recoveryCommand(operation, "resume")}`));
|
|
@@ -415,7 +433,7 @@ async function onTerminal(screens: (io: ScreenIO) => Promise<void>): Promise<voi
|
|
|
415
433
|
// The command owns the terminal while it runs; a reader whose input already ended has nothing to pause.
|
|
416
434
|
if (!ended) rl.pause();
|
|
417
435
|
const done = (code: number) => { if (!ended) rl.resume(); resolve(code); };
|
|
418
|
-
const child = spawn(process.execPath, argv, { cwd, stdio: "inherit" });
|
|
436
|
+
const child = spawn(process.execPath, [argv[0]!, ...liveTestArgs, ...argv.slice(1)], { cwd, stdio: "inherit" });
|
|
419
437
|
child.on("error", () => done(1));
|
|
420
438
|
child.on("close", (code) => done(code ?? 1));
|
|
421
439
|
}),
|
|
@@ -444,7 +462,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
444
462
|
lock: recoveryLock,
|
|
445
463
|
read: (id) => readOperation<RecoveryOperation>(id),
|
|
446
464
|
runner: recoveryRunner,
|
|
447
|
-
live: (op) => liveProjects(op, makeRecoveryDriver().inspect),
|
|
465
|
+
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer).inspect),
|
|
448
466
|
endPeer: (planned, peer) => endPlannedPeer(planned, peer),
|
|
449
467
|
runnerStoppable: (id) => signedRunner(id) !== undefined,
|
|
450
468
|
stopRunner: (id) => stopSignedRunner(id),
|
|
@@ -470,7 +488,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
470
488
|
if (to !== undefined) { try { exactVersion(to); } catch (error) { fail((error as Error).message); } }
|
|
471
489
|
// An older CLI must not coordinate a newer target (docs/operations.md): the target's own coordinator takes over.
|
|
472
490
|
if (kind === "upgrade" && newerVersion(to!, VERSION) && process.env.AGENTHUB_UPGRADE_HANDOVER !== "1") {
|
|
473
|
-
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", "upgrade", "--to", to!, ...rest];
|
|
491
|
+
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", ...liveTestArgs, "upgrade", "--to", to!, ...rest];
|
|
474
492
|
console.error(`ahub: ${to} is newer than this CLI (${VERSION}), so its own coordinator runs the upgrade: bun ${argv.join(" ")}`);
|
|
475
493
|
if (interactive) {
|
|
476
494
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -532,31 +550,31 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
532
550
|
const runner = recoveryRunner(id);
|
|
533
551
|
if (action === "status") {
|
|
534
552
|
// #215: abort and resume are offered by what the open sources show, read as the runner's errors read them.
|
|
535
|
-
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver().inspect);
|
|
553
|
+
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
536
554
|
console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
537
555
|
}
|
|
538
|
-
else if (action === "abort") { await abortRecovery(id, makeRecoveryDriver()); console.log("preflight cancelled; no committed transition was rolled back"); }
|
|
556
|
+
else if (action === "abort") { await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer)); console.log("preflight cancelled; no committed transition was rolled back"); }
|
|
539
557
|
else if (action === "dispose") {
|
|
540
|
-
//
|
|
558
|
+
// Console authority (including explicit --live-test delegation); the reason is kept in the operation's audit.
|
|
541
559
|
const { one, rest: flags } = takeFlags(rest, ["--fresh-session", "--reason"], []);
|
|
542
560
|
const stop = flags.length === 1 && flags[0] === "--stop-and-archive";
|
|
543
561
|
const reason = one["--reason"]?.trim() ?? "";
|
|
544
562
|
if (!reason || reason.length > 500 || (flags.length && !stop) || stop === !!one["--fresh-session"]) fail(usage);
|
|
545
|
-
const result = await disposeRecovery(id, stop ? { stop: true } : { fresh: one["--fresh-session"]! }, reason, makeRecoveryDriver());
|
|
563
|
+
const result = await disposeRecovery(id, stop ? { stop: true } : { fresh: one["--fresh-session"]! }, reason, makeRecoveryDriver(undefined, delegatedPeer));
|
|
546
564
|
if (!stop) { console.log(`${one["--fresh-session"]}: a fresh session is accepted and the lost one is recorded; resuming`); return spawnRecovery(result); }
|
|
547
565
|
console.log(JSON.stringify(publicOperation(result), null, 2));
|
|
548
566
|
console.log(`${result.plan.kind} abandoned, not completed; the recovery lock is released. A project whose target ran starts again with that version's CLI.`);
|
|
549
567
|
}
|
|
550
568
|
else if (["completed", "cancelled"].includes(operation.phase)) console.log(`recovery is already ${operation.phase}`);
|
|
551
569
|
// #215: an operation being abandoned is never resumed, whichever coordinator started it (an older one would).
|
|
552
|
-
else if (operation.disposition) fail(`a stop-and-archive of this operation is partway; nothing was resumed; ${nextActionsText(operation, await liveProjects(operation, makeRecoveryDriver().inspect))}`);
|
|
570
|
+
else if (operation.disposition) fail(`a stop-and-archive of this operation is partway; nothing was resumed; ${nextActionsText(operation, await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect))}`);
|
|
553
571
|
else if (runner === "unknown") console.log(`whether a runner holds this operation could not be read; nothing was started; ${recoveryCommand(operation, "status")}`);
|
|
554
572
|
else if (runner) console.log(`runner ${runner} is still working on this operation; ${recoveryCommand(operation, "status")}`);
|
|
555
573
|
else spawnRecovery(operation);
|
|
556
574
|
},
|
|
557
575
|
"recovery-run": async () => {
|
|
558
576
|
if (args.length !== 1 || process.env.AGENTHUB_RECOVERY_OPERATION !== args[0]) fail("recovery-run is an internal command");
|
|
559
|
-
const result = await runRecovery(args[0]!, makeRecoveryDriver());
|
|
577
|
+
const result = await runRecovery(args[0]!, makeRecoveryDriver(undefined, delegatedPeer));
|
|
560
578
|
if (result.phase !== "completed") process.exitCode = 1;
|
|
561
579
|
},
|
|
562
580
|
help: () => {
|
|
@@ -593,12 +611,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
593
611
|
return console.log("dashboard manager stopped; project hubs were kept running");
|
|
594
612
|
}
|
|
595
613
|
let url: string;
|
|
596
|
-
if (args.includes("--all")) url = await openManager();
|
|
614
|
+
if (args.includes("--all")) url = await openManager({ liveTest: !!delegatedPeer });
|
|
597
615
|
else {
|
|
598
616
|
const hub = await connect();
|
|
599
617
|
try {
|
|
600
|
-
const res = await hub.request({ t: "ui", ...(settings ? { settings: true } : {}) }, 10_000);
|
|
618
|
+
const res = await hub.request({ t: "ui", ...(settings ? { settings: true } : {}), ...(delegatedPeer ? { liveTest: true } : {}) }, 10_000);
|
|
601
619
|
if (!res.ok) fail(res.error);
|
|
620
|
+
if (delegatedPeer && res.liveTest !== true) fail("this hub cannot restrict delegated dashboard content; use a live-test-capable hub");
|
|
602
621
|
// A hub older than #269 ignores the field and would hand out an ordinary session without saying so.
|
|
603
622
|
if (settings && res.settings !== true) fail("this hub does not open settings sessions; upgrade the running hub to use ahub ui --settings");
|
|
604
623
|
url = res.url;
|
|
@@ -673,17 +692,17 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
673
692
|
if (args.some(arg => arg !== "--panels" && arg !== "--no-pet" && !arg.startsWith("--color=")) || args.filter(arg => arg.startsWith("--color=")).length > 1) fail("usage: ahub console [--panels] [--no-pet] [--color=auto|always|never]");
|
|
674
693
|
const color = resolveColor(args.find(arg => arg.startsWith("--color="))?.slice("--color=".length), { isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR });
|
|
675
694
|
if (color instanceof Error) return fail("usage: ahub console [--panels] [--no-pet] [--color=auto|always|never]");
|
|
676
|
-
await runConsole({ client: await connect(), cwd, stateDir, panels: args.includes("--panels"), noPet: args.includes("--no-pet"), color });
|
|
695
|
+
await runConsole({ client: await connect(), cwd, stateDir, liveTest, panels: args.includes("--panels"), noPet: args.includes("--no-pet"), color });
|
|
677
696
|
},
|
|
678
697
|
|
|
679
698
|
up: async () => {
|
|
680
699
|
const unattended = unattendedEnv || args.includes("--unattended");
|
|
681
700
|
if (unattended) console.error(UNATTENDED_WARNING);
|
|
682
|
-
const status = await startProject(registeredProject(), { unattended });
|
|
701
|
+
const status = await startProject(registeredProject(), { unattended, liveTest });
|
|
683
702
|
console.log(`ahub up (ws://127.0.0.1:${status.controlPort}), state in ${stateDir}`);
|
|
684
703
|
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
685
704
|
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
686
|
-
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir });
|
|
705
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir, liveTest });
|
|
687
706
|
},
|
|
688
707
|
|
|
689
708
|
claude: async () => {
|
|
@@ -958,17 +977,24 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
958
977
|
return;
|
|
959
978
|
}
|
|
960
979
|
const [id, ...flags] = rest;
|
|
961
|
-
if (!id || !["show", "resolve"].includes(operation)) fail("usage: ahub queue show <id> | resolve <id> --action completed|retry|discard --reason <text>");
|
|
962
|
-
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
963
|
-
if (!shown.ok) fail(shown.error);
|
|
980
|
+
if (!id || !["show", "resolve"].includes(operation)) fail("usage: ahub queue show <id> | resolve <id> --action completed|retry|discard --reason <text> [--revision N]");
|
|
964
981
|
if (operation === "show") {
|
|
965
982
|
if (flags.length) fail("usage: ahub queue show <id>");
|
|
983
|
+
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
984
|
+
if (!shown.ok) fail(shown.error);
|
|
966
985
|
if (options.json) return console.log(JSON.stringify(shown.delivery, null, 2));
|
|
967
986
|
return printOutput(renderQueueShow(shown.delivery, options.columns, Date.now()), options.color);
|
|
968
987
|
}
|
|
969
|
-
const { one, rest: extra } = takeFlags(
|
|
970
|
-
|
|
971
|
-
|
|
988
|
+
const { one, rest: extra } = takeFlags(flags, ["--action", "--reason", "--revision"], []);
|
|
989
|
+
const rawRevision = one["--revision"], parsedRevision = rawRevision === undefined ? undefined : Number(rawRevision);
|
|
990
|
+
if (extra.length || !["completed", "retry", "discard"].includes(one["--action"] ?? "") || !one["--reason"]?.trim() || (rawRevision !== undefined && (!/^\d+$/.test(rawRevision) || !Number.isSafeInteger(parsedRevision)))) fail("usage: ahub queue resolve <id> --action completed|retry|discard --reason <text> [--revision N]");
|
|
991
|
+
let revision = parsedRevision;
|
|
992
|
+
if (revision === undefined) {
|
|
993
|
+
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
994
|
+
if (!shown.ok) fail(shown.error);
|
|
995
|
+
revision = shown.delivery.revision;
|
|
996
|
+
}
|
|
997
|
+
const result = await hub.request({ t: "queue", op: "resolve", id, revision, action: one["--action"], reason: one["--reason"] });
|
|
972
998
|
if (!result.ok) fail(result.error);
|
|
973
999
|
console.log(JSON.stringify(result.delivery, null, 2));
|
|
974
1000
|
} finally { hub.close(); }
|
|
@@ -1166,10 +1192,16 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1166
1192
|
if (sub === "show") return console.log(await taskOp("task_show", { id: rest[0] }));
|
|
1167
1193
|
if (sub === "escalate") return console.log(await taskOp("task_escalate", { id: rest[0] }));
|
|
1168
1194
|
if (sub === "assign") {
|
|
1169
|
-
const { one, rest: positional } = takeFlags(rest, ["--role"], []);
|
|
1170
|
-
const role = one["--role"] ?? "owner";
|
|
1171
|
-
if (positional.length !== 2 || !["owner", "reviewer"].includes(role)) fail("usage: ahub task assign <id> <peer> [--role owner|reviewer]");
|
|
1172
|
-
return console.log(await taskOp("task_assign", { id: positional[0], peer: positional[1], ...(role === "reviewer" ? { role } : {}) }));
|
|
1195
|
+
const { one, rest: positional } = takeFlags(rest, ["--role", "--revision"], []);
|
|
1196
|
+
const role = one["--role"] ?? "owner", rawRevision = one["--revision"], revision = rawRevision === undefined ? undefined : Number(rawRevision);
|
|
1197
|
+
if (positional.length !== 2 || !["owner", "reviewer"].includes(role) || (rawRevision !== undefined && (!/^\d+$/.test(rawRevision) || !Number.isSafeInteger(revision)))) fail("usage: ahub task assign <id> <peer> [--role owner|reviewer] [--revision N]");
|
|
1198
|
+
return console.log(await taskOp(revision === undefined ? "task_assign" : "task_assign_checked", { id: positional[0], peer: positional[1], ...(role === "reviewer" ? { role } : {}), ...(revision === undefined ? {} : { revision }) }));
|
|
1199
|
+
}
|
|
1200
|
+
if (sub === "unassign") {
|
|
1201
|
+
const { one, rest: positional } = takeFlags(rest, ["--role", "--revision"], []);
|
|
1202
|
+
const role = one["--role"] ?? "owner", rawRevision = one["--revision"], revision = rawRevision === undefined ? undefined : Number(rawRevision);
|
|
1203
|
+
if (positional.length !== 1 || !["owner", "reviewer"].includes(role) || (rawRevision !== undefined && (!/^\d+$/.test(rawRevision) || !Number.isSafeInteger(revision)))) fail("usage: ahub task unassign <id> [--role owner|reviewer] [--revision N]");
|
|
1204
|
+
return console.log(await taskOp("task_unassign", { id: positional[0], role, ...(revision === undefined ? {} : { revision }) }));
|
|
1173
1205
|
}
|
|
1174
1206
|
if (sub === "label") {
|
|
1175
1207
|
// #247: a person's later verdict on an approved task (the identity gate keeps it human-only), for research records.
|
|
@@ -1182,7 +1214,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1182
1214
|
appendRecords(project.id, [{ schema: RESEARCH_SCHEMA, kind: "label", project: projectKey(project.id), task: id, createdAt: record!.createdAt!, label, at: new Date().toISOString() }]);
|
|
1183
1215
|
return console.log(`task #${id} (approved ${record!.approvedAt}) labelled ${label} in the research records`);
|
|
1184
1216
|
}
|
|
1185
|
-
if (sub !== "propose" || rest.length < 1) fail("usage: ahub task propose [<class>] <title...> | show <id> | assign <id> <peer> | escalate <id>");
|
|
1217
|
+
if (sub !== "propose" || rest.length < 1) fail("usage: ahub task propose [<class>] <title...> | show <id> | assign <id> <peer> | unassign <id> | escalate <id>");
|
|
1186
1218
|
// `--class` is the explicit form. A first word that is a class name is still taken as the class (the documented
|
|
1187
1219
|
// short form), but said out loud: "review the auth module" would otherwise be filed as class review, silently.
|
|
1188
1220
|
const urgent = rest.includes("--urgent"), backlog = rest.includes("--backlog");
|
|
@@ -1394,7 +1426,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1394
1426
|
printOutput(renderStatus({ ...status, budget: quota.budget ?? {} }, options.columns, Date.now(), options.full), options.color);
|
|
1395
1427
|
if (status.needs) { const pet = loadPetConfig(cwd); if (pet.enabled) console.log(renderPet(petView(status), false, pet.character)); }
|
|
1396
1428
|
// #258: open long-term tasks and who holds them, as of this machine's last memory sync.
|
|
1397
|
-
if (existsSync(join(hubHome(), "memory", "index.db"))) {
|
|
1429
|
+
if (status.memoryProfile ? status.memoryProfile.state === "ready" : existsSync(join(hubHome(), "memory", "index.db"))) {
|
|
1398
1430
|
try { const lines = await goalCommand(["list"], cwd); if (!lines.startsWith("no open")) console.log(`\nlong-term tasks\n${lines}`); } catch { /* memory unreadable: status goes on */ }
|
|
1399
1431
|
}
|
|
1400
1432
|
} finally { hub.close(); }
|
|
@@ -1470,7 +1502,14 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1470
1502
|
},
|
|
1471
1503
|
memory: async () => {
|
|
1472
1504
|
// #258: the hub's own memory on this machine; reads are anyone's, changes a person's.
|
|
1473
|
-
|
|
1505
|
+
if (args.filter(arg => arg !== "--json")[0] === "profile-check") {
|
|
1506
|
+
if (args.some(arg => arg !== "profile-check" && arg !== "--json")) fail("usage: ahub memory profile-check [--json]");
|
|
1507
|
+
const result = await checkMemoryProfiles(selectedMemoryProfile());
|
|
1508
|
+
console.log(args.includes("--json") ? JSON.stringify(result, null, 2) : formatMemoryProfileCheck(result));
|
|
1509
|
+
if (!result.ready) process.exitCode = 1;
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
try { console.log(await memoryCommand(args, cwd, { person: caller.role === "console" })); } catch (e) { fail((e as Error).message); }
|
|
1474
1513
|
},
|
|
1475
1514
|
goal: async () => {
|
|
1476
1515
|
// #258: long-term tasks; a link goes through the project's hub, so Tasks validates the board task.
|
|
@@ -1479,7 +1518,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1479
1518
|
try { const r = await hub.request({ t: "goal_link", goal, task: Number(task) }); if (!r.ok) throw new Error(String(r.error)); return String(r.text); }
|
|
1480
1519
|
finally { hub.close(); }
|
|
1481
1520
|
};
|
|
1482
|
-
try { console.log(await goalCommand(args, cwd, { link })); } catch (e) { fail((e as Error).message); }
|
|
1521
|
+
try { console.log(await goalCommand(args, cwd, { link, person: caller.role === "console" })); } catch (e) { fail((e as Error).message); }
|
|
1483
1522
|
},
|
|
1484
1523
|
research: () => {
|
|
1485
1524
|
// #247: measures from the opt-in research records; `export` writes them out, `backfill` builds them from events.jsonl.
|