@staix/agent-hub 0.25.0 → 0.27.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 +17 -0
- package/README.md +12 -6
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +1 -1
- package/docs/agent-notes/daemon.md +7 -5
- package/docs/agent-notes/tasks.md +7 -6
- package/docs/agent-notes/tests.md +1 -0
- package/docs/dogfood.md +7 -1
- package/docs/operations.md +160 -17
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +18 -4
- package/docs/sdlc.md +3 -3
- package/docs/security.md +52 -9
- package/docs/specs/2026-09-19-agent-hub-design.md +1 -1
- package/docs/specs/2026-11-01-research-campaigns-design.md +21 -7
- package/docs/verification/2026-11-01-0.26.0.md +54 -0
- package/docs/verification/2026-11-01-0.27.0.md +49 -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 +190 -101
- 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 +9 -8
- package/src/cli/identity.ts +5 -5
- package/src/cli/main.ts +68 -24
- 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/study-output.ts +56 -0
- package/src/cli/study.ts +26 -3
- 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/action-policy.ts +5 -3
- 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 +103 -26
- package/src/hub/db-schema.ts +143 -0
- package/src/hub/delivery-journal.ts +3 -4
- package/src/hub/events.ts +2 -1
- package/src/hub/execution-budget.ts +3 -4
- package/src/hub/folder-trigger.ts +3 -4
- package/src/hub/hub-tools.ts +7 -5
- package/src/hub/invocations.ts +3 -4
- package/src/hub/lifecycle.ts +87 -29
- package/src/hub/manager.ts +49 -33
- package/src/hub/no-writes.ts +1 -1
- package/src/hub/orca-env.ts +19 -0
- package/src/hub/project-runtime-state.ts +164 -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/sdlc-release-authorization.ts +11 -5
- package/src/hub/session-identity.ts +1 -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/templates/presets/confirmation-matrix.json +5 -5
- package/templates/release-guard.block.md +2 -1
|
@@ -6,7 +6,7 @@ import type { StartForm } from "./registry-types.ts";
|
|
|
6
6
|
/** Only the control socket operations the peer owns; authentication remains in the daemon. */
|
|
7
7
|
export interface ChannelSocket {
|
|
8
8
|
readyState: number;
|
|
9
|
-
data: { launchId?: string };
|
|
9
|
+
data: { launchId?: string; protocol?: number };
|
|
10
10
|
send(data: string): unknown;
|
|
11
11
|
close(code?: number, reason?: string): void;
|
|
12
12
|
}
|
|
@@ -142,7 +142,9 @@ export class WsPeer extends BasePeer {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
recoveryMetadata(): Record<string, unknown> {
|
|
145
|
-
|
|
145
|
+
const protocol = this.attached ? this.sock?.data.protocol : undefined;
|
|
146
|
+
return { launch: definitionOf(this.id)?.attach?.recoveryLaunch?.(this.id) ?? { kind: "claude-channel", peer: this.id },
|
|
147
|
+
...(typeof protocol === "number" && Number.isSafeInteger(protocol) && protocol > 0 ? { controlProtocol: protocol } : {}) };
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
@@ -76,7 +76,7 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
76
76
|
projection: { roleRank: 0, helpRank: 0, plannerEligible: true },
|
|
77
77
|
api: 1, kind: "claude", displayName: "Claude Code", family: "claude", transport: "claude-channel",
|
|
78
78
|
roles: Object.freeze(["planner", "reviewer"]), defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
79
|
-
start: { forms: Object.freeze(["tui"] as const), defaultForm: "tui", launch: claudeNativeLaunch, version: claudeObservedVersion, ...CLAUDE_LAUNCH_POLICY, conductorStartable: true,
|
|
79
|
+
start: { forms: Object.freeze(["tui"] as const), defaultForm: "tui", launch: claudeNativeLaunch, version: claudeObservedVersion, ...CLAUDE_LAUNCH_POLICY, conductorStartable: true, upStart: { allowed: true, default: true },
|
|
80
80
|
unattended: true, hubSwitches: switches, ownedFlags: Object.freeze(["--dangerously-load-development-channels", "--dangerously-skip-permissions"]) },
|
|
81
81
|
identity: { mcp: "channel-plugin" }, instances: { mode: "external", roles: ["planner", "implementer", "verifier", "reviewer"] }, attach: { externalHello: true, recoveryLaunch: claudeRecoveryLaunch },
|
|
82
82
|
// Approval enforcement belongs to the verified native transport and host permission gate.
|
|
@@ -93,7 +93,7 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
93
93
|
projection: { roleRank: 1, startRank: 1, helpRank: 1, plannerEligible: true },
|
|
94
94
|
api: 1, kind: "codex", displayName: "Codex", family: "codex", transport: "codex-appserver",
|
|
95
95
|
roles: Object.freeze(["implementer"]), defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
96
|
-
start: { create: createCodex, daemonPort: "codex-appserver", forms: dualForms, defaultForm: "tui", launch: codexNativeLaunch, version: codexObservedVersion, ...CODEX_LAUNCH_POLICY, conductorStartable: true,
|
|
96
|
+
start: { create: createCodex, daemonPort: "codex-appserver", forms: dualForms, defaultForm: "tui", launch: codexNativeLaunch, version: codexObservedVersion, ...CODEX_LAUNCH_POLICY, conductorStartable: true, upStart: { allowed: true, default: true },
|
|
97
97
|
unattended: true, hubSwitches: switches, ownedFlags: Object.freeze(["--remote", "--remote-auth-token-env", "--yolo", "--dangerously-bypass-approvals-and-sandbox"]) },
|
|
98
98
|
identity: { mcp: "env" }, instances: { mode: "headless" },
|
|
99
99
|
permission: { trackUnattended: true, grants: { ask: "the session's own approval policy applies again", "ask-when-needed": "approval policy on-request on every turn", "never-ask": "approval policy never on every turn: nothing asks" } },
|
|
@@ -106,7 +106,7 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
106
106
|
projection: { roleRank: 2, startRank: 2, helpRank: 2, plannerEligible: true },
|
|
107
107
|
api: 1, kind: "kimi", displayName: "Kimi Code", family: "kimi", transport: "acp",
|
|
108
108
|
roles: implementerRoles, defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
109
|
-
start: { create: createKimi, acpCommand: kimiAcpCommand, explicitHeadlessMode: true, planCommand: () => "ahub kimi", daemonPort: "acp", forms: dualForms, defaultForm: "tui", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
109
|
+
start: { create: createKimi, acpCommand: kimiAcpCommand, explicitHeadlessMode: true, planCommand: () => "ahub kimi", daemonPort: "acp", forms: dualForms, defaultForm: "tui", conductorStartable: true, upStart: { allowed: true }, hubSwitches: [], ownedFlags: [] },
|
|
110
110
|
// ACP sessions carry headless identity; the existing CLI alone owns the TUI's project MCP entry.
|
|
111
111
|
identity: { mcp: "acp-session" }, instances: { mode: "headless" }, attach: { pullOnlyForms: Object.freeze(["tui"] as const), recoveryLaunch: kimiRecoveryLaunch,
|
|
112
112
|
pullOnlyNotice: "pull-only: messages wait for hub_inbox; no push, steer, watchdog or automatic quota handoff",
|
|
@@ -121,7 +121,7 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
121
121
|
projection: { roleRank: 4, startRank: 0, helpRank: 6, plannerEligible: false },
|
|
122
122
|
api: 1, kind: "pi", enabledSwitchStartOnly: true, displayName: "Pi", family: "pi", transport: "hub-native",
|
|
123
123
|
roles: implementerRoles, defaultOnRoster: false, trust: { onPrem: false, hubSandboxed: true },
|
|
124
|
-
start: { create: createPi, exclusiveStartRefusal: "Pi start is in progress; inspect status before retrying", refreshTools: true, explicitHeadlessMode: true, daemonPort: "pi-rpc", forms: dualForms, defaultForm: "tui", conductorStartable: true, autoStart: "pi.auto_start", conductorStartArgs: piConductorStartArgs, hubSwitches: [], ownedFlags: [] },
|
|
124
|
+
start: { create: createPi, exclusiveStartRefusal: "Pi start is in progress; inspect status before retrying", refreshTools: true, explicitHeadlessMode: true, daemonPort: "pi-rpc", forms: dualForms, defaultForm: "tui", conductorStartable: true, upStart: { allowed: true }, autoStart: "pi.auto_start", conductorStartArgs: piConductorStartArgs, hubSwitches: [], ownedFlags: [] },
|
|
125
125
|
identity: { mcp: "env" }, instances: { mode: "headless" },
|
|
126
126
|
// Approval enforcement belongs to the verified native transport and host permission gate.
|
|
127
127
|
permission: { grants: { ...hubGrants } },
|
|
@@ -134,7 +134,7 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
134
134
|
projection: { roleRank: 3, helpRank: 7, plannerEligible: false },
|
|
135
135
|
api: 1, kind: "local", displayName: "Local worker", family: "local", transport: "hub-native",
|
|
136
136
|
roles: implementerRoles, defaultOnRoster: true, trust: { onPrem: true, hubSandboxed: true },
|
|
137
|
-
start: { create: createLocal, reconfigureWhileAttached: true, refreshTools: true, daemonPort: "local-worker", forms: headlessForms, defaultForm: "headless", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
137
|
+
start: { create: createLocal, reconfigureWhileAttached: true, refreshTools: true, daemonPort: "local-worker", forms: headlessForms, defaultForm: "headless", conductorStartable: true, upStart: { allowed: true }, hubSwitches: [], ownedFlags: [] },
|
|
138
138
|
identity: { mcp: "env" }, instances: { mode: "headless" },
|
|
139
139
|
// Approval enforcement belongs to the verified native transport and host permission gate.
|
|
140
140
|
permission: { grants: { ...hubGrants } },
|
|
@@ -147,7 +147,7 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
147
147
|
projection: { roleRank: 6, startRank: 3, helpRank: 3, plannerEligible: true },
|
|
148
148
|
api: 1, kind: "jcode", displayName: "Jcode", family: "jcode", transport: "acp",
|
|
149
149
|
roles: implementerRoles, optional: true, defaultOnRoster: false, trust: { onPrem: false, hubSandboxed: false },
|
|
150
|
-
start: { create: createJcode, preflight: jcodeStartPreflight, daemonPort: "owned-acp", forms: dualForms, defaultForm: "tui", explicitHeadlessMode: true, conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
150
|
+
start: { create: createJcode, preflight: jcodeStartPreflight, daemonPort: "owned-acp", forms: dualForms, defaultForm: "tui", explicitHeadlessMode: true, conductorStartable: true, upStart: { allowed: true }, hubSwitches: [], ownedFlags: [] },
|
|
151
151
|
// #685: an extra Jcode is a headless instance with its own native home, socket and state (instances.<id>.jcode_home).
|
|
152
152
|
identity: { mcp: "project-file" }, instances: { mode: "headless", nativeHome: true },
|
|
153
153
|
permission: { unmanaged: "Jcode native policy is unmanaged; no hub mode mapping", grants: {
|
|
@@ -26,6 +26,8 @@ export const QWEN_DEFINITION: AdapterDefinition = {
|
|
|
26
26
|
forms: ["headless"],
|
|
27
27
|
defaultForm: "headless",
|
|
28
28
|
conductorStartable: true,
|
|
29
|
+
// #608: the TUI is explicit only, so `ahub up` never starts Qwen from the start set; the text names why.
|
|
30
|
+
upStart: { allowed: false, refusal: "the hub never opens its TUI, and a Qwen is started only by ahub qwen" },
|
|
29
31
|
hubSwitches: [],
|
|
30
32
|
ownedFlags: [],
|
|
31
33
|
// The TUI (#286 Part 3) is explicit only: forms stays headless, so Qwen never becomes a start-mode peer and the hub never opens it by itself.
|
|
@@ -188,6 +188,8 @@ export interface AdapterDefinition {
|
|
|
188
188
|
preflight?(ctx: StartRequest): string | undefined;
|
|
189
189
|
version?(ctx: VersionContext): string | undefined;
|
|
190
190
|
conductorStartable: boolean;
|
|
191
|
+
/** #608: whether `ahub up` may start this kind from the `start.agents` set, and whether it is in the default set. `allowed: false` carries the reason a listing is refused. Absent: not startable by `ahub up`, refused with the generic reason. Fixed builtins only. */
|
|
192
|
+
upStart?: { allowed: true; default?: true } | { allowed: false; refusal: string };
|
|
191
193
|
passthrough?: boolean;
|
|
192
194
|
autoStart?: "pi.auto_start";
|
|
193
195
|
exclusiveStartRefusal?: string;
|
package/src/adapters/registry.ts
CHANGED
|
@@ -247,6 +247,7 @@ export function assertNativeCapabilityScope(definition: AdapterDefinition): void
|
|
|
247
247
|
["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
|
|
248
248
|
["start.daemonPort qwen-dual", definition.start.daemonPort === "qwen-dual"],
|
|
249
249
|
["start.createTui", declared(definition.start, "createTui")],
|
|
250
|
+
["start.upStart", declared(definition.start, "upStart")], // #608: which kinds `ahub up` may start is a fixed-builtin decision, never a declared one
|
|
250
251
|
["routing", definition.routing !== undefined],
|
|
251
252
|
["projection", definition.projection !== undefined],
|
|
252
253
|
["instances.nativeHome", declared(definition.instances, "nativeHome")], // #685: a fixed-builtin assumption, one native data home per declared instance
|
|
@@ -9,14 +9,15 @@ import { readRestartSnapshot } from "../hub/restart.ts";
|
|
|
9
9
|
import { processLiveness } from "../pi/process-signature.ts";
|
|
10
10
|
import { packageDigest, recoveryTargetDigest } from "./recovery-package.ts";
|
|
11
11
|
import type { Project } from "../hub/registry.ts";
|
|
12
|
-
import { planFingerprint, registeredProjects, type RecoveryOperation, type UpgradePlan } from "./upgrade.ts";
|
|
12
|
+
import { planFingerprint, recoveryScope, registeredProjects, type RecoveryOperation, type UpgradePlan } from "./upgrade.ts";
|
|
13
13
|
import { realPath } from "../hub/project.ts";
|
|
14
14
|
|
|
15
15
|
function unavailable(): never { throw new RecoveryAuthorityError(); }
|
|
16
16
|
function validateConductorOperation(op: RecoveryOperation): void {
|
|
17
17
|
try {
|
|
18
18
|
const { fingerprint, ...body } = op.plan;
|
|
19
|
-
|
|
19
|
+
const scope = recoveryScope(op.plan);
|
|
20
|
+
if (!(op.schema === 1 || op.schema === 3 && scope !== undefined) || !["restart", "upgrade"].includes(op.plan.kind) || planFingerprint(body) !== fingerprint || !op.plan.projects.length
|
|
20
21
|
|| op.plan.projects.length > 128 || new Set(op.plan.projects.map(row => row.project.id)).size !== op.plan.projects.length) unavailable();
|
|
21
22
|
} catch { unavailable(); }
|
|
22
23
|
}
|
package/src/cli/help.ts
CHANGED
|
@@ -10,15 +10,15 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
10
10
|
["ahub --project <path|id> <command>", "select a repository or worktree explicitly"],
|
|
11
11
|
["ahub projects [--json] [--full] [--color=auto|always|never]", "list registered projects and live status"],
|
|
12
12
|
["ahub projects remove <id>", "forget a stopped registration (keeps project files)"],
|
|
13
|
-
["ahub setup [--yes]", "install or update the channel plugin and project-neutral hook in Claude configuration homes in use, preserving unrelated profile settings, register the acon console plugin (console/plugins/ahub.json in the hub home, read at acon's next start), then run doctor"],
|
|
14
|
-
["ahub init [--profile dev|research|writing|general|<pack>] [--shadow] [--release-guard] [--dry-run --json]", "preview or write .agenthub/config.json and the AGENTS.md marker block (drops a legacy CLAUDE.md block); a pack is a shipped one (writing-pack, analysis, ops) or a project file .agenthub/roles/<name>.pack.json; a new project inherits a trusted machine autonomy default into config.local.json"],
|
|
13
|
+
["ahub setup [--yes] [--agents <a,b>]", "install or update the channel plugin and project-neutral hook in Claude configuration homes in use, preserving unrelated profile settings, ask once which agents ahub up starts (--agents answers it, --yes keeps the current choice or claude,codex, no terminal keeps the default), register the acon console plugin (console/plugins/ahub.json in the hub home, read at acon's next start), then run doctor"],
|
|
14
|
+
["ahub init [--profile dev|research|writing|general|<pack>] [--shadow] [--release-guard] [--dry-run --json]", "preview or write .agenthub/config.json and the AGENTS.md marker block (drops a legacy CLAUDE.md block); a pack is a shipped one (writing-pack, analysis, ops) or a project file .agenthub/roles/<name>.pack.json; a new project inherits a trusted machine autonomy default and the agent set chosen at ahub setup (start.agents) into config.local.json"],
|
|
15
15
|
["ahub --live-test [--project <path|id>] <command> [args...]", "user-authorized operational console delegation; ordinary calls retain peer identity; person-authored content refuses"],
|
|
16
16
|
["ahub help [command]", "this help, or one command's entries; also --help and -h"],
|
|
17
17
|
["ahub version | --version", "print the installed version"],
|
|
18
18
|
["ahub completion zsh|bash|fish", "print a shell completion script; its candidates come from these help entries"],
|
|
19
19
|
]],
|
|
20
20
|
["Daemon and runtime", [
|
|
21
|
-
["ahub up [--unattended] [--no-console]", "start the daemon;
|
|
21
|
+
["ahub up [--unattended] [--no-console] [--no-agents]", "start the daemon, then ask it to start this project's default agents (start.agents, claude and codex unless chosen at setup; one line each, a refusal names the manual command; --no-agents skips them), then enter console in an interactive terminal"],
|
|
22
22
|
["ahub console [--panels] [--color=auto|always|never]", "enter the human console; P cycles built-in pet characters/off; exiting leaves the daemon running"],
|
|
23
23
|
["ahub console --no-pet", "turn off attention for this session; combines with --panels and --color"],
|
|
24
24
|
["ahub pet --window [--close]", "start or close the macOS floating window, from a person's terminal"],
|
|
@@ -27,7 +27,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
27
27
|
["ahub sdlc status [task] | explain <task> [--json] [--color=auto|always|never] | export <task> | record <task> <receipt-json>", "read head-bound lifecycle gates and the next actor/action; status and explain are open to any agent as a public view (private tasks stay stubs), export and record are person only; export stays JSON"],
|
|
28
28
|
["ahub sdlc queue <owner/repo> | release <owner/repo> [live-issue...] [--json] [--color=auto|always|never]", "read-only merge and release evidence; open to any agent as public readback of the checkout's repositories, never an authorization"],
|
|
29
29
|
["ahub sdlc merge <task> [--dry-run] [--json] [--color=auto|always|never]", "guarded squash merge of an approved task at its approved head after every gate is re-read; follows sdlc.toml auto_merge (off, dry-run, on); --dry-run only reports (person or conductor)"],
|
|
30
|
-
["ahub sdlc authorize-release <version> [--json] [--color=auto|always|never]", "approve this daemon checkout commit, version and fresh docs (person
|
|
30
|
+
["ahub sdlc authorize-release <version> [--json] [--color=auto|always|never]", "approve this daemon checkout commit, version and fresh docs (person or unique configured conductor on the person's explicit release instruction naming the release in its own session; hub/task/review/file/peer relays never qualify; restart revokes)"],
|
|
31
31
|
["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"],
|
|
32
32
|
["ahub sdlc release-guard-hook <remote-name> <remote-url>", "internal pre-push readback; validates version tags and forwards exact stdin on success"],
|
|
33
33
|
["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)"],
|
|
@@ -41,7 +41,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
41
41
|
["ahub status [--json] [--full] [--color=auto|always|never]", "the hub, its peers, model backends and task counts"],
|
|
42
42
|
["ahub status --all [--json] [--color=auto|always|never]", "show every registered project"],
|
|
43
43
|
["ahub logs [-f]", "person: last 100 hub.log lines (-f starts at 10); conductor: bounded public lines and follow while authority remains"],
|
|
44
|
-
["ahub doctor [--json] [--full] [--color=auto|always|never]", "check the tools, daemon, plugin, gateway, models, memory library and memory capture setup (read from `amem setup <tool> --json`, a preview) this project uses"],
|
|
44
|
+
["ahub doctor [--json] [--full] [--color=auto|always|never]", "check the tools, daemon, plugin, gateway, models, memory library and memory capture setup (read from `amem setup <tool> --json`, a preview) this project uses, and the runtime capture pruning ledger; warn when the CLI or plugin is older than its running daemon"],
|
|
45
45
|
["ahub doctor --stale-anchors [--json]", "read-only bounded report of public memory symbol anchors at current HEAD; private or unavailable evidence stays unknown"],
|
|
46
46
|
["ahub doctor --orphans [--kill] [--json] [--color=auto|always|never]", "person: inspect missing-root registrations; conductor --kill: selected project only; signed hub ownership is required before any stop"],
|
|
47
47
|
["ahub agents [--json]", "show each gated agent CLI's installed version and verdict (verified, in-line, trusted, refused or unknown)"],
|
|
@@ -50,8 +50,8 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
50
50
|
["ahub down", "stop this project's hub, the counterpart of ahub up (same as ahub kill)"],
|
|
51
51
|
["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"],
|
|
52
52
|
["ahub restart [--dry-run] [--yes] [--json] [--verbose] [--color=auto|always|never]", "recover this project's runtime; a configured conductor's --yes requires a same-project, source-issued continuation proof, otherwise run it as the person"],
|
|
53
|
-
["ahub upgrade [--to <version>] [--dry-run] [--yes] [--json] [--verbose] [--color=auto|always|never]", "review and upgrade
|
|
54
|
-
["ahub recovery [status|resume|abort] [<operation-id>] [--json] [--color=auto|always|never]", "inspect a preflight; without an id, resume/abort select
|
|
53
|
+
["ahub upgrade [--project <path|id>] [--to <version>] [--dry-run] [--yes] [--json] [--verbose] [--color=auto|always|never]", "review and upgrade this selected project (default: current directory; --project selects another); global CLI/plugin updates are separate; --to defaults to latest for interactive review or dry-run, while --yes requires --to <exact-version>; conductor apply needs the same project's supported proof, and a newer-coordinator handoff is person-only"],
|
|
54
|
+
["ahub recovery [status|resume|abort] [<operation-id>] [--json] [--color=auto|always|never]", "inspect a preflight; without an id, resume/abort select this project's hold or a retained legacy machine hold; a bare terminal opens the person's screen; only its initiating conductor may continue its own supported restart/upgrade with fresh proof"],
|
|
55
55
|
["ahub recovery reattach <operation-id> codex|claude|pi", "person only: repair that blocked peer with its recorded session; leaves the rest for recovery resume"],
|
|
56
56
|
["ahub recovery dispose [<operation-id>] --fresh-session codex|claude --reason <text>", "end a partial operation by starting that peer as a new session, recording the lost conversation"],
|
|
57
57
|
["ahub recovery dispose [<operation-id>] --stop-and-archive --reason <text>", "end a partial operation: stop its own targets, archive its snapshot and release the lock; the upgrade is abandoned, not completed"],
|
|
@@ -77,7 +77,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
77
77
|
["ahub autonomy on [--all-projects] [--yes] | off [--all-projects] | status [--json]", "person only: apply the autonomy preset and machine default with one confirmation, optionally to every registered project; off removes fixed preset keys (manual edits included); status reports modes, floor enforcement and Claude homes; settings undo restores a live-hub project write"],
|
|
78
78
|
["ahub settings [list] [--json] | get <key> [--json]", "the settings the dashboard can change, each with its value, the file it comes from and when it applies"],
|
|
79
79
|
["ahub settings set <key> <value|inherit> [--yes] [--preview] [--now] | undo | reload", "validated settings: conductor set is limited to exact live operational keys; undo/reload and other keys are person only; --preview saves nothing; conductor writes keep the person undo record"],
|
|
80
|
-
["ahub settings edit <config.local.json|sdlc.toml> (set <path> <json> | remove <path>)... [--reason <text>] [--preview] [--yes] | backups [--json]", "human only: structural config (instances, review, snapshots, approvals, context.gate, sdlc.toml mode and required_checks) validated by the hub's own loaders, previewed as a diff and written with a backup; a key that runs code or locates secrets needs its name typed, never --yes"],
|
|
80
|
+
["ahub settings edit <config.local.json|sdlc.toml> (set <path> <json> | remove <path>)... [--reason <text>] [--preview] [--yes] | backups [--json]", "human only: structural config (instances, review, snapshots, approvals, context.gate, research.runners, sdlc.toml mode and required_checks) validated by the hub's own loaders, previewed as a diff and written with a backup; a key that runs code or locates secrets needs its name typed, never --yes"],
|
|
81
81
|
["ahub pause|resume <peer>", "hold a peer's deliveries in its queue / release them"],
|
|
82
82
|
["ahub queue list [--peer <id>] [--json] [--full] [--color=auto|always|never]", "inspect durable deliveries; open to any agent as public queue metadata, held bodies stay with the person or conductor"],
|
|
83
83
|
["ahub queue show <delivery-id> [--json] [--color=auto|always|never]", "person or configured conductor: inspect one delivery and revision; agent output uses the public view"],
|
|
@@ -108,6 +108,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
108
108
|
["ahub learn list [--all] | accept <id> | reject <id> [--json] [--color=auto|always|never]", "person only, never a conductor or an agent shell: review a recurring-unmet-criterion candidate the hub filed (#514a); accept writes its one review checklist line, added to your current checklist, to .agenthub/config.local.json through the settings writer (this machine only, never a tracked file, ahub settings undo puts it back, applies after a restart) and closes the card; reject only closes it"],
|
|
109
109
|
["ahub study draft <parent> <contract.json> | revise <id> <revision> <contract.json> | approve <id> <revision> | close <id> <revision> --reason <text> | approve-protocol <hash> [--json] [--color=auto|always|never]", "person only: store a research campaign contract as a draft, revise it, approve it (the only step that creates the unit tasks and lets work start) or close it; approve-protocol binds approval to the exact protocol and evaluator hashes; a planner drafts through hub_study_draft and hub_study_revise and can never approve"],
|
|
110
110
|
["ahub study show [<id> | --after <id>] [--json] [--color=auto|always|never]", "read-only for anyone: list this project's research campaigns, 50 per page (the page names the --after cursor that continues it), or one campaign's coverage per unit, handoff state, budget support and why dispatch is held; a private campaign, task or source shows ids, counts and states only"],
|
|
111
|
+
["ahub study experiments (--campaign <id> [--after <n>] | --attempt <ra-id> [--events-after <n>] | --plan <hash> --baseline <ra-id> --variant <ra-id>) [--json] [--color=auto|always|never]", "read-only for anyone, the same read as hub_experiment_show: a campaign's recorded attempts (50 a page, --after continues), one attempt with its events, or one comparison under a recorded plan; each value names its provenance and a declared value is never shown as observed"],
|
|
111
112
|
["ahub deliverables [--task <id>] [--approved] [--check] [--json] [--color=auto|always|never]", "what each task delivered and a reviewer approved, by id, hash and size (a path only where the PII screen allows); --check measures the files now and marks one changed since review; an agent sees only the tasks hub_task_show shows it"],
|
|
112
113
|
["ahub task show <id> [--json] [--full] [--color=auto|always|never] | escalate <id>", "full task with history (the person sees PII text, an agent the public view) / hand it to the next peer in escalate_to (person or configured conductor)"],
|
|
113
114
|
["ahub task role <id> <role|none>", "the person, or an agent on a task it owns: change bounded catalog guidance on an open task; routing and authority are unchanged"],
|
package/src/cli/identity.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function liveTestRefusal(command: string, args: readonly string[], contex
|
|
|
23
23
|
|| (command === "backlog" && sub !== "list") || (command === "release" && !["list", "show"].includes(sub ?? ""))
|
|
24
24
|
|| (command === "memory" && sub !== "graph")
|
|
25
25
|
// #601: a campaign's draft, revise, approve and close are the person's verdicts; only `show` is a read. Without this an agent shell could run them as the person through `--live-test`.
|
|
26
|
-
|| (command === "study" && sub !== "show")
|
|
26
|
+
|| (command === "study" && sub !== "show" && sub !== "experiments")
|
|
27
27
|
|| (command === "recovery" && (sub === "reattach" || sub === "dispose" && context.repairedFresh === true))
|
|
28
28
|
|| (command === "board" && args[0] === "import") || (command === "sdlc" && (args[0] === "record" || args[0] === "accept" || args[0] === "accept-departure"))
|
|
29
29
|
|| (command === "bench" && args.find(arg => arg !== "--json") === "run") || command === "pet"
|
|
@@ -85,7 +85,7 @@ const peerReadForms: Readonly<Record<string, Readonly<Record<string, PeerReadFor
|
|
|
85
85
|
sdlc: { status: { positional: 1, displayFlags: sdlcReadDisplayFlags }, explain: { positional: 1, displayFlags: sdlcReadDisplayFlags }, queue: { positional: 1, minimum: 1, displayFlags: sdlcReadDisplayFlags }, release: { positional: 33, minimum: 1, displayFlags: sdlcReadDisplayFlags } },
|
|
86
86
|
release: { show: { positional: 1, minimum: 1, displayFlags: [] } },
|
|
87
87
|
// #601: reading a campaign is harmless for any agent (the hub stubs a private one); draft, revise, approve and close fall to `console`.
|
|
88
|
-
study: { show: { positional: 1, valueFlags: ["--after"], displayFlags: sdlcReadDisplayFlags } },
|
|
88
|
+
study: { show: { positional: 1, valueFlags: ["--after"], displayFlags: sdlcReadDisplayFlags }, experiments: { valueFlags: ["--campaign", "--attempt", "--after", "--events-after", "--plan", "--baseline", "--variant"], displayFlags: sdlcReadDisplayFlags } },
|
|
89
89
|
projects: { "": {} },
|
|
90
90
|
models: { status: {} },
|
|
91
91
|
recovery: { status: { positional: 1 } },
|
|
@@ -138,8 +138,8 @@ export function classifyPeerCommand(command: string, args: readonly string[], ca
|
|
|
138
138
|
if (conductorOperation(command,args)) return "conductor";
|
|
139
139
|
if (command === "stop") return "console";
|
|
140
140
|
if (command === "sdlc" && ["release-guard", "release-guard-hook"].includes(args[0] ?? "")) return "allowed";
|
|
141
|
-
// #786:
|
|
142
|
-
if (command === "sdlc" && args[0]
|
|
141
|
+
// #786, #1063: guarded merge and recording explicitly instructed release consent admit the person or unique configured conductor; the daemon checks the role.
|
|
142
|
+
if (command === "sdlc" && ["merge", "authorize-release"].includes(args[0] ?? "")) return "conductor";
|
|
143
143
|
if (command === "role") return ["list", "show"].includes(args[0] ?? "") ? "allowed" : "console";
|
|
144
144
|
if (command === "task") {
|
|
145
145
|
// #933: cancel, label and role act on the caller's own task only; the daemon refuses another peer's task by role (Tasks.cancel, task_catalog_role, task_label_check).
|
|
@@ -196,7 +196,7 @@ export function cliCommandLabel(command: string, args: readonly string[], catalo
|
|
|
196
196
|
if (command === "learn") return ["list", "accept", "reject"].includes(args.find(arg => arg !== "--json") ?? "") ? `learn ${args.find(arg => arg !== "--json")}` : "learn";
|
|
197
197
|
if (command === "source") return ["add", "list"].includes(args.find(arg => arg !== "--json") ?? "") ? `source ${args.find(arg => arg !== "--json")}` : "source";
|
|
198
198
|
// #601: a campaign id and a contract path are text the person chose, so the audit names the subcommand only.
|
|
199
|
-
if (command === "study") return ["draft", "revise", "approve", "close", "show"].includes(args[0] ?? "") ? `study ${args[0]}` : "study";
|
|
199
|
+
if (command === "study") return ["draft", "revise", "approve", "close", "show", "experiments"].includes(args[0] ?? "") ? `study ${args[0]}` : "study";
|
|
200
200
|
if (command === "release") return ["new", "list", "show", "plan", "activate", "freeze", "ship", "drop"].includes(args[0] ?? "") ? `release ${args[0]}` : "release";
|
|
201
201
|
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", "down", "reset", "doctor", "agents", "source", "broker", "schedule", "intake", "learn", "study"].includes(command)) return "unknown";
|
|
202
202
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench", "agents", "roster", "settings"].includes(command)) {
|
package/src/cli/main.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { assertConductorOperationLocal, authorizeConductorOperation, authorizeConductorLaunch, issueConductorOperation, preflightConductorRecovery, retireConductorOperation } from "./conductor-recovery.ts";
|
|
3
|
+
import { runtimePruneHealth } from "../hub/runtime-prune-health.ts";
|
|
3
4
|
import { conductorLocalOperation, conductorOperation, OPERATIONAL_PERSON_GUIDANCE } from "../hub/operational-authority.ts";
|
|
4
5
|
import { sayRecipients } from "./say-input.ts";
|
|
5
6
|
import type { DeliverableRow } from "../hub/tasks.ts";
|
|
@@ -29,7 +30,7 @@ import { renderIntake, renderIntakeChange } from "./intake-output.ts";
|
|
|
29
30
|
import { renderLearnChange, renderLearnList } from "./learn-output.ts";
|
|
30
31
|
import { renderCapabilities } from "./capabilities-output.ts";
|
|
31
32
|
import { parseStudyArgs } from "./study.ts";
|
|
32
|
-
import { renderStudyList, renderStudyReceipt, renderStudyShow } from "./study-output.ts";
|
|
33
|
+
import { renderExperiments, renderStudyList, renderStudyReceipt, renderStudyShow } from "./study-output.ts";
|
|
33
34
|
import { renderHandoffReceipt, renderHandoffs } from "./handoff-output.ts";
|
|
34
35
|
import { parseHandoffArgs } from "./handoff-args.ts";
|
|
35
36
|
import { terminalText } from "./console-state.ts";
|
|
@@ -84,11 +85,13 @@ import { childEnv, personEnv } from "../hub/child-process.ts";
|
|
|
84
85
|
import { abortRecovery, createOperation, disposeRecovery, recordRecoveryDelegation, repairedFreshSession, reattachRecovery, liveProjects, nextChoices, nextActionsText, publicOperation, recoveryCommand, registeredProjects, runRecovery, type RecoveryOperation, type Inspection } from "./upgrade.ts";
|
|
85
86
|
import { endPlannedPeer, makeRecoveryDriver, makeUpgradePlan, preserveSource } from "./upgrade-runtime.ts";
|
|
86
87
|
import { exactVersion, latestRelease, newerVersion } from "./recovery-package.ts";
|
|
88
|
+
import { runtimeVersionWarnings } from "./runtime-version-warning.ts";
|
|
87
89
|
import { operationScreen, planScreen, type ScreenIO, type UpgradeHost } from "./upgrade-interactive.ts";
|
|
88
90
|
import { readRecoveryPresentationFlags } from "./upgrade-presentation-flags.ts";
|
|
89
91
|
import { renderOperation, renderBlocked, renderSummary, type RecoveryPresentationOptions } from "./upgrade-presentation.ts";
|
|
90
92
|
import { renderBudgetResult, renderRecoveryStatus, renderUpgradePlan } from "./upgrade-output.ts";
|
|
91
93
|
import { recordTerminalLaunch, prepareTerminalLaunch, registerTerminalLaunch, type TerminalPeer } from "./terminal-recovery.ts";
|
|
94
|
+
import { effectiveStartAgents, inheritMachineStart, setupStartAgents, startProjectAgents } from "./start-agents-run.ts";
|
|
92
95
|
import { ensureMlx, inspectMlx, stopMlx } from "../models/mlx.ts";
|
|
93
96
|
import { setupOllamaModel } from "./models-setup.ts";
|
|
94
97
|
|
|
@@ -142,6 +145,16 @@ while (argv[0] === "--project" || argv[0] === "--live-test") {
|
|
|
142
145
|
if (!selector || selector.startsWith("--")) fail("--project needs a path or project ID");
|
|
143
146
|
}
|
|
144
147
|
}
|
|
148
|
+
// Upgrade accepts its documented command-local selector as well as the leading global form.
|
|
149
|
+
if (argv[0] === "upgrade") {
|
|
150
|
+
const at = argv.indexOf("--project", 1);
|
|
151
|
+
if (at >= 0) {
|
|
152
|
+
if (selector !== undefined || argv.indexOf("--project", at + 1) >= 0) fail("--project may appear only once");
|
|
153
|
+
selector = argv[at + 1];
|
|
154
|
+
if (!selector || selector.startsWith("--")) fail("--project needs a path or project ID");
|
|
155
|
+
argv.splice(at, 2);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
145
158
|
const [given = "help", ...rest] = argv;
|
|
146
159
|
let selected: { root: string; stateDir: string };
|
|
147
160
|
try {
|
|
@@ -385,10 +398,10 @@ function matchingProject(): Project | undefined {
|
|
|
385
398
|
finally { registry.close(); }
|
|
386
399
|
}
|
|
387
400
|
const hubManifest = () => !!readControl(stateDir) || existsSync(join(stateDir, "hub.pid"));
|
|
388
|
-
/** A reset is never part of an upgrade or recovery:
|
|
401
|
+
/** A reset is never part of an upgrade or recovery: this project's or a legacy machine hold refuses it, whatever
|
|
389
402
|
* AGENTHUB_RECOVERY_OPERATION says (assertLifecycleAvailable lets that operation's own processes through). */
|
|
390
403
|
function resetLockFree(): void {
|
|
391
|
-
const owner = recoveryLock();
|
|
404
|
+
const owner = recoveryLock(undefined, cwd);
|
|
392
405
|
if (owner) throw new Error(`${activeOperation(owner)}; nothing was changed`);
|
|
393
406
|
}
|
|
394
407
|
/** A manifest whose daemon is alive or uncertain, by inspectProject's rule; a crashed daemon's leftovers do not count. */
|
|
@@ -680,11 +693,11 @@ async function onTerminal(screens: (io: ScreenIO) => Promise<void>, presentation
|
|
|
680
693
|
}
|
|
681
694
|
|
|
682
695
|
function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost {
|
|
683
|
-
const scope =
|
|
696
|
+
const scope = cwd;
|
|
684
697
|
return {
|
|
685
698
|
plan: () => makeUpgradePlan(kind, version, scope),
|
|
686
699
|
apply: async (plan, interrupted) => {
|
|
687
|
-
assertLifecycleAvailable();
|
|
700
|
+
assertLifecycleAvailable(cwd);
|
|
688
701
|
const current = await makeUpgradePlan(kind, plan.version, scope);
|
|
689
702
|
if (current.fingerprint !== plan.fingerprint) throw new Error("the plan changed during review; review it again");
|
|
690
703
|
// The check above takes seconds: a Ctrl+C in that time is still a no.
|
|
@@ -693,7 +706,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
693
706
|
spawnRecovery(operation, true);
|
|
694
707
|
return operation;
|
|
695
708
|
},
|
|
696
|
-
lock: recoveryLock,
|
|
709
|
+
lock: (projectRoot = cwd) => recoveryLock(undefined, projectRoot),
|
|
697
710
|
read: (id) => readOperation<RecoveryOperation>(id),
|
|
698
711
|
runner: recoveryRunner,
|
|
699
712
|
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect),
|
|
@@ -709,8 +722,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
709
722
|
async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
710
723
|
const display = recoveryDisplay();
|
|
711
724
|
const { one, rest } = takeFlags(display.args, ["--to"], []);
|
|
712
|
-
if (rest.some((a) => !["--dry-run", "--yes"].includes(a)) || (kind === "restart" && one["--to"])) fail("usage: ahub upgrade [--to <version>] [--dry-run] [--yes] | ahub restart [--dry-run] [--yes]");
|
|
713
|
-
if (kind === "upgrade" && selector) fail("upgrade changes the shared package/plugin; omit --project to review all affected running projects");
|
|
725
|
+
if (rest.some((a) => !["--dry-run", "--yes"].includes(a)) || (kind === "restart" && one["--to"])) fail("usage: ahub upgrade [--project <path|id>] [--to <version>] [--dry-run] [--yes] | ahub restart [--dry-run] [--yes]");
|
|
714
726
|
const yes = rest.includes("--yes"), dry = rest.includes("--dry-run") || (display.json && !yes);
|
|
715
727
|
// #272: on a terminal, with neither flag, the plan is reviewed, applied and followed in one session.
|
|
716
728
|
const interactive = !dry && !yes && humanRecovery(display.json);
|
|
@@ -725,7 +737,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
725
737
|
// An older CLI must not coordinate a newer target (docs/operations.md): the target's own coordinator takes over.
|
|
726
738
|
if (conductorPeer && kind === "upgrade" && newerVersion(to!, VERSION)) fail("a supporting target coordinator is required; the person runs it in ahub console or a terminal");
|
|
727
739
|
if (kind === "upgrade" && newerVersion(to!, VERSION) && process.env.AGENTHUB_UPGRADE_HANDOVER !== "1") {
|
|
728
|
-
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", ...liveTestArgs, "upgrade", "--to", to!, ...rest, ...display.flags, ...(dry && !rest.includes("--dry-run") ? ["--dry-run"] : [])];
|
|
740
|
+
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", "--project", cwd, ...liveTestArgs, "upgrade", "--to", to!, ...rest, ...display.flags, ...(dry && !rest.includes("--dry-run") ? ["--dry-run"] : [])];
|
|
729
741
|
console.error(`ahub: ${to} is newer than this CLI (${VERSION}), so its own coordinator runs the upgrade: bun ${argv.join(" ")}`);
|
|
730
742
|
if (interactive) {
|
|
731
743
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -743,7 +755,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
743
755
|
}
|
|
744
756
|
if (kind === "upgrade") console.error("ahub: Kimi now defaults to its own TUI (pull-only) in projects without peers.kimi.start_mode. To keep ACP: ahub settings set peers.kimi.start_mode headless");
|
|
745
757
|
if (interactive) return onTerminal((io) => planScreen(upgradeHost(kind, to ?? VERSION), io), display.options);
|
|
746
|
-
const plan = await makeUpgradePlan(kind, to ?? VERSION,
|
|
758
|
+
const plan = await makeUpgradePlan(kind, to ?? VERSION, cwd, undefined, conductorPeer);
|
|
747
759
|
if (conductorPeer) await preflightConductorRecovery(plan, conductorPeer);
|
|
748
760
|
const jsonApply = display.json && yes && !dry;
|
|
749
761
|
// #814: a person reading a pipe or a dry run gets the plan screen's rows, with every blocker, reconnect-only session and
|
|
@@ -757,7 +769,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
757
769
|
if (display.json) for (const blocker of blockers) console.error(`ahub: blocker: ${blocker}`);
|
|
758
770
|
if (dry) return;
|
|
759
771
|
if (blockers.length) { if (jsonApply) console.log(JSON.stringify(plan, null, 2)); fail(conductorPeer ? "conductor recovery cannot prove this plan; no runtime was changed; the person runs it in ahub console or a terminal" : "plan has blockers; no runtime was changed"); }
|
|
760
|
-
assertLifecycleAvailable();
|
|
772
|
+
assertLifecycleAvailable(cwd);
|
|
761
773
|
if (!yes) {
|
|
762
774
|
if (!process.stdin.isTTY) fail("review --dry-run and use --yes in non-interactive sessions");
|
|
763
775
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -765,14 +777,14 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
765
777
|
if (!/^(y|yes)$/i.test((await rl.question("Apply this project list and restore its sessions? [y/N] ")).trim())) return;
|
|
766
778
|
} finally { rl.close(); }
|
|
767
779
|
}
|
|
768
|
-
const current = await makeUpgradePlan(kind, plan.version,
|
|
780
|
+
const current = await makeUpgradePlan(kind, plan.version, cwd, undefined, conductorPeer);
|
|
769
781
|
if (current.fingerprint !== plan.fingerprint) fail("plan changed during review; run the command again");
|
|
770
782
|
const routingDefaults = conductorPeer ? await preflightConductorRecovery(plan, conductorPeer) : undefined;
|
|
771
783
|
const sourceRoot = preserveSource(plan);
|
|
772
784
|
let operation: RecoveryOperation;
|
|
773
785
|
if (conductorPeer) {
|
|
774
786
|
const id = randomUUID();
|
|
775
|
-
const pending: RecoveryOperation = { schema:
|
|
787
|
+
const pending: RecoveryOperation = { schema: 3, id, plan, sourceRoot, createdAt: Date.now(), updatedAt: Date.now(), phase: "pending", step: "stage", ...(routingDefaults ? { conductorRoutingDefaults: routingDefaults } : {}), projects: plan.projects.map(row => ({ id: row.project.id, phase: "pending", terminals: {} })) };
|
|
776
788
|
const target = await makeRecoveryDriver().stage(pending);
|
|
777
789
|
pending.targetRoot = target.root; pending.targetDigest = target.digest; pending.targetFeatures = target.targetFeatures;
|
|
778
790
|
await issueConductorOperation(pending, conductorPeer);
|
|
@@ -868,9 +880,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
868
880
|
printRecovery(renderBlocked(result, nextChoices(result, undefined, live), display.options));
|
|
869
881
|
return;
|
|
870
882
|
}
|
|
871
|
-
// #272: without an id,
|
|
883
|
+
// #272: without an id, this project's hold (or a retained legacy machine hold); bare on a terminal, its screen.
|
|
872
884
|
const named = !!given && operationIdPattern.test(given);
|
|
873
|
-
const id = named ? given : recoveryLock();
|
|
885
|
+
const id = named ? given : recoveryLock(undefined, cwd);
|
|
874
886
|
const rest = named || given === undefined ? more : [given, ...more];
|
|
875
887
|
if (action === undefined && humanRecovery(display.json)) {
|
|
876
888
|
if (!id) return console.log("no recovery operation is open");
|
|
@@ -1009,26 +1021,38 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1009
1021
|
},
|
|
1010
1022
|
|
|
1011
1023
|
manager: async () => {
|
|
1012
|
-
|
|
1024
|
+
// #608: the dashboard's "start" counts as the person's `ahub up`: the manager starts the project's default agent set after the hub is ready.
|
|
1025
|
+
const manager = await startManager({ lifecycle, afterStart: async (project) => {
|
|
1026
|
+
const chosen = effectiveStartAgents(project.root, loadConfig(project.root).start.agents, hubHome(), [project.root]);
|
|
1027
|
+
return startProjectAgents(project, chosen.agents);
|
|
1028
|
+
} });
|
|
1013
1029
|
for (const signal of ["SIGINT", "SIGTERM"] as const) process.on(signal, () => void manager.stop());
|
|
1014
1030
|
await manager.stopped;
|
|
1015
1031
|
},
|
|
1016
1032
|
|
|
1017
1033
|
setup: async () => {
|
|
1018
1034
|
recoveryLauncherCurrent?.();
|
|
1019
|
-
assertLifecycleAvailable();
|
|
1035
|
+
assertLifecycleAvailable(cwd);
|
|
1020
1036
|
const root = join(import.meta.dir, "..", "..");
|
|
1037
|
+
const setupFlags = takeFlags(args.filter(arg => arg !== "--yes"), ["--agents"], []);
|
|
1038
|
+
if (setupFlags.rest.length) fail("usage: ahub setup [--yes] [--agents <a,b>]");
|
|
1021
1039
|
const registry = new Registry(), rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined;
|
|
1022
1040
|
try {
|
|
1023
1041
|
const found = recoveryLauncherCurrent || delegatedPeer ? await discoverClaudeHomes({ home: homedir(), env: process.env }) : await usedClaudeHomes(registry, homedir(), process.env);
|
|
1024
1042
|
for (const notice of found.notices) console.log(terminalText(notice));
|
|
1025
1043
|
await setupClaudeHomes({ homes: found.homes, env: process.env, packageRoot: root, yes: args.includes("--yes"),
|
|
1026
1044
|
interactive: !!rl,
|
|
1027
|
-
current: () => { recoveryLauncherCurrent?.(); assertLifecycleAvailable(); },
|
|
1045
|
+
current: () => { recoveryLauncherCurrent?.(); assertLifecycleAvailable(cwd); },
|
|
1028
1046
|
print: line => console.log(terminalText(line)), ask: async prompt => rl ? rl.question(prompt) : null,
|
|
1029
1047
|
probe: (argv, options) => { recoveryLauncherCurrent?.(); const result = spawnSync(argv[0]!, argv.slice(1), { encoding: "utf8", env: options.env, timeout: options.timeoutMs, maxBuffer: 1024 * 1024 }); recoveryLauncherCurrent?.(); return { status: result.status ?? -1, stdout: result.stdout ?? "" }; },
|
|
1030
1048
|
run: async (argv, env) => { recoveryLauncherCurrent?.(); const result = spawnSync(argv[0]!, argv.slice(1), { stdio: "inherit", env, timeout: 30_000 }); recoveryLauncherCurrent?.(); return result.status ?? -1; },
|
|
1031
1049
|
});
|
|
1050
|
+
// #608: the one question about the default agent set, after the Claude homes and before doctor.
|
|
1051
|
+
if (!recoveryLauncherCurrent && !delegatedPeer) {
|
|
1052
|
+
const refused = await setupStartAgents({ home: hubHome(), roots: [...registry.list().map(row => row.root), cwd], ...(setupFlags.one["--agents"] !== undefined ? { flag: setupFlags.one["--agents"] } : {}), yes: args.includes("--yes"),
|
|
1053
|
+
...(rl ? { ask: async (prompt: string) => { try { return await rl.question(prompt); } catch { return null; } } } : {}), print: line => console.log(terminalText(line)) });
|
|
1054
|
+
if (refused) fail(refused);
|
|
1055
|
+
}
|
|
1032
1056
|
} finally { registry.close(); rl?.close(); }
|
|
1033
1057
|
// #935: register the acon console plugin (acon reads it at its next start; no acon install is needed for this).
|
|
1034
1058
|
const acon = registerAconPlugin(hubHome(), join(root, "src", "ui", "plugin.ts"));
|
|
@@ -1050,7 +1074,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1050
1074
|
if (defaults.notice) console.error(defaults.notice);
|
|
1051
1075
|
return console.log(args.includes("--json") ? JSON.stringify(plan, null, 2) : plan.map(change => `${change.action} ${change.path}: ${change.reason}${change.managedBlock ? ` (managed block: ${change.managedBlock})` : ""}`).join("\n") || "already up to date");
|
|
1052
1076
|
}
|
|
1053
|
-
assertLifecycleAvailable();
|
|
1077
|
+
assertLifecycleAvailable(cwd);
|
|
1054
1078
|
if (newProject && delegatedPeer && readAutonomyDefaults(hubHome(), [cwd]).enabled) fail("machine autonomy inheritance is person-only; delegated live tests cannot apply it");
|
|
1055
1079
|
const changed = init(cwd, requestedProfile, args.includes("--shadow"), args.includes("--release-guard"));
|
|
1056
1080
|
const project = registeredProject();
|
|
@@ -1059,6 +1083,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1059
1083
|
const registry = new Registry();
|
|
1060
1084
|
try { for (const line of await inheritMachineAutonomy(project, hubHome(), autonomyEnvironment(process.env, !!process.stdin.isTTY), registry)) console.log(terminalText(line)); }
|
|
1061
1085
|
finally { registry.close(); }
|
|
1086
|
+
// #608: the machine's default agent set, copied the same way (person-only: a delegated live test never inherits it).
|
|
1087
|
+
if (!delegatedPeer) { const roots = (() => { const r = new Registry(); try { return r.list().map(row => row.root); } finally { r.close(); } })(); for (const line of inheritMachineStart(cwd, hubHome(), [...roots, cwd])) console.log(terminalText(line)); }
|
|
1062
1088
|
}
|
|
1063
1089
|
const config = projectConfig(), examples = resolveProfile(config.profile, config.profile_overrides).example_checks;
|
|
1064
1090
|
if (examples.length) console.log(terminalText("Example checks (advisory):\n" + examples.map(example => "- " + example).join("\n")));
|
|
@@ -1086,12 +1112,20 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1086
1112
|
if (recoveryLauncherCurrent) { recoveryLauncherCurrent(); return; }
|
|
1087
1113
|
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
1088
1114
|
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
1115
|
+
// #608: the default agent set, only for a person's own `ahub up` (never an agent's, a live test's, or an upgrade or recovery relaunch, which set AGENTHUB_RECOVERY_OPERATION).
|
|
1116
|
+
if (!args.includes("--no-agents") && !liveTest && caller.role === "console" && identity.role === "console" && !process.env.AGENTHUB_RECOVERY_OPERATION) {
|
|
1117
|
+
try {
|
|
1118
|
+
const chosen = effectiveStartAgents(cwd, loadConfig(cwd).start.agents, hubHome(), [cwd]);
|
|
1119
|
+
if (chosen.notice) console.error(chosen.notice);
|
|
1120
|
+
for (const line of await startProjectAgents({ root: cwd, stateDir }, chosen.agents)) console.log(terminalText(line));
|
|
1121
|
+
} catch (error) { console.error(`default agents were not started: ${terminalText((error as Error).message)}`); }
|
|
1122
|
+
}
|
|
1089
1123
|
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir, liveTest });
|
|
1090
1124
|
},
|
|
1091
1125
|
|
|
1092
1126
|
...Object.fromEntries(BUILTIN_CLI_ENTRIES.map(entry => [entry.command, async () => {
|
|
1093
1127
|
// Preserve the original guard before native effects; previews remain read-only and precede this guard.
|
|
1094
|
-
if (entry.lifecycleAdmission && !args.includes("--print-command") && !args.includes("--dry-run")) assertLifecycleAvailable();
|
|
1128
|
+
if (entry.lifecycleAdmission && !args.includes("--print-command") && !args.includes("--dry-run")) assertLifecycleAvailable(cwd);
|
|
1095
1129
|
const nativeConnect = async () => {
|
|
1096
1130
|
const hub = await connectLaunch();
|
|
1097
1131
|
try { if (selectedInstance) { requirePeerInstances(hub.features); requireActiveInstance((await hub.request({ t: "status" })).status, selectedInstance); } return hub; }
|
|
@@ -1491,6 +1525,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1491
1525
|
catch { return fail("cannot read the contract file"); }
|
|
1492
1526
|
try { return JSON.parse(text); } catch { return fail("the contract file is not valid JSON"); }
|
|
1493
1527
|
};
|
|
1528
|
+
if (parsed.op === "experiments") {
|
|
1529
|
+
// A read of the records Tasks keeps (hub_experiment_show): it runs no command and reads no file, and the hub blanks a private record itself.
|
|
1530
|
+
const args = parsed.attempt ? { attemptId: parsed.attempt, ...(parsed.eventsAfter === undefined ? {} : { eventAfter: parsed.eventsAfter }) } : parsed.plan ? { planHash: parsed.plan, baselineAttemptId: parsed.baseline, variantAttemptId: parsed.variant } : { campaignId: parsed.campaign, ...(parsed.after === undefined ? {} : { after: parsed.after }) };
|
|
1531
|
+
return showToolText(data => renderExperiments(data, present.columns), await taskOp("hub_experiment_show", args), present);
|
|
1532
|
+
}
|
|
1494
1533
|
if (parsed.op === "show") return showToolText(data => parsed.id ? renderStudyShow(data, present.columns) : renderStudyList(data, present.columns), await taskOp("hub_study_show", parsed.id ? { id: parsed.id } : parsed.after ? { after: parsed.after } : {}), present);
|
|
1495
1534
|
const request = parsed.op === "draft" ? { parentTaskId: parsed.parentTaskId, contract: contract(parsed.file) }
|
|
1496
1535
|
: parsed.op === "revise" ? { id: parsed.id, revision: parsed.revision, contract: contract(parsed.file) }
|
|
@@ -1959,7 +1998,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1959
1998
|
try {
|
|
1960
1999
|
const environment = autonomyEnvironment(process.env, !!process.stdin.isTTY), home = hubHome();
|
|
1961
2000
|
if (args.includes("--all-projects")) {
|
|
1962
|
-
assertLifecycleAvailable();
|
|
2001
|
+
assertLifecycleAvailable(cwd);
|
|
2002
|
+
for (const project of registry.list()) assertLifecycleAvailable(project.root);
|
|
1963
2003
|
await runAllProjectAutonomy(args, { home, environment, projects: registry.list(),
|
|
1964
2004
|
prepare: (project, enabled) => prepareAutonomyProject(project, environment, enabled, registry),
|
|
1965
2005
|
ask: async prompt => rl ? rl.question(prompt) : null, print: line => console.log(terminalText(line)),
|
|
@@ -1994,7 +2034,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1994
2034
|
contextLines: [...(defaults.notice ? [defaults.notice] : []), "On/off also changes this machine's autonomy default for new projects; --all-projects applies the change to every registered project."],
|
|
1995
2035
|
confirmPrompt: "Apply this project's preset and this machine's autonomy default? [y/N] ",
|
|
1996
2036
|
beforeApply: action => {
|
|
1997
|
-
assertLifecycleAvailable();
|
|
2037
|
+
assertLifecycleAvailable(cwd);
|
|
1998
2038
|
if (action === "off" && defaults.fingerprint === "untrusted") console.log("Unsafe machine defaults are already ignored; their file was left unchanged.");
|
|
1999
2039
|
else writeAutonomyDefaults(home, action === "on", defaults.fingerprint, roots);
|
|
2000
2040
|
console.log(`Machine autonomy default: ${action}.`);
|
|
@@ -2550,7 +2590,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2550
2590
|
throw new Error(holder?.instanceId?.startsWith("reset-") ? `another ahub reset of this project is running (pid ${holder.pid}); nothing was reset` : "a hub started after the stop; nothing was reset, run ahub reset again");
|
|
2551
2591
|
}
|
|
2552
2592
|
if (liveManifest()) throw new Error("a hub started after the stop; nothing was reset, run ahub reset again");
|
|
2553
|
-
//
|
|
2593
|
+
// A recovery that took this project's hold after the first check must not run beside its reset.
|
|
2554
2594
|
resetLockFree();
|
|
2555
2595
|
if (all) {
|
|
2556
2596
|
let archived: string;
|
|
@@ -2635,6 +2675,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2635
2675
|
const row = (ok: boolean | undefined, name: string, detail: string) => { checks.push(nativeDoctorCheck(ok, name, detail)); };
|
|
2636
2676
|
|
|
2637
2677
|
try {
|
|
2678
|
+
for (const check of runtimePruneHealth()) row(check.ok, check.name, check.detail);
|
|
2638
2679
|
const catalog = loadRoleCatalog(cwd);
|
|
2639
2680
|
for (const error of catalog.errors) row(false, "catalog role", `${error.id}: ${error.reason}`);
|
|
2640
2681
|
try {
|
|
@@ -2646,11 +2687,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2646
2687
|
row(!!v, bin, v ?? "not found on PATH");
|
|
2647
2688
|
}
|
|
2648
2689
|
const up = await healthy();
|
|
2690
|
+
let daemonVersion: unknown;
|
|
2649
2691
|
row(up, "ahub daemon", up ? readControl(stateDir)!.url : "not running (ahub up)");
|
|
2650
2692
|
if (up) {
|
|
2651
2693
|
const hub = await connect();
|
|
2652
2694
|
try {
|
|
2653
2695
|
const { status } = await hub.request({ t: "status" });
|
|
2696
|
+
daemonVersion = status.version; // Authenticated reply, never the unauthenticated disk manifest.
|
|
2654
2697
|
const pending = Object.values(status.peers as Record<string, PeerRow>).reduce((n, peer) => n + (peer.needsReview ?? 0), 0);
|
|
2655
2698
|
row(!status.deliveryError && pending === 0, "delivery recovery", status.deliveryError ?? (pending ? `${pending} deliveries need review; run ahub queue list` : "journal healthy; no deliveries need review"));
|
|
2656
2699
|
// #285 AC10: where Kimi's number comes from, and whether the hub can trust it right now. An older hub answers without `kimiSource`: no row, not a failure.
|
|
@@ -2666,6 +2709,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2666
2709
|
}
|
|
2667
2710
|
const plugin = pluginState(parseList<InstalledPlugin>(spawnSync(DOCTOR_PLUGIN_PROBE.command, [...DOCTOR_PLUGIN_PROBE.argv], { encoding: "utf8" }).stdout ?? ""), join(import.meta.dir, "..", ".."));
|
|
2668
2711
|
row(plugin.state === "current", DOCTOR_PLUGIN_PROBE.name, plugin.state === "missing" ? "missing: run ahub setup" : plugin.state === "current" ? `agent-hub@agent-hub ${plugin.version}` : `agent-hub@agent-hub is stale (${plugin.why}): run ahub setup`);
|
|
2712
|
+
if (up) checks.push(...runtimeVersionWarnings(daemonVersion, VERSION, plugin.state === "missing" ? undefined : plugin.version));
|
|
2669
2713
|
|
|
2670
2714
|
const config = loadConfig(cwd);
|
|
2671
2715
|
row(!config.ignored, "config", config.ignored ? `${config.ignored.join("; ")} (move them to .agenthub/config.local.json)` : "no machine-local field ignored");
|
|
@@ -2862,7 +2906,7 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2862
2906
|
}
|
|
2863
2907
|
const runConfigured = configuredCommand ? async () => {
|
|
2864
2908
|
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances, peerConfig.declaredExternalInstances);
|
|
2865
|
-
if (!selection.preview) assertLifecycleAvailable();
|
|
2909
|
+
if (!selection.preview) assertLifecycleAvailable(cwd);
|
|
2866
2910
|
await runConfiguredAcpEntry({ kind: cmd, raw: args, catalog: projectCatalog, instances: declaredInstances, declaredExternalInstances: peerConfig.declaredExternalInstances, projectRoot: cwd, connect: connectLaunch, print: text => console.log(text), operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
2867
2911
|
} : undefined;
|
|
2868
2912
|
const run = identity.role === "tools" && commandAccess === "conductor" ? runConductorCommand : (Object.hasOwn(commands, cmd) ? commands[cmd] : undefined) ?? runConfigured ?? (() => fail(`unknown command "${cmd}"; run ahub help`));
|