@staix/agent-hub 0.25.0 → 0.26.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 +10 -0
- package/README.md +10 -5
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +1 -1
- package/docs/agent-notes/daemon.md +6 -4
- package/docs/agent-notes/tasks.md +5 -4
- package/docs/agent-notes/tests.md +1 -0
- package/docs/dogfood.md +4 -0
- package/docs/operations.md +131 -14
- package/docs/quickstart.md +14 -4
- package/docs/security.md +51 -8
- package/docs/specs/2026-11-01-research-campaigns-design.md +9 -6
- package/docs/verification/2026-11-01-0.26.0.md +54 -0
- package/docs/verified.json +28 -16
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +182 -95
- package/src/adapters/channel-peer.ts +4 -2
- package/src/adapters/definitions/builtin-metadata.ts +6 -6
- package/src/adapters/definitions/qwen.ts +2 -0
- package/src/adapters/registry-types.ts +2 -0
- package/src/adapters/registry.ts +1 -0
- package/src/cli/conductor-recovery.ts +3 -2
- package/src/cli/help.ts +7 -7
- package/src/cli/main.ts +62 -23
- package/src/cli/recovery-package.ts +94 -4
- package/src/cli/reset.ts +14 -4
- package/src/cli/runtime-version-warning.ts +28 -0
- package/src/cli/start-agents-run.ts +125 -0
- package/src/cli/task-output.ts +5 -0
- package/src/cli/upgrade-interactive.ts +4 -4
- package/src/cli/upgrade-runtime.ts +105 -57
- package/src/cli/upgrade.ts +90 -28
- package/src/hub/autonomy-defaults.ts +32 -7
- package/src/hub/board.ts +129 -13
- package/src/hub/budget.ts +3 -4
- package/src/hub/child-process.ts +4 -3
- package/src/hub/conductor.ts +3 -4
- package/src/hub/config-apply.ts +13 -2
- package/src/hub/config-edit-host.ts +1 -0
- package/src/hub/config-tier.ts +3 -0
- package/src/hub/config-trust.ts +10 -2
- package/src/hub/control-client.ts +30 -10
- package/src/hub/daemon.ts +82 -17
- package/src/hub/db-schema.ts +143 -0
- package/src/hub/delivery-journal.ts +3 -4
- package/src/hub/execution-budget.ts +3 -4
- package/src/hub/folder-trigger.ts +3 -4
- package/src/hub/invocations.ts +3 -4
- package/src/hub/lifecycle.ts +87 -29
- package/src/hub/manager.ts +49 -33
- package/src/hub/orca-env.ts +19 -0
- package/src/hub/project-runtime-state.ts +139 -0
- package/src/hub/project-runtime.ts +62 -0
- package/src/hub/protocol-window.ts +19 -0
- package/src/hub/recovery-store.ts +102 -12
- package/src/hub/registry.ts +5 -0
- package/src/hub/research-experiments.ts +33 -4
- package/src/hub/research-runner.ts +172 -0
- package/src/hub/restart.ts +9 -0
- package/src/hub/runtime-capture-prune.ts +415 -0
- package/src/hub/runtime-prune-health.ts +112 -0
- package/src/hub/schedules.ts +3 -4
- package/src/hub/sdlc-merge.ts +7 -0
- package/src/hub/sdlc-queue.ts +13 -1
- package/src/hub/settings.ts +4 -0
- package/src/hub/snapshots.ts +3 -4
- package/src/hub/start-agents.ts +44 -0
- package/src/hub/task-approval.ts +23 -0
- package/src/hub/tasks.ts +133 -34
- package/src/hub/usage-rollup.ts +3 -4
- package/src/pi/tool-receipts.ts +3 -4
package/src/cli/reset.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { constants, Database } from "bun:sqlite";
|
|
2
|
+
import { assertHubDbCompatible, openHubDatabase } from "../hub/db-schema.ts";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -50,21 +51,30 @@ const isEnvelope = (v: unknown) => isObject(v) && typeof v.id === "string";
|
|
|
50
51
|
/** Valid JSON of the wrong shape is damage as well: the journal refuses it on every open, so the message says so. */
|
|
51
52
|
const shaped = (ok: boolean, what: string) => { if (!ok) throw new Error(`invalid delivery journal: ${what} has the wrong shape`); };
|
|
52
53
|
|
|
54
|
+
/** The existing quiet archive preview owns its raw URI handle and sets its one bounded timeout at open. */
|
|
55
|
+
function quietResetDatabase(file: string): Database {
|
|
56
|
+
const db = new Database(`file:${file.replace(/[%?#]/g, (c) => `%${c.charCodeAt(0).toString(16)}`)}?immutable=1`, constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI);
|
|
57
|
+
try {
|
|
58
|
+
db.run(`PRAGMA busy_timeout = ${HUB_DB_BUSY_TIMEOUT_MS}`);
|
|
59
|
+
return db;
|
|
60
|
+
} catch (error) { db.close(); throw error; }
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
/** What a reset would act on, read from the state directory without changing it (hub.db opened read-only). */
|
|
54
64
|
export function planReset(stateDir: string, projectId: string): ResetPlan {
|
|
55
65
|
const plan: ResetPlan = { queued: [], needsReview: [], inFlight: [], manualHolds: [], budgetPauses: [], conductorHolds: [],
|
|
56
66
|
sessionPointers: sessionPointers(stateDir).filter((name) => existsSync(join(stateDir, name))), entries: existsSync(stateDir) ? readdirSync(stateDir).length : 0 };
|
|
57
67
|
const file = join(stateDir, "hub.db");
|
|
58
68
|
if (!existsSync(file)) return plan;
|
|
69
|
+
assertHubDbCompatible(file); // A normal readonly guard sees WAL metadata before the quiet preview may use immutable.
|
|
59
70
|
// A read-only open of a WAL database creates hub.db-wal and -shm when the last writer's close removed them (Linux),
|
|
60
71
|
// and fails without them on macOS. Without them no writer is mid-transaction, so `immutable` reads hub.db alone and
|
|
61
72
|
// writes nothing. ponytail: a -wal left without its -shm (deleted by hand) is ignored then, and commits still in
|
|
62
73
|
// it go unlisted; open plainly in that corner if it ever matters. SQLITE_OPEN_URI is passed because Linux builds do
|
|
63
74
|
// not parse `file:` names by default (the name then fails as a plain path).
|
|
64
75
|
const quiet = !existsSync(`${file}-wal`) || !existsSync(`${file}-shm`);
|
|
65
|
-
const db = quiet ?
|
|
76
|
+
const db = quiet ? quietResetDatabase(file) : openHubDatabase(file, { readonly: true });
|
|
66
77
|
try {
|
|
67
|
-
db.run(`PRAGMA busy_timeout = ${HUB_DB_BUSY_TIMEOUT_MS}`);
|
|
68
78
|
const has = (table: string) => !!db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
69
79
|
const peers = (table: string) => has(table) ? (db.query(`SELECT peer FROM ${table} ORDER BY peer`).all() as { peer: string }[]).map((row) => row.peer) : [];
|
|
70
80
|
plan.budgetPauses = peers("budget_pauses");
|
|
@@ -120,6 +130,7 @@ export function resetLines(plan: ResetPlan): string[] {
|
|
|
120
130
|
*/
|
|
121
131
|
export function resetRuntime(stateDir: string, project: Project): { settled: string[]; plan: ResetPlan; stale: string[] } {
|
|
122
132
|
const file = join(stateDir, "hub.db");
|
|
133
|
+
assertHubDbCompatible(file); // Refuse before journal settlement or any direct runtime-state mutation.
|
|
123
134
|
const plan = planReset(stateDir, project.id);
|
|
124
135
|
const settled: string[] = [];
|
|
125
136
|
if (existsSync(file)) {
|
|
@@ -135,9 +146,8 @@ export function resetRuntime(stateDir: string, project: Project): { settled: str
|
|
|
135
146
|
}
|
|
136
147
|
bus.setManualPaused([]);
|
|
137
148
|
} finally { bus.closeJournal(); }
|
|
138
|
-
const db =
|
|
149
|
+
const db = openHubDatabase(file);
|
|
139
150
|
try {
|
|
140
|
-
db.run(`PRAGMA busy_timeout = ${HUB_DB_BUSY_TIMEOUT_MS}`);
|
|
141
151
|
db.transaction(() => {
|
|
142
152
|
for (const table of ["budget_pauses", "conductor_holds"]) {
|
|
143
153
|
if (db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table)) db.run(`DELETE FROM ${table}`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DoctorCheck } from "./output.ts";
|
|
2
|
+
|
|
3
|
+
const VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/;
|
|
4
|
+
const version = (value: unknown): string | undefined => typeof value === "string" && value.length <= 64 && VERSION.exec(value)?.[0] === value ? value : undefined;
|
|
5
|
+
|
|
6
|
+
/** Authenticated daemon status and the existing plugin inventory only. Suggestions never run an update. */
|
|
7
|
+
export function runtimeVersionWarnings(daemonVersion: unknown, cliVersion: unknown, pluginVersion: unknown): DoctorCheck[] {
|
|
8
|
+
const running = version(daemonVersion), cli = version(cliVersion), plugin = version(pluginVersion);
|
|
9
|
+
const rows: DoctorCheck[] = [];
|
|
10
|
+
const unknown = (name: string, detail: string) => rows.push({ section: "Hub", level: "unknown", name, detail });
|
|
11
|
+
if (!running) {
|
|
12
|
+
unknown("runtime version comparison", "authenticated daemon version unavailable; CLI and plugin age are unknown");
|
|
13
|
+
return rows;
|
|
14
|
+
}
|
|
15
|
+
let cliOlder = false;
|
|
16
|
+
if (!cli) unknown("CLI versus daemon", "this CLI version is unavailable; its age relative to the daemon is unknown");
|
|
17
|
+
else try {
|
|
18
|
+
cliOlder = Bun.semver.order(cli, running) < 0;
|
|
19
|
+
if (cliOlder) rows.push({ section: "Hub", level: "warn", name: "CLI versus daemon",
|
|
20
|
+
detail: `running daemon ${running} is newer than this CLI ${cli}; update separately: bun add --global @staix/agent-hub@${running}` });
|
|
21
|
+
} catch { unknown("CLI versus daemon", "CLI and daemon versions could not be compared"); }
|
|
22
|
+
if (!plugin) unknown("plugin versus daemon", "installed plugin version unavailable; run ahub setup with an updated CLI to inspect it");
|
|
23
|
+
else try {
|
|
24
|
+
if (Bun.semver.order(plugin, running) < 0) rows.push({ section: "Hub", level: "warn", name: "plugin versus daemon",
|
|
25
|
+
detail: `running daemon ${running} is newer than installed plugin ${plugin}; run ahub setup${cliOlder ? " after the CLI update" : " with a CLI at least as new as the daemon"}` });
|
|
26
|
+
} catch { unknown("plugin versus daemon", "plugin and daemon versions could not be compared"); }
|
|
27
|
+
return rows;
|
|
28
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ControlClient } from "../hub/control-client.ts";
|
|
4
|
+
import { readAutonomyDefaults, writeMachineStartAgents } from "../hub/autonomy-defaults.ts";
|
|
5
|
+
import { configRefusal } from "../hub/config-trust.ts";
|
|
6
|
+
import { START_AGENTS_DEFAULT, parseStartAgents } from "../hub/start-agents.ts";
|
|
7
|
+
import { MAX_COMMAND_MS } from "./terminal-recovery.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* #608 (agent-hub#1040): the default agent set `ahub up` starts. Every start goes through the console `peer_start`, so it is
|
|
11
|
+
* `hubStart`'s start (the peer's own start mode, the terminal provider, `already` for an attached peer, the manual command for
|
|
12
|
+
* a refusal) and never a headless fallback. Nothing here reaches the daemon except that one message per listed peer.
|
|
13
|
+
*/
|
|
14
|
+
export interface StartRequester { request(message: Record<string, unknown>, timeoutMs?: number): Promise<Record<string, any>> }
|
|
15
|
+
|
|
16
|
+
/** One printable line per agent. A refusal carries the hub's own reason, which already names the manual command. */
|
|
17
|
+
export function startLine(id: string, reply: Record<string, any>): string {
|
|
18
|
+
if (reply.ok === true) {
|
|
19
|
+
if (reply.already === true) return `${id}: already attached`;
|
|
20
|
+
return `${id}: started${reply.mode ? ` ${reply.mode}` : ""}${reply.opened ? ` (${reply.opened})` : ""}${reply.mode === "tui" ? ", it attaches when its TUI is ready" : ""}`;
|
|
21
|
+
}
|
|
22
|
+
return `${id}: not started: ${String(reply.error ?? "the hub refused")}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Asks for every listed peer at once and returns the lines in list order. A refusal or a failed request never stops the others. */
|
|
26
|
+
export async function startDefaultAgents(client: StartRequester, agents: readonly string[]): Promise<string[]> {
|
|
27
|
+
return Promise.all(agents.map(async id => {
|
|
28
|
+
try { return startLine(id, await client.request({ t: "peer_start", peer: id }, MAX_COMMAND_MS + 5_000)); }
|
|
29
|
+
catch (error) { return `${id}: not started: ${(error as Error).message}`; }
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The list `ahub up` starts: the project's machine-local value, else this machine's `ahub setup` choice, else claude and codex. */
|
|
34
|
+
export function effectiveStartAgents(projectRoot: string, projectValue: readonly string[], home: string, roots: readonly string[] = []): { agents: string[]; source: "project" | "machine" | "default"; notice?: string } {
|
|
35
|
+
let set = false;
|
|
36
|
+
try {
|
|
37
|
+
const file = join(projectRoot, ".agenthub", "config.local.json");
|
|
38
|
+
if (existsSync(file)) { const doc = JSON.parse(readFileSync(file, "utf8")); set = Array.isArray(doc?.start?.agents); }
|
|
39
|
+
} catch { set = false; }
|
|
40
|
+
if (set) return { agents: [...projectValue], source: "project" };
|
|
41
|
+
const machine = readAutonomyDefaults(home, roots);
|
|
42
|
+
if (machine.startAgents) return { agents: [...machine.startAgents], source: "machine" };
|
|
43
|
+
return { agents: [...START_AGENTS_DEFAULT], source: "default", ...(machine.notice ? { notice: machine.notice } : {}) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The dashboard manager's and `ahub up`'s common path: a console connection to the running hub, then the set. */
|
|
47
|
+
export async function startProjectAgents(project: { root: string; stateDir: string }, agents: readonly string[], connect?: () => Promise<ControlClient>): Promise<string[]> {
|
|
48
|
+
if (!agents.length) return [];
|
|
49
|
+
let client: ControlClient;
|
|
50
|
+
try { client = await (connect ?? (() => ControlClient.connect(project.stateDir, { role: "console", projectRoot: project.root }, 5_000)))(); }
|
|
51
|
+
catch (error) { return agents.map(id => `${id}: not started: the hub did not answer (${(error as Error).message})`); }
|
|
52
|
+
try { return await startDefaultAgents(client, agents); }
|
|
53
|
+
finally { client.close(); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SetupStartPorts {
|
|
57
|
+
home: string;
|
|
58
|
+
roots: readonly string[];
|
|
59
|
+
/** `--agents <a,b>`, already split from argv, or undefined. */
|
|
60
|
+
flag?: string;
|
|
61
|
+
yes: boolean;
|
|
62
|
+
ask?: (prompt: string) => Promise<string | null>;
|
|
63
|
+
print: (line: string) => void;
|
|
64
|
+
}
|
|
65
|
+
const show = (agents: readonly string[]) => agents.length ? agents.join(", ") : "none";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `ahub setup`'s one question (#608): which agents `ahub up` starts. `--agents` answers it, `--yes` takes this machine's current
|
|
69
|
+
* choice (or claude and codex), a terminal is asked once (an answer already on this machine is shown, not asked again), and a
|
|
70
|
+
* run with no terminal and neither flag keeps the default and says so. Writes only `start.agents` of `~/.agenthub/defaults.json`.
|
|
71
|
+
* Returns the refusal text for a bad `--agents`, else undefined.
|
|
72
|
+
*/
|
|
73
|
+
export async function setupStartAgents(ports: SetupStartPorts): Promise<string | undefined> {
|
|
74
|
+
const held = readAutonomyDefaults(ports.home, ports.roots);
|
|
75
|
+
const current = held.startAgents ?? [...START_AGENTS_DEFAULT];
|
|
76
|
+
const save = (agents: string[]): void => {
|
|
77
|
+
try { writeMachineStartAgents(ports.home, agents, held.fingerprint, ports.roots); ports.print(`Agents ahub up starts: ${show(agents)} (saved in this machine's defaults; a new project inherits it at ahub init).`); }
|
|
78
|
+
catch { ports.print("Agents ahub up starts: not saved, because this machine's defaults.json is unsafe or changed; the default stays (claude, codex)."); }
|
|
79
|
+
};
|
|
80
|
+
if (ports.flag !== undefined) {
|
|
81
|
+
let agents: string[];
|
|
82
|
+
try { agents = parseStartAgents(ports.flag); } catch (error) { return (error as Error).message; }
|
|
83
|
+
save(agents); return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (ports.yes) { if (held.startAgents) ports.print(`Agents ahub up starts: ${show(current)} (kept).`); else save(current); return undefined; }
|
|
86
|
+
if (held.startAgents) { ports.print(`Agents ahub up starts: ${show(current)} (change it with ahub setup --agents <a,b> or ahub settings set start.agents <a,b>).`); return undefined; }
|
|
87
|
+
if (!ports.ask) { ports.print(`Agents ahub up starts: ${show(current)} (the default; no terminal to ask in, so pass --agents <a,b> to choose).`); return undefined; }
|
|
88
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
89
|
+
const answer = await ports.ask(`Agents to start with ahub up [${show(current)}] (comma list, "none" for no agent): `);
|
|
90
|
+
if (answer === null || !answer.trim()) { save(current); return undefined; }
|
|
91
|
+
try { save(parseStartAgents(answer)); return undefined; } catch (error) { ports.print((error as Error).message); }
|
|
92
|
+
}
|
|
93
|
+
ports.print(`Agents ahub up starts: ${show(current)} (no valid answer; the default stays).`);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `ahub init` of a new project (#608): copies this machine's chosen set into the project's `.agenthub/config.local.json`, as the
|
|
99
|
+
* autonomy preset is inherited. Only when `ahub setup` chose one, never over a value the project already has, and never into a
|
|
100
|
+
* file git tracks. Person-only by its caller; returns the lines to print.
|
|
101
|
+
*/
|
|
102
|
+
export function inheritMachineStart(projectRoot: string, home: string, roots: readonly string[]): string[] {
|
|
103
|
+
const machine = readAutonomyDefaults(home, roots);
|
|
104
|
+
if (machine.notice) return [machine.notice];
|
|
105
|
+
if (!machine.startAgents) return [];
|
|
106
|
+
const file = join(projectRoot, ".agenthub", "config.local.json");
|
|
107
|
+
let doc: Record<string, unknown> = {};
|
|
108
|
+
try {
|
|
109
|
+
if (existsSync(file)) {
|
|
110
|
+
const parsed: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
111
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return ["start.agents not copied: .agenthub/config.local.json is not a JSON object"];
|
|
112
|
+
doc = parsed as Record<string, unknown>;
|
|
113
|
+
}
|
|
114
|
+
} catch { return ["start.agents not copied: .agenthub/config.local.json could not be read"]; }
|
|
115
|
+
const start = doc.start;
|
|
116
|
+
if (start !== undefined && (typeof start !== "object" || start === null || Array.isArray(start))) return ["start.agents not copied: start in .agenthub/config.local.json is not an object"];
|
|
117
|
+
if (start && (start as Record<string, unknown>).agents !== undefined) return [];
|
|
118
|
+
const why = configRefusal(projectRoot, "config.local.json");
|
|
119
|
+
if (why) return [`start.agents not copied: ${why}`];
|
|
120
|
+
const text = `${JSON.stringify({ ...doc, start: { ...(start as object | undefined), agents: machine.startAgents } }, null, 2)}\n`;
|
|
121
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
122
|
+
try { writeFileSync(temporary, text, { mode: 0o600, flag: "wx" }); renameSync(temporary, file); }
|
|
123
|
+
catch (error) { try { unlinkSync(temporary); } catch { /* nothing was written */ } return [`start.agents not copied: ${(error as NodeJS.ErrnoException).code ?? "write failed"}`]; }
|
|
124
|
+
return [`Inherited the machine's agent set for ahub up: ${show(machine.startAgents)}.`];
|
|
125
|
+
}
|
package/src/cli/task-output.ts
CHANGED
|
@@ -120,6 +120,11 @@ function taskBlock(task: any, columns: number | undefined, now: number, detail:
|
|
|
120
120
|
const refText = [refs.repo, refs.branch ? `branch ${refs.branch}` : "", refs.commit ? `commit ${short(refs.commit)}` : ""].filter(Boolean).join(", ");
|
|
121
121
|
if (refText) out.push(blank, ...pair("refs", refText, columns, w, undefined, " "));
|
|
122
122
|
out.push(...bullets("ref paths", list(refs.paths), columns, undefined, " "));
|
|
123
|
+
if (!isStub(task) && task?.approval) {
|
|
124
|
+
const approval = task.approval;
|
|
125
|
+
const binding = `head ${approval.head ?? "unknown"}; latest ${approval.latestHead ?? "unknown"}; ${approval.mismatch ? "HEAD MISMATCH, approval is stale" : approval.current ? "current" : "not current, review required"}`;
|
|
126
|
+
out.push(...pair("approval", binding, columns, w, approval.current ? "success" : "attention", " "));
|
|
127
|
+
}
|
|
123
128
|
const plan = planBlock(task?.plan, columns);
|
|
124
129
|
if (plan.length) out.push(blank, [s("plan", "muted")], ...plan);
|
|
125
130
|
const sdlc = task?.sdlc;
|
|
@@ -36,8 +36,8 @@ export interface UpgradeHost {
|
|
|
36
36
|
apply(plan: UpgradePlan, interrupted: () => boolean): Promise<RecoveryOperation>;
|
|
37
37
|
/** End one attached agent now: close a TUI agent's terminal, or ask the hub to stop a headless one. One line of outcome. */
|
|
38
38
|
endPeer(planned: PlannedProject, peer: RecoveryPeer): Promise<string>;
|
|
39
|
-
/**
|
|
40
|
-
lock(): string | undefined;
|
|
39
|
+
/** This selected project's hold, or an authoritative retained legacy machine hold. A reset may name another project. */
|
|
40
|
+
lock(projectRoot?: string): string | undefined;
|
|
41
41
|
read(id: string): RecoveryOperation;
|
|
42
42
|
runner(id: string): number | "unknown" | undefined;
|
|
43
43
|
live(op: RecoveryOperation): Promise<Record<string, Inspection | undefined>>;
|
|
@@ -215,13 +215,13 @@ export async function endAgents(host: UpgradeHost, io: ScreenIO, endable: { plan
|
|
|
215
215
|
|
|
216
216
|
/** `ahub reset` for one project: the scope, its dry run, a confirmation, then the reset itself. */
|
|
217
217
|
export async function resetFlow(host: UpgradeHost, io: ScreenIO, projects: { id: string; root: string }[]): Promise<void> {
|
|
218
|
-
if (host.lock()) return human(io, "an operation holds the recovery lock; cancel or end it before a reset");
|
|
219
218
|
let project = projects[0];
|
|
220
219
|
if (projects.length > 1) {
|
|
221
220
|
projects.forEach((p, i) => human(io, ` ${i + 1} ${p.id} ${p.root}`));
|
|
222
221
|
project = projects[Number(await ask(io, "Reset which project? [number, Enter: none] ")) - 1];
|
|
223
222
|
}
|
|
224
223
|
if (!project) return human(io, "nothing was reset");
|
|
224
|
+
if (host.lock(project.root)) return human(io, "an operation holds the recovery lock; cancel or end it before a reset");
|
|
225
225
|
const scope = ((await ask(io, "[r] runtime reset (deliveries, holds, pauses, session pointers) [a] full reset (archive the state directory) [Enter] none: ")) ?? "").toLowerCase();
|
|
226
226
|
if (scope !== "r" && scope !== "a") return human(io, "nothing was reset");
|
|
227
227
|
const reset = [host.entry, "--project", project.root, "reset", ...(scope === "a" ? ["--all"] : [])];
|
|
@@ -323,7 +323,7 @@ export async function planScreen(host: UpgradeHost, io: ScreenIO): Promise<void>
|
|
|
323
323
|
if (io.presentation) emit(io, renderPlan(plan, host.version, io.presentation, planLines(plan, host.version)));
|
|
324
324
|
else for (const line of planLines(plan, host.version)) human(io, line);
|
|
325
325
|
const blocked = plan.blockers.length > 0 || plan.projects.some((p) => p.blockers.length > 0);
|
|
326
|
-
if (!plan.projects.length) human(io, plan.kind === "upgrade" ? `no hub is running, so nothing is carried over: install with \`bun add -g @staix/agent-hub@${plan.version}\`, then \`ahub setup\`` : "this project's hub is not running: `ahub up` starts it");
|
|
326
|
+
if (!plan.projects.length) human(io, plan.kind === "upgrade" && plan.scope === undefined ? `no hub is running, so nothing is carried over: install with \`bun add -g @staix/agent-hub@${plan.version}\`, then \`ahub setup\`` : "this project's hub is not running: `ahub up` starts it");
|
|
327
327
|
const endable = plan.projects.flatMap((p) => p.source.peers.filter((peer) => ["tui", "headless"].includes(peerKind(p, peer))).map((peer) => ({ planned: p, peer })));
|
|
328
328
|
const keys = [...(blocked ? [] : ["[a] apply"]), "[r] refresh", ...(endable.length ? ["[k] end agents"] : []), "[j] plan as JSON", ...(plan.projects.length ? ["[x] reset a project's hub"] : []), "[q] quit"];
|
|
329
329
|
const preferred = "r"; // Enter never closes terminals, stops a hub, resumes or reattaches implicitly.
|