@staix/agent-hub 0.24.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 +23 -0
- package/README.md +35 -8
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +2 -1
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +18 -3
- package/docs/agent-notes/tasks.md +12 -4
- package/docs/agent-notes/tests.md +3 -1
- package/docs/dogfood.md +16 -2
- package/docs/memory.md +1 -1
- package/docs/operations.md +264 -14
- package/docs/profiles.md +2 -0
- package/docs/quickstart.md +23 -6
- package/docs/security.md +198 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +25 -1
- package/docs/specs/2026-09-19-multi-project-design.md +12 -1
- package/docs/specs/2026-11-01-research-campaigns-design.md +354 -0
- package/docs/verification/2026-11-01-0.25.0.md +91 -0
- package/docs/verification/2026-11-01-0.26.0.md +54 -0
- package/docs/verified.json +45 -17
- package/package.json +4 -3
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +5294 -4797
- package/src/adapters/channel-peer.ts +4 -2
- package/src/adapters/claude-channel.ts +29 -6
- package/src/adapters/definitions/builtin-metadata.ts +6 -6
- package/src/adapters/definitions/native-cli-doctor.ts +30 -0
- package/src/adapters/definitions/qwen.ts +2 -0
- package/src/adapters/jcode.ts +3 -0
- package/src/adapters/registry-types.ts +2 -0
- package/src/adapters/registry.ts +2 -1
- package/src/cli/acon-register.ts +29 -0
- package/src/cli/conductor-recovery.ts +3 -2
- package/src/cli/handoff-args.ts +27 -0
- package/src/cli/handoff-output.ts +25 -0
- package/src/cli/help.ts +11 -8
- package/src/cli/identity.ts +7 -1
- package/src/cli/main.ts +138 -30
- package/src/cli/output.ts +8 -5
- 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/schedule-output.ts +8 -4
- package/src/cli/start-agents-run.ts +125 -0
- package/src/cli/study-output.ts +71 -0
- package/src/cli/study.ts +58 -0
- package/src/cli/task-output.ts +6 -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 +993 -8
- package/src/hub/budget.ts +3 -4
- package/src/hub/channel-project.ts +61 -0
- 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 +39 -10
- package/src/hub/daemon.ts +326 -25
- package/src/hub/db-schema.ts +143 -0
- package/src/hub/delivery-journal.ts +3 -4
- package/src/hub/envelope.ts +2 -0
- package/src/hub/events.ts +4 -1
- package/src/hub/execution-budget.ts +15 -4
- package/src/hub/folder-trigger.ts +3 -4
- package/src/hub/handoff.ts +126 -0
- package/src/hub/hub-tools.ts +12 -2
- package/src/hub/invocations.ts +3 -4
- package/src/hub/lifecycle.ts +87 -29
- package/src/hub/manager.ts +110 -87
- package/src/hub/no-writes.ts +1 -1
- package/src/hub/orca-env.ts +19 -0
- package/src/hub/path-policy.ts +12 -8
- package/src/hub/project-runtime-state.ts +139 -0
- package/src/hub/project-runtime.ts +62 -0
- package/src/hub/project.ts +14 -5
- package/src/hub/protocol-window.ts +19 -0
- package/src/hub/provenance.ts +30 -0
- package/src/hub/recovery-store.ts +102 -12
- package/src/hub/registry.ts +5 -0
- package/src/hub/research-campaign.ts +265 -0
- package/src/hub/research-experiments.ts +474 -0
- package/src/hub/research-runner.ts +172 -0
- package/src/hub/restart.ts +35 -0
- package/src/hub/runtime-capture-prune.ts +415 -0
- package/src/hub/runtime-prune-health.ts +112 -0
- package/src/hub/schedules.ts +213 -13
- package/src/hub/sdlc-merge.ts +7 -0
- package/src/hub/sdlc-queue.ts +13 -1
- package/src/hub/session-identity.ts +49 -0
- 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 +1005 -34
- package/src/hub/ui.ts +15 -6
- package/src/hub/usage-rollup.ts +3 -4
- package/src/memory/capture.ts +2 -2
- package/src/pi/tool-receipts.ts +3 -4
- package/src/ui/index.html +5 -3
- package/src/ui/office-space.ts +118 -0
- package/src/ui/plugin.ts +293 -0
- package/templates/AGENTS.block.md +1 -0
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +4 -4
- package/templates/profiles/writing.json +1 -1
- package/templates/roles/research/analyst.md +1 -0
- package/templates/roles/research/fact-checker.md +1 -0
- package/templates/roles/research/research-lead.md +1 -0
- package/templates/roles/research/researcher.md +1 -0
- package/templates/roles/research/statistician.md +1 -0
|
@@ -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
|
|
|
@@ -12,7 +12,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
12
12
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import { ControlClient, INVOCATION_CONTROL_TIMEOUT_MS
|
|
15
|
+
import { ControlClient, INVOCATION_CONTROL_TIMEOUT_MS } from "../hub/control-client.ts";
|
|
16
|
+
import { channelProject } from "../hub/channel-project.ts";
|
|
16
17
|
import { VERSION } from "../version.ts";
|
|
17
18
|
import { IMPLEMENTER_HUB_TOOL_NAMES, IMPLEMENTER_PERMISSION_TOOL, ACTIONABLE_MESSAGE_INSTRUCTION, projectConductorTools, projectToolList, CONDUCTOR_TOOL_NAMES, DEFAULT_ROLES, roleContract, TASK_TOOL_NAMES, TASK_TOOLS, profileTaskTools } from "../hub/hub-tools.ts";
|
|
18
19
|
import { frame, replyParent, sanitize, HUB_MESSAGE_INSTRUCTION, type Envelope } from "../hub/envelope.ts";
|
|
@@ -22,8 +23,23 @@ import { acceptsNativePermissionRequest, buildNativePermissionProbe, NATIVE_PERM
|
|
|
22
23
|
// A native session is pinned at launch. Unlike a new CLI invocation after `cd`,
|
|
23
24
|
// its MCP server must not silently move to another hub (including while offline).
|
|
24
25
|
// ControlClient verifies the manifest identity on every connection.
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
//
|
|
27
|
+
// agent-hub#1036: which project this plugin joins is decided by `channelProject`, the same decision the CLI makes at startup. An inherited AGENTHUB_STATE_DIR is used as the peer only when the
|
|
28
|
+
// root recorded in it equals the resolved root of this process's cwd, or (a declared extra instance) when its managed worktree is this cwd. A session started in `<repo>/.worktrees/<name>`
|
|
29
|
+
// uses its own project. A session with no project of its own that inherited another project's state joins THAT hub as a distinct session (`<peer>~<id>`, minted by the hub): it can send and
|
|
30
|
+
// read, holds none of the peer's roles, and never replaces the attached session. Nothing is refused. Limit: this makes sessions distinguishable, it does not authenticate them.
|
|
31
|
+
const own = channelProject(process.cwd(), process.env, root => {
|
|
32
|
+
const read = (name: string) => { try { return JSON.parse(readFileSync(join(root, ".agenthub", name), "utf8")).roles ?? {}; } catch { return {}; } };
|
|
33
|
+
return { ...DEFAULT_ROLES, ...read("config.json"), ...read("config.local.json") };
|
|
34
|
+
});
|
|
35
|
+
/** Set when this process joins as a distinct session. The hub's answer names the minted id; until then the session is only "a session". */
|
|
36
|
+
const sessionJoin = own?.session;
|
|
37
|
+
/** A hub that predates `session_identity` would ignore the request and bind the real peer id, which is the takeover this exists to prevent: such a session does not connect, and says why. */
|
|
38
|
+
const NO_SESSION_SUPPORT = "this process has no project of its own and the hub it inherited predates session identities, so it would be taken for the peer it names; restart the hub (ahub restart) or start this session from its own project";
|
|
39
|
+
const NO_PROJECT = "agent-hub has no hub to join: this process has no project of its own and inherited no state directory. Start it from a project that runs agent-hub.";
|
|
40
|
+
if (process.env.AGENTHUB_STATE_DIR && own && !own.inherited && !own.session) console.error(`[agent-hub] ignoring an inherited AGENTHUB_STATE_DIR: it records another project, not ${own.root}. Using this project's own hub.`);
|
|
41
|
+
const stateDir = own?.stateDir ?? "";
|
|
42
|
+
const projectRoot = own?.root ?? "";
|
|
27
43
|
/** tools mode: the same server, run by Kimi (ACP mcpServers), Codex (mcp_servers override) or Jcode. Their messages arrive through their own adapters, so no channel here. Not the "tools-only" Claude below. */
|
|
28
44
|
const toolsMode = process.env.AGENTHUB_MODE === "tools";
|
|
29
45
|
const permissionTool = toolsMode && process.env.AGENTHUB_PERMISSION_TOOL === "1";
|
|
@@ -111,6 +127,10 @@ const INBOX_CAP = 200;
|
|
|
111
127
|
* whoever said hello last, so reconnecting blind would evict the session that just took it and the two
|
|
112
128
|
* would trade the peer forever. This file is advisory: a standby hello also checks live ownership at the daemon.
|
|
113
129
|
*/
|
|
130
|
+
/** Whether the hub this process would join as a session knows `session_identity`, from the daemon's own status file. Unknown reads as unsupported. */
|
|
131
|
+
function sessionSupported(): boolean {
|
|
132
|
+
try { return (JSON.parse(readFileSync(join(stateDir, "status.json"), "utf8")).features ?? []).includes("session_identity"); } catch { return false; }
|
|
133
|
+
}
|
|
114
134
|
function peerHeld(): boolean {
|
|
115
135
|
try {
|
|
116
136
|
const status = JSON.parse(readFileSync(join(stateDir, "status.json"), "utf8")) as { peers?: Record<string, { state?: string; claiming?: boolean; toolsOnly?: string }> };
|
|
@@ -266,7 +286,8 @@ async function connectLoop(): Promise<void> {
|
|
|
266
286
|
try { executionCwd = realPath(process.cwd()); }
|
|
267
287
|
catch { /* A removed native cwd revokes root evidence, never the messaging connection. */ }
|
|
268
288
|
}
|
|
269
|
-
const client = await ControlClient.connect(stateDir, { role: toolsMode ? "tools" : "peer", peer: peerId, ...(toolsMode ? {} : { channel }),
|
|
289
|
+
const client = await ControlClient.connect(stateDir, { role: toolsMode || sessionJoin ? "tools" : "peer", peer: peerId, ...(toolsMode || sessionJoin ? {} : { channel }),
|
|
290
|
+
...(sessionJoin ? { session: sessionJoin.label } : {}),
|
|
270
291
|
...(process.env.AGENTHUB_LAUNCH_ID ? { launchId: process.env.AGENTHUB_LAUNCH_ID } : {}),
|
|
271
292
|
...(executionCwd !== undefined ? { cwd: executionCwd } : {}),
|
|
272
293
|
...(!toolsMode && standingBy ? { standby: true } : {}),
|
|
@@ -293,7 +314,7 @@ async function connectLoop(): Promise<void> {
|
|
|
293
314
|
attempt = -1;
|
|
294
315
|
standingBy = false;
|
|
295
316
|
detached = undefined;
|
|
296
|
-
log(`connected to hub as "${peerId}"`);
|
|
317
|
+
log(sessionJoin ? `connected to hub as the distinct session "${client.sessionId ?? "?"}", not as "${peerId}": it can send and read, and holds none of ${peerId}'s roles` : `connected to hub as "${peerId}"`);
|
|
297
318
|
// The tool list depends on the connected hub (its lifecycle): when it differs from what the host last listed,
|
|
298
319
|
// ask the host to list it again (#490).
|
|
299
320
|
if (listedTools !== undefined) void reannounce(client).catch(() => {});
|
|
@@ -394,6 +415,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
394
415
|
async function callTool(req: { params: { name: string; arguments?: Record<string, unknown> } }, signal?: AbortSignal) {
|
|
395
416
|
const { name, arguments: args } = req.params;
|
|
396
417
|
if (unidentified) return { ...text(UNIDENTIFIED), isError: true };
|
|
418
|
+
if (!own) return { ...text(NO_PROJECT), isError: true };
|
|
419
|
+
if (sessionJoin && !sessionSupported()) return { ...text(NO_SESSION_SUPPORT), isError: true };
|
|
397
420
|
if (permissionTool && !IMPLEMENTER_HUB_TOOL_NAMES.includes(name)) return { ...text("tool unavailable to this owned implementer child"), isError: true };
|
|
398
421
|
if (name === "hub_permission_request") {
|
|
399
422
|
const deny = (message: string) => text(JSON.stringify({ behavior: "deny", message }));
|
|
@@ -473,4 +496,4 @@ await server.connect(new StdioServerTransport());
|
|
|
473
496
|
// The host (Claude Code, Codex, Kimi) talks over stdin; once it is gone nothing can use this server, and a leftover would keep reconnecting.
|
|
474
497
|
process.stdin.on("end", () => process.exit(0));
|
|
475
498
|
process.stdin.on("close", () => process.exit(0));
|
|
476
|
-
if (unidentified) log(UNIDENTIFIED); else void connectLoop();
|
|
499
|
+
if (unidentified) log(UNIDENTIFIED); else if (!own) log(NO_PROJECT); else if (sessionJoin && !sessionSupported()) log(NO_SESSION_SUPPORT); else void connectLoop();
|
|
@@ -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: {
|
|
@@ -8,6 +8,36 @@ const nativeTools = [
|
|
|
8
8
|
] as const;
|
|
9
9
|
export const DOCTOR_TOOL_PROBES: readonly string[] = Object.freeze(["bun", ...nativeTools.filter(tool => tool.initial).map(tool => tool.id)]);
|
|
10
10
|
export const DOCTOR_MEMORY_CAPTURE_PEERS = Object.freeze(["codex", "kimi"] as const);
|
|
11
|
+
/**
|
|
12
|
+
* What one read of agent-mem's own setup state gave (`amem setup <tool> --json`, a preview that writes nothing): the parsed plan, or why there is none. Agent-mem owns the state;
|
|
13
|
+
* doctor never reads dotfiles text or a client's config itself (#1008).
|
|
14
|
+
*/
|
|
15
|
+
export type MemorySetupReading = { kind: "read"; plan: unknown } | { kind: "unavailable"; reason: string };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One memory-capture doctor row from agent-mem's structured setup plan: `applied` (the hub's receipt matches and the MCP entry and hooks are as setup would write them), `pending`
|
|
19
|
+
* (setup would add or replace something, so `amem setup <tool> --apply` is the fix), or `client missing` (the native CLI is not installed). A reading that cannot be trusted as the shape
|
|
20
|
+
* agent-mem documents is unknown, never ready: the row says it could not be read instead of guessing. Pure: the plan is data, never text, and nothing here runs a process.
|
|
21
|
+
* Codex also needs the person's native `/hooks` review before a newly registered hook runs, which agent-mem's setup does not grant, so an applied Codex row says so.
|
|
22
|
+
*/
|
|
23
|
+
export function nativeMemoryCaptureRow(tool: string, reading: MemorySetupReading): { ok: boolean | undefined; detail: string } {
|
|
24
|
+
const unknown = (why: string) => ({ ok: undefined, detail: `unknown (${why})` });
|
|
25
|
+
if (reading.kind !== "read") return unknown(reading.reason);
|
|
26
|
+
const plan = reading.plan as { mode?: unknown; actions?: unknown } | null;
|
|
27
|
+
const actions = plan && typeof plan === "object" && plan.mode === "preview" && Array.isArray(plan.actions) ? plan.actions : undefined;
|
|
28
|
+
const action = actions?.length === 1 ? actions[0] as Record<string, unknown> | null : undefined;
|
|
29
|
+
if (!action || typeof action !== "object" || action.target !== tool || typeof action.available !== "boolean" || !["add", "replace", "restore", "unchanged"].includes(String(action.mcpChange)))
|
|
30
|
+
return unknown("`amem setup " + tool + " --json` did not return the setup plan it documents");
|
|
31
|
+
if (action.available === false) return { ok: false, detail: `client missing: the ${tool} CLI is not installed, so there is nothing to set up` };
|
|
32
|
+
const setup = `amem setup ${tool} --apply`;
|
|
33
|
+
if (action.mcpChange !== "unchanged") {
|
|
34
|
+
const state = typeof action.state === "string" ? action.state.slice(0, 60).replace(/[^\x20-\x7e]/g, "?") : "unknown";
|
|
35
|
+
return { ok: false, detail: `not set up (${state}); run \`${setup}\`` };
|
|
36
|
+
}
|
|
37
|
+
const hooks = tool === "codex" ? "; newly registered hooks need the native /hooks review in Codex before they run (amem setup does not grant hook trust)" : "";
|
|
38
|
+
return { ok: true, detail: `set up, per \`amem setup ${tool}\`${hooks}` };
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
export const DOCTOR_PLUGIN_PROBE = Object.freeze({ command: "claude", argv: Object.freeze(["plugin", "list", "--json"] as const), name: "claude plugin" });
|
|
12
42
|
|
|
13
43
|
/** Exact legacy section and severity classification, including non-native and unknown diagnostic names. */
|
|
@@ -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.
|
package/src/adapters/jcode.ts
CHANGED
|
@@ -210,6 +210,9 @@ export class JcodePeer extends AcpPeer {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/** True only for the id of this peer's current owned launch, while it is not ending: the proof a hub-tools hello must carry (#368). */
|
|
213
|
+
/** The id of this peer's current owned launch, for the provenance of what it publishes (agent-hub#1036). Evidence for the log, never a credential: nothing admits a connection by reading it. */
|
|
214
|
+
get ownedLaunchId(): string | undefined { return this.ending ? undefined : this.launchId; }
|
|
215
|
+
|
|
213
216
|
ownsLaunch(launchId: unknown): boolean { return !this.ending && typeof launchId === "string" && launchId === this.launchId; }
|
|
214
217
|
|
|
215
218
|
override async start(): Promise<void> {
|
|
@@ -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
|
|
@@ -292,7 +293,7 @@ export function builtinPreviewOf(id: string) {
|
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
/** Fixed builtin doctor metadata and pure observed-value projections; the CLI retains all probe effects. */
|
|
295
|
-
export { DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "./definitions/native-cli-doctor.ts";
|
|
296
|
+
export { DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeMemoryCaptureRow, type MemorySetupReading, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "./definitions/native-cli-doctor.ts";
|
|
296
297
|
|
|
297
298
|
/** Definition-owned CLI entries are restricted to fixed builtins, never config or external execution extensions. */
|
|
298
299
|
export const BUILTIN_CLI_ENTRIES = Object.freeze(builtinCliEntries(ADAPTERS).map(entry => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** Where `ahub setup` registers the acon plugin (#935): acon reads $AGENTHUB_HOME/console/plugins/<tool>.json. */
|
|
6
|
+
export function aconManifestPath(home: string): string {
|
|
7
|
+
return join(home, "console", "plugins", "ahub.json");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Write the acon registration for this package's plugin module with its sha256, so acon refuses a module that changed
|
|
12
|
+
* after setup. The directory is created 0700; the file is replaced atomically. Returns what was written, or why not.
|
|
13
|
+
*/
|
|
14
|
+
export function registerAconPlugin(home: string, module: string): { path: string; changed: boolean } | { error: string } {
|
|
15
|
+
if (!existsSync(module)) return { error: `acon plugin module ${module} is missing` };
|
|
16
|
+
const dir = join(home, "console", "plugins");
|
|
17
|
+
for (const part of [join(home, "console"), dir]) {
|
|
18
|
+
if (!existsSync(part)) mkdirSync(part, { mode: 0o700 });
|
|
19
|
+
if (!lstatSync(part).isDirectory()) return { error: `${part} is not a directory` };
|
|
20
|
+
chmodSync(part, 0o700);
|
|
21
|
+
}
|
|
22
|
+
const path = aconManifestPath(home);
|
|
23
|
+
const body = `${JSON.stringify({ api: 1, tool: "ahub", module, sha256: createHash("sha256").update(readFileSync(module)).digest("hex") })}\n`;
|
|
24
|
+
if (existsSync(path) && readFileSync(path, "utf8") === body) return { path, changed: false };
|
|
25
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
26
|
+
writeFileSync(temp, body, { mode: 0o600 });
|
|
27
|
+
renameSync(temp, path);
|
|
28
|
+
return { path, changed: true };
|
|
29
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Argument parsing for `ahub task handoff` (#604, agent-hub#1030). Pure: argv in, one request or a usage line out, so main.ts stays a thin dispatcher and every form is testable without a daemon.
|
|
2
|
+
import { CLASSES } from "../hub/board.ts";
|
|
3
|
+
|
|
4
|
+
export const HANDOFF_USAGE = "usage: ahub task handoff --project <root|id> [<class>] <title...> [--path <p>]... [--issue <n>] [--task <n>] [--detail <text>] [--json] [--color=auto|always|never]";
|
|
5
|
+
|
|
6
|
+
export interface HandoffCliRequest { project: string; title: string; class?: string; detail?: string; paths?: string[]; issue?: number; task?: number }
|
|
7
|
+
|
|
8
|
+
/** `operands` is argv after `task handoff`, presentation flags already removed. A first word that is a class name is the class, said out loud by the caller (the documented short form). */
|
|
9
|
+
export function parseHandoffArgs(operands: readonly string[]): HandoffCliRequest | { usage: string } {
|
|
10
|
+
const one: Record<string, string> = {}, paths: string[] = [], rest: string[] = [];
|
|
11
|
+
for (let i = 0; i < operands.length; i++) {
|
|
12
|
+
const arg = operands[i]!;
|
|
13
|
+
if (arg === "--project" || arg === "--detail" || arg === "--issue" || arg === "--task" || arg === "--path") {
|
|
14
|
+
const value = operands[++i];
|
|
15
|
+
if (value === undefined || value.startsWith("--")) return { usage: HANDOFF_USAGE };
|
|
16
|
+
if (arg === "--path") paths.push(value); else { if (Object.hasOwn(one, arg)) return { usage: HANDOFF_USAGE }; one[arg] = value; }
|
|
17
|
+
} else if (arg.startsWith("--")) return { usage: HANDOFF_USAGE };
|
|
18
|
+
else rest.push(arg);
|
|
19
|
+
}
|
|
20
|
+
const project = one["--project"];
|
|
21
|
+
const classed = rest[0] !== undefined && (CLASSES as readonly string[]).includes(rest[0]) && rest.length > 1;
|
|
22
|
+
const title = (classed ? rest.slice(1) : rest).join(" ").trim();
|
|
23
|
+
if (!project || !title) return { usage: HANDOFF_USAGE };
|
|
24
|
+
if (one["--issue"] !== undefined && !/^[1-9]\d{0,9}$/.test(one["--issue"])) return { usage: `${HANDOFF_USAGE}\n--issue is the target repository's issue number` };
|
|
25
|
+
if (one["--task"] !== undefined && !/^[1-9]\d{0,9}$/.test(one["--task"])) return { usage: `${HANDOFF_USAGE}\n--task is this project's own task id` };
|
|
26
|
+
return { project, title, ...(classed ? { class: rest[0]! } : {}), ...(one["--detail"] !== undefined ? { detail: one["--detail"] } : {}), ...(paths.length ? { paths } : {}), ...(one["--issue"] !== undefined ? { issue: Number(one["--issue"]) } : {}), ...(one["--task"] !== undefined ? { task: Number(one["--task"]) } : {}) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Human output for `ahub task handoff` and `ahub board --handoffs` (#604, agent-hub#1030). Pure: the hub's parsed JSON in, spans out, no I/O. `--json` is the caller's branch and prints the same text.
|
|
2
|
+
// Only ids, states and counts are shown: the title of a handed-off task is the caller's own and was bounded and sanitized by the hub.
|
|
3
|
+
import { blank, grid, heading, pair, s } from "./sdlc-output.ts";
|
|
4
|
+
import type { Span, Tone } from "./console-state.ts";
|
|
5
|
+
|
|
6
|
+
const STATE_TONE: Record<string, Tone | undefined> = { approved: undefined, cancelled: "muted", declined: "attention", proposed: "attention" };
|
|
7
|
+
const num = (value: unknown): string => typeof value === "number" && Number.isSafeInteger(value) ? String(value) : "-";
|
|
8
|
+
|
|
9
|
+
/** The receipt of one handoff: where the task now lives and what to run to follow it. */
|
|
10
|
+
export function renderHandoffReceipt(receipt: unknown, columns?: number): Span[][] {
|
|
11
|
+
const r = receipt as any;
|
|
12
|
+
return [...heading(`Handed off to project ${r?.project ?? "?"}`, columns),
|
|
13
|
+
...pair("task", `#${num(r?.task)} (${r?.state ?? "proposed"})`, columns, 8, "taskRef"),
|
|
14
|
+
blank, [s("it is on that project's board as an ownerless proposal; its own roles plan, implement and review it, and you are told when it changes state", "muted")],
|
|
15
|
+
[s("ahub board --handoffs shows the last state it reported", "muted")]];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What this project handed to others, with the last state each target reported. */
|
|
19
|
+
export function renderHandoffs(data: unknown, columns?: number): Span[][] {
|
|
20
|
+
const rows: any[] = Array.isArray((data as any)?.handoffs) ? (data as any).handoffs : [];
|
|
21
|
+
if (!rows.length) return [[s("no handoffs: hand work that belongs to another project with `ahub task handoff --project <root|id> <title...>`", "muted")]];
|
|
22
|
+
return [...heading(`Handoffs (${rows.length})`, columns), ...grid(["PROJECT", "TASK", "STATE", "PR", "BY", "TITLE"], rows.map(row => [
|
|
23
|
+
s(String(row.remoteProject ?? "?").slice(0, 24), "issueRef"), s(`#${num(row.remoteTask)}`, "taskRef"), s(String(row.state ?? "?"), STATE_TONE[row.state]),
|
|
24
|
+
s(typeof row.pr === "number" ? `#${row.pr}` : "-"), s(String(row.originPeer ?? "?")), s(String(row.title ?? ""))]), columns)];
|
|
25
|
+
}
|
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, 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"],
|
|
@@ -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 and memory
|
|
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"],
|
|
@@ -86,8 +86,9 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
86
86
|
]],
|
|
87
87
|
["Tasks and review", [
|
|
88
88
|
["ahub board import github --label <label> [--milestone <milestone>] [--repo <owner/repo>]", "person or configured conductor: read bounded open issues into deduplicated Backlog; never writes GitHub"],
|
|
89
|
-
["ahub board [state | --ready] [--columns] [--release <name> | --release-id <id>] [--json] [--full] [--color=auto|always|never]", "the task board, one line per task at a terminal width; --full: whole titles and SDLC advice; --ready: proposed tasks with nothing left to wait for"],
|
|
89
|
+
["ahub board [state | --ready] [--columns] [--release <name> | --release-id <id>] [--handoffs] [--json] [--full] [--color=auto|always|never]", "the task board, one line per task at a terminal width; --full: whole titles and SDLC advice; --ready: proposed tasks with nothing left to wait for; --handoffs: what this project handed to other projects and the last state each reported"],
|
|
90
90
|
["ahub task propose [--class <c> | <class>] <title...> [--owner <peer>] [--path <p>]... [--after <id>]... [--urgent] [--backlog] [--detail <text>]", "put a task on the board; a first word that names a class is taken as the class; while a task named by --after is not approved yet, --owner reserves the task for that peer, offered to it first once it is ready; otherwise --owner assigns it now"],
|
|
91
|
+
["ahub task handoff --project <root|id> [<class>] <title...> [--path <p>]... [--issue <n>] [--task <n>] [--detail <text>] [--json] [--color=auto|always|never]", "person only: register work that belongs to ANOTHER project that runs agent-hub on that project's board (an ownerless proposal its own roles take from there); the target must be registered and running, nothing is queued; a feature or behavior change first needs a spec issue there (--issue); its state changes show in ahub board --handoffs (an agent that hands off gets them as task messages)"],
|
|
91
92
|
["ahub backlog list [--json] [--full] [--color=auto|always|never] | add <class> <title...> | plan <id> <revision> <fields-json> <reason...> | pin <id> <revision> true|false <reason...> | undo <id> <revision> [--json] [--color=auto|always|never]", "person only: plan work with a reason; {} records a note-only revision, fields.sdlc sets backlog repo/issue identity and clears old spec acceptance; preserve pins/frozen scope and refuse stale edits; list is by rank (--json: raw listing)"],
|
|
92
93
|
["ahub release new <name> <goal> <order> | list | show <name> | plan <name> <revision> <dates-json> | activate|freeze|ship|drop <name> <revision> [--json] [--color=auto|always|never]", "person only, except show (any agent, public metadata): planned UTC millisecond dates and phased scope; ship records a time after approval, never tags or publishes"],
|
|
93
94
|
["ahub backlog split <id> <revision> <parts-json> <reason...> [--json] [--color=auto|always|never]", "person only: stage2..8 child prerequisites; original task retains integration, undo refuses used or changed children"],
|
|
@@ -105,6 +106,8 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
105
106
|
["ahub intake \"<text>\" [--source <id>...] | approve <id> | discard <id> [--json] [--color=auto|always|never]", "person only, except approve (person or configured conductor): turn a plain request into a draft backlog card that nothing offers or pulls until you approve it; the configured planner is asked to split it with hub_backlog_split, or the hub model proposes the split (a PII request only on an on-prem model, never to a planner); each --source must be a registered src: id; approve clears the draft on it and its parts, discard cancels them through task cancel"],
|
|
106
107
|
["ahub capabilities [--json] [--color=auto|always|never]", "person only, never a conductor or an agent shell: list the skills and commands each attached peer offers (Kimi 2.3.2 and Qwen 0.26.1 sessions, Codex 0.166.0 skills; other versions list nothing), whether you share each one, its digest and the class a call would count as; sharing is invocation.share in .agenthub/config.local.json, this machine only; it only lists and never calls anything"],
|
|
107
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
|
+
["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
|
+
["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"],
|
|
108
111
|
["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"],
|
|
109
112
|
["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)"],
|
|
110
113
|
["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
|
@@ -22,6 +22,8 @@ export function liveTestRefusal(command: string, args: readonly string[], contex
|
|
|
22
22
|
|| (command === "room" && !["list", "status"].includes(sub ?? ""))
|
|
23
23
|
|| (command === "backlog" && sub !== "list") || (command === "release" && !["list", "show"].includes(sub ?? ""))
|
|
24
24
|
|| (command === "memory" && sub !== "graph")
|
|
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")
|
|
25
27
|
|| (command === "recovery" && (sub === "reattach" || sub === "dispose" && context.repairedFresh === true))
|
|
26
28
|
|| (command === "board" && args[0] === "import") || (command === "sdlc" && (args[0] === "record" || args[0] === "accept" || args[0] === "accept-departure"))
|
|
27
29
|
|| (command === "bench" && args.find(arg => arg !== "--json") === "run") || command === "pet"
|
|
@@ -82,6 +84,8 @@ const peerReadForms: Readonly<Record<string, Readonly<Record<string, PeerReadFor
|
|
|
82
84
|
// #814 renders these reads for people and keeps JSON as an explicit data format.
|
|
83
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 } },
|
|
84
86
|
release: { show: { positional: 1, minimum: 1, displayFlags: [] } },
|
|
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 } },
|
|
85
89
|
projects: { "": {} },
|
|
86
90
|
models: { status: {} },
|
|
87
91
|
recovery: { status: { positional: 1 } },
|
|
@@ -191,8 +195,10 @@ export function cliCommandLabel(command: string, args: readonly string[], catalo
|
|
|
191
195
|
if (command === "capabilities") return "capabilities"; // #445 C1: no argument is ever echoed into the audit
|
|
192
196
|
if (command === "learn") return ["list", "accept", "reject"].includes(args.find(arg => arg !== "--json") ?? "") ? `learn ${args.find(arg => arg !== "--json")}` : "learn";
|
|
193
197
|
if (command === "source") return ["add", "list"].includes(args.find(arg => arg !== "--json") ?? "") ? `source ${args.find(arg => arg !== "--json")}` : "source";
|
|
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";
|
|
194
200
|
if (command === "release") return ["new", "list", "show", "plan", "activate", "freeze", "ship", "drop"].includes(args[0] ?? "") ? `release ${args[0]}` : "release";
|
|
195
|
-
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"].includes(command)) return "unknown";
|
|
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";
|
|
196
202
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench", "agents", "roster", "settings"].includes(command)) {
|
|
197
203
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
198
204
|
if (sub && ["propose", "show", "assign", "unassign", "escalate", "cancel", "claim", "label", "list", "add", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "reattach", "setup", "start", "stop", "remove", "discard", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "dedupe", "decay", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take", "trust"].includes(sub)) return `${command} ${sub}`;
|