@staix/agent-hub 0.12.33 → 0.12.35
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 +22 -0
- package/README.md +3 -3
- package/docs/agent-notes/adapters.md +9 -3
- package/docs/agent-notes/budget.md +7 -3
- package/docs/agent-notes/bus.md +2 -1
- package/docs/agent-notes/daemon.md +7 -3
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/tasks.md +6 -2
- package/docs/agent-notes/tests.md +2 -1
- package/docs/gsd-collaboration-analysis.md +5 -1
- package/docs/operations.md +80 -16
- package/docs/quickstart.md +10 -2
- package/docs/sdlc.md +59 -34
- package/docs/security.md +7 -4
- package/docs/smoke.md +1 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +407 -10
- package/docs/verification/2026-10-18-0.12.34.md +48 -0
- package/docs/verification/2026-10-18-0.12.35.md +49 -0
- package/docs/verification/2026-10-18-493-claude-busy-channel.md +91 -0
- package/docs/verified.json +35 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +23567 -16606
- package/src/adapters/acp.ts +3 -1
- package/src/adapters/claude-channel.ts +59 -29
- package/src/adapters/codex-appserver.ts +82 -12
- package/src/adapters/definitions/builtin-metadata.ts +101 -7
- package/src/adapters/definitions/native-cli-claude-launch.ts +90 -0
- package/src/adapters/definitions/native-cli-claude-setup.ts +80 -0
- package/src/adapters/definitions/native-cli-codex-launch.ts +14 -0
- package/src/adapters/definitions/native-cli-doctor.ts +46 -0
- package/src/adapters/definitions/native-cli-entry-claude.ts +52 -0
- package/src/adapters/definitions/native-cli-entry-codex.ts +34 -0
- package/src/adapters/definitions/native-cli-entry-jcode.ts +27 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +57 -0
- package/src/adapters/definitions/native-cli-entry-local.ts +15 -0
- package/src/adapters/definitions/native-cli-entry-pi.ts +53 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +21 -0
- package/src/adapters/definitions/native-cli-entry-types.ts +41 -0
- package/src/adapters/definitions/native-cli-entry.ts +19 -0
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +58 -0
- package/src/adapters/definitions/native-cli-launch-common.ts +17 -0
- package/src/adapters/definitions/native-cli-pi.ts +33 -0
- package/src/adapters/definitions/native-cli-preview-claude.ts +37 -0
- package/src/adapters/definitions/native-cli-preview-codex.ts +20 -0
- package/src/adapters/definitions/native-cli-preview-common.ts +57 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +20 -0
- package/src/adapters/definitions/native-cli-preview-kimi.ts +28 -0
- package/src/adapters/definitions/native-cli-preview-pi.ts +36 -0
- package/src/adapters/definitions/native-cli-preview-qwen.ts +22 -0
- package/src/adapters/definitions/native-cli-preview.ts +16 -0
- package/src/adapters/definitions/native-config-defaults.ts +19 -0
- package/src/adapters/definitions/native-config-policy.ts +35 -0
- package/src/adapters/definitions/native-context.ts +35 -0
- package/src/adapters/definitions/native-daemon-kimi.ts +5 -0
- package/src/adapters/definitions/native-daemon-policy.ts +18 -0
- package/src/adapters/definitions/native-display.ts +18 -0
- package/src/adapters/definitions/native-factories.ts +48 -0
- package/src/adapters/definitions/native-policy-aux.ts +130 -0
- package/src/adapters/definitions/native-quota.ts +29 -0
- package/src/adapters/definitions/native-recovery-continuity.ts +12 -0
- package/src/adapters/definitions/native-recovery.ts +88 -0
- package/src/adapters/definitions/qwen-native.ts +2 -2
- package/src/adapters/definitions/qwen.ts +8 -4
- package/src/adapters/local-worker.ts +3 -2
- package/src/adapters/pi.ts +3 -1
- package/src/adapters/registry-types.ts +100 -10
- package/src/adapters/registry.ts +127 -11
- package/src/cli/console-state.ts +5 -5
- package/src/cli/identity.ts +8 -7
- package/src/cli/jcode-mcp.ts +2 -58
- package/src/cli/launch.ts +41 -102
- package/src/cli/main.ts +126 -290
- package/src/cli/output.ts +3 -2
- package/src/cli/preview.ts +26 -126
- package/src/cli/recovery-guidance.ts +43 -22
- package/src/cli/setup.ts +2 -80
- package/src/cli/shell-quote.ts +3 -0
- package/src/cli/terminal-recovery.ts +81 -22
- package/src/cli/upgrade-interactive.ts +4 -2
- package/src/cli/upgrade-restore-state.ts +17 -7
- package/src/cli/upgrade-runtime.ts +126 -147
- package/src/cli/upgrade.ts +44 -27
- package/src/hub/budget.ts +59 -31
- package/src/hub/bus.ts +1 -1
- package/src/hub/conductor.ts +19 -9
- package/src/hub/config-apply.ts +22 -15
- package/src/hub/config-reload.ts +18 -3
- package/src/hub/config-trust.ts +1 -1
- package/src/hub/context-window.ts +3 -27
- package/src/hub/control-client.ts +1 -1
- package/src/hub/crash.ts +15 -39
- package/src/hub/daemon.ts +506 -341
- package/src/hub/execution-budget.ts +3 -2
- package/src/hub/hub-tools.ts +19 -8
- package/src/hub/instance-budget.ts +73 -0
- package/src/hub/instance-config.ts +47 -0
- package/src/hub/instance-execution.ts +50 -0
- package/src/hub/instances.ts +100 -0
- package/src/hub/lifecycle-schema.ts +7 -3
- package/src/hub/peer-catalog.ts +4 -13
- package/src/hub/permission-mode.ts +14 -17
- package/src/hub/recovery-continuity.ts +6 -5
- package/src/hub/report.ts +9 -8
- package/src/hub/roster.ts +9 -9
- package/src/hub/routing.ts +94 -19
- package/src/hub/sdlc-queue.ts +2 -2
- package/src/hub/sdlc.ts +84 -38
- package/src/hub/settings.ts +39 -10
- package/src/hub/settled-roles.ts +1 -1
- package/src/hub/snapshots.ts +40 -9
- package/src/hub/start-mode.ts +13 -8
- package/src/hub/status-roster.ts +8 -0
- package/src/hub/tasks.ts +60 -27
- package/src/hub/text-projection.ts +7 -3
- package/src/hub/tooling.ts +4 -60
- package/src/hub/ui.ts +3 -1
- package/src/hub/workflow.ts +30 -52
- package/src/memory/recall.ts +4 -5
- package/src/models/route/signals.ts +3 -1
- package/src/pi/launch.ts +5 -3
- package/src/ui/index.html +4 -3
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +16 -6
- package/templates/AGENTS.block.md +2 -1
- package/templates/routing.toml +2 -2
package/src/cli/output.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definitionOf } from "../adapters/registry.ts";
|
|
1
2
|
import { boardColumns, type BoardPeerInput } from "../ui/task-board.ts";
|
|
2
3
|
import { peerStyle } from "../ui/peer-palette.ts";
|
|
3
4
|
import { cells, TABLES, table, fitLine, wrap, duration, relative, plural, terminalText, initialConsoleState, stateTone, type Span, type Tone } from "./console-state.ts";
|
|
@@ -234,8 +235,8 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
234
235
|
: flat(p.paused).replace(/^(budget: .*), resets .+$/, "$1; reset unknown");
|
|
235
236
|
out.push(...detail("paused", pauseText, columns, "attention"));
|
|
236
237
|
}
|
|
237
|
-
if (p.toolsOnly) out.push(...detail(id
|
|
238
|
-
if (p.pullOnly && id
|
|
238
|
+
if (p.toolsOnly) out.push(...detail(definitionOf(id)?.attach?.pullOnlyForms?.includes("tui") && p.pullOnly ? "pull-only" : "tools-only", flat(p.toolsOnly).replace(/^(?:tools-only|pull-only):\s*/, ""), columns, "attention"));
|
|
239
|
+
if (p.pullOnly && definitionOf(id)?.attach?.pullOnlyForms?.includes("tui")) out.push(...detail(`${id} last read`, typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never", columns, "muted"));
|
|
239
240
|
if (p.permissionNote) out.push(...detail("permission", p.permissionNote, columns, "muted"));
|
|
240
241
|
if (p.requestedModel && p.servedBy && p.requestedModel !== p.servedBy) out.push(...detail("requested model", p.requestedModel, columns));
|
|
241
242
|
if (typeof p.oldestQueuedAt === "number") out.push(...detail(`${id} oldest queued`, relative(p.oldestQueuedAt, now), columns, "muted"));
|
package/src/cli/preview.ts
CHANGED
|
@@ -1,134 +1,34 @@
|
|
|
1
|
-
import { definitionOf,
|
|
2
|
-
import { jcodeCommand } from "../adapters/jcode.ts";
|
|
3
|
-
import { qwenSessionId } from "../hub/crash.ts";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
1
|
+
import { definitionOf, builtinPreviewOf } from "../adapters/registry.ts";
|
|
6
2
|
import { loadConfig } from "../hub/daemon.ts";
|
|
7
3
|
import { childEnv } from "../hub/child-process.ts";
|
|
8
|
-
import { relayModelIds } from "../models/relay.ts";
|
|
9
|
-
import { buildPiLaunch } from "../pi/launch.ts";
|
|
10
4
|
import { startModeOf } from "../hub/start-mode.ts";
|
|
11
|
-
import { buildLaunch, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, claudeObservationHooks
|
|
5
|
+
import { instanceLaunchArguments, buildLaunch, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, claudeObservationHooks } from "./launch.ts";
|
|
12
6
|
import { kimiMcpPlan } from "./kimi-mcp.ts";
|
|
7
|
+
import { jcodeCommand } from "../adapters/jcode.ts";
|
|
8
|
+
import { qwenSessionId } from "../hub/crash.ts";
|
|
9
|
+
import { relayModelIds } from "../models/relay.ts";
|
|
10
|
+
import { buildPiLaunch } from "../pi/launch.ts";
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/** Arbitrary native arguments can contain secrets even without a credential-looking key. */
|
|
17
|
-
function publicArg(arg: string): string {
|
|
18
|
-
const flags = new Set(["--settings", "--model", "--mcp-config", "--session-id", "--session-file", "--resume", "--continue", "--print", "--safe", "--new"]);
|
|
19
|
-
const key = arg.split("=", 1)[0]!;
|
|
20
|
-
return flags.has(key) ? (arg.includes("=") ? key + "=" + REDACTED : key) : REDACTED;
|
|
21
|
-
}
|
|
22
|
-
function nativeCommand(command: string[], trusted: string[]): string[] {
|
|
23
|
-
return command.map((value, i) => value === trusted[i] ? value : REDACTED);
|
|
24
|
-
}
|
|
25
|
-
/** Read-only preview. Inputs passed to shared native builders are redacted before serialization. */
|
|
26
|
-
export function launcherPreview(tool: "claude" | "codex" | "kimi" | "pi" | "qwen" | "jcode", raw: string[], cwd: string, stateDir: string, unattended: boolean) {
|
|
27
|
-
const args = raw.filter(arg => !["--print-command", "--dry-run", "--json"].includes(arg));
|
|
12
|
+
/** Read-only builtin preview. Native redaction uses the same launch builders as actual startup. */
|
|
13
|
+
export function launcherPreview(tool: "claude" | "codex" | "kimi" | "pi" | "qwen" | "jcode", raw: string[], cwd: string, stateDir: string, unattended: boolean, requestedInstance?: string) {
|
|
28
14
|
const config = loadConfig(cwd);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
if (!next || next.startsWith("--")) throw new Error("missing option value");
|
|
38
|
-
return next;
|
|
39
|
-
};
|
|
40
|
-
if (tool === "claude") {
|
|
41
|
-
let original: { command?: string; refreshInterval?: number; padding?: number } | undefined;
|
|
42
|
-
for (const file of [join(cwd, ".claude/settings.local.json"), join(cwd, ".claude/settings.json"), join(process.env.HOME ?? "", ".claude/settings.json")]) {
|
|
43
|
-
try { original ??= JSON.parse(readFileSync(file, "utf8")).statusLine; } catch { /* optional */ }
|
|
44
|
-
}
|
|
45
|
-
// Build with original inputs so channel selection and owned-flag validation match normal launch.
|
|
46
|
-
const facts = claudeObservationHooks(config, { script: join(import.meta.dir, "facts-hook.ts"), stateDir });
|
|
47
|
-
launch = buildLaunch(tool, args, { preview: true, unattended, statusLine: { script: join(import.meta.dir, "statusline-tee.ts"), stateDir, ...(original ? { original: { command: original.command ? REDACTED : "", ...(typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}), ...(typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {}) } } : {}) }, ...(facts ? { facts } : {}) });
|
|
48
|
-
// Merged caller settings change the native argument count. Rebuild a redacted preview,
|
|
49
|
-
// retaining the managed hooks but never serializing caller settings or their file path.
|
|
50
|
-
const safeArgs = args.map((arg, i) => {
|
|
51
|
-
if (args[i - 1] === "--settings") return "{}";
|
|
52
|
-
if (arg.startsWith("--settings=")) return "--settings={}";
|
|
53
|
-
if (["--unattended", "--safe", "--new"].includes(arg)) return arg;
|
|
54
|
-
return publicArg(arg);
|
|
55
|
-
});
|
|
56
|
-
const publicLaunch = buildLaunch(tool, safeArgs, { preview: true, unattended, statusLine: { script: join(import.meta.dir, "statusline-tee.ts"), stateDir,
|
|
57
|
-
...(original ? { original: { command: original.command ? REDACTED : "", ...(typeof original.refreshInterval === "number" && Number.isFinite(original.refreshInterval) ? { refreshInterval: original.refreshInterval } : {}), ...(typeof original.padding === "number" && Number.isFinite(original.padding) ? { padding: original.padding } : {}) } } : {}) }, facts });
|
|
58
|
-
publicLaunch.args.splice(0, 2, ...launch.args.slice(0, 2)); // channel selection was verified against the original MCP config
|
|
59
|
-
launch = publicLaunch;
|
|
60
|
-
} else if (tool === "codex") {
|
|
61
|
-
// A fresh native proxy is assigned by start, never guessed from an old status file.
|
|
62
|
-
launch = buildLaunch(tool, args, { unattended, proxyUrl: UNRESOLVED, codexBin: config.codex_bin === "codex" ? "codex" : REDACTED });
|
|
63
|
-
const passthrough = args.filter(arg => !["--unattended", "--safe", "--new"].includes(arg));
|
|
64
|
-
launch.args = launch.args.slice(0, launch.args.length - passthrough.length).concat(passthrough.map(publicArg));
|
|
65
|
-
unresolved.push({ field: "proxyUrl", reason: "assigned when the native adapter starts; preview does not start it" });
|
|
66
|
-
} else if (tool === "jcode") {
|
|
67
|
-
const mode = value("--mode") ?? "headless", model = value("--model"), profile = value("--provider-profile"), session = value("--session-id");
|
|
68
|
-
if (mode !== "headless") throw new Error("Jcode TUI driving is unverified");
|
|
69
|
-
for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode", "--provider-profile", "--session-id"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Jcode option");
|
|
70
|
-
if (session !== undefined && !qwenSessionId(session)) throw new Error("invalid Jcode session identity");
|
|
71
|
-
const cmd = jcodeCommand(nativeCommand(config.jcode_cmd, ["jcode"]), "acp", UNRESOLVED, UNRESOLVED, model ? REDACTED : undefined, profile || config.jcode_provider_profile ? REDACTED : undefined);
|
|
72
|
-
launch = { cmd: cmd[0]!, args: cmd.slice(1) };
|
|
73
|
-
unresolved.push({ field: "ownedDaemon", reason: "private daemon/socket and source-pinned native identity verified at start; preview starts nothing" });
|
|
74
|
-
unresolved.push({ field: "hubTools", reason: "project MCP bridge required; native tool-call receipt remains unverified" });
|
|
75
|
-
} else if (kindOf(tool) === "qwen") {
|
|
76
|
-
const mode = value("--mode") ?? "headless", model = value("--model");
|
|
77
|
-
const adapter = definitionOf(tool)!;
|
|
78
|
-
if (!(adapter.start.forms as readonly string[]).includes(mode)) throw new Error("Qwen TUI is not offered before verification");
|
|
79
|
-
for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode", "--session-id"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Qwen option");
|
|
80
|
-
const sessionId = value("--session-id");
|
|
81
|
-
if (sessionId !== undefined && !adapter.session.crashIdentity?.({ sessionId }).sessionId) throw new Error("invalid Qwen session identity");
|
|
82
|
-
if (sessionId) unresolved.push({ field: "sessionId", reason: "requested through ACP session/load at start; preview does not load or expose it" });
|
|
83
|
-
launch = adapter.start.launch({ instance: tool, form: "headless", projectRoot: cwd, stateDir, entrypoint: join(import.meta.dir, "main.js"),
|
|
84
|
-
userArgs: [], unattended: false, env: {}, preview: true, nativeCommand: nativeCommand(config.qwen_cmd, ["qwen", "--acp"]), model: model ? REDACTED : undefined });
|
|
85
|
-
unresolved.push({ field: "nativeIdentity", reason: "Qwen Code 0.25.0 identity and permission modes are checked at ACP initialization; preview starts nothing" });
|
|
86
|
-
} else if (tool === "kimi") {
|
|
87
|
-
const model = value("--model");
|
|
88
|
-
const mode = value("--mode") ?? startModeOf(config.peers, "kimi");
|
|
89
|
-
if (mode !== "tui" && mode !== "headless") throw new Error("invalid Kimi mode");
|
|
90
|
-
for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Kimi option");
|
|
91
|
-
if (mode === "headless") {
|
|
92
|
-
launch = buildKimiLaunch(nativeCommand(config.kimi_cmd, ["kimi", "acp"]), model ? REDACTED : undefined);
|
|
93
|
-
unresolved.push({ field: "sessionId", reason: "ACP session identity is available only after native initialization" });
|
|
94
|
-
} else {
|
|
95
|
-
const native = buildKimiTuiLaunch(config.kimi_cmd, model ? REDACTED : undefined);
|
|
96
|
-
launch = { ...native, cmd: native.cmd === "kimi" ? "kimi" : REDACTED, args: native.args.map(arg => arg === "--model" || arg === REDACTED ? arg : REDACTED) };
|
|
97
|
-
envNames = Object.keys(nativeLaunchEnv("kimi", childEnv())).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]);
|
|
98
|
-
}
|
|
99
|
-
const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join(import.meta.dir, "../../plugins/agent-hub/server.js"), mode);
|
|
100
|
-
kimiMetadata = { mode, mcp: { file, action, changed, tracked } };
|
|
101
|
-
} else {
|
|
102
|
-
const mode = value("--mode") ?? startModeOf(config.peers, "pi"); // #269: the same default `ahub pi` takes
|
|
103
|
-
const backend = value("--backend") ?? config.pi.backend;
|
|
104
|
-
if (!["headless", "tui"].includes(mode) || !["auto", "dgx", "mlx"].includes(backend) || (value("--session-id") && value("--session-file"))) throw new Error("invalid Pi options");
|
|
105
|
-
if (args.includes("--replace-session") && (!value("--session-file") || value("--session-id"))) throw new Error("--replace-session requires --session-file, without --session-id");
|
|
106
|
-
for (let i = 0; i < args.length; i++) {
|
|
107
|
-
if (args[i] === "--replace-session") continue;
|
|
108
|
-
if (!["--mode", "--backend", "--model", "--session-id", "--session-file"].includes(args[i]!) || !value(args[i]!)) throw new Error("unknown Pi option");
|
|
109
|
-
i++;
|
|
110
|
-
}
|
|
111
|
-
if (args.includes("--replace-session")) unresolved.push({ field: "session replacement", reason: "requires a person, a validated managed file and an offline settled owner at runtime; preview changes nothing" });
|
|
112
|
-
const models = relayModelIds({ enableHubAuto: true, mlx: config.mlx.enabled === false ? undefined : config.mlx, mlxAlias: "mlx/fast", allowedDGXmodels: { "dgx/coding": config.pi.dgx_coding, "dgx/fast": config.pi.dgx_fast } });
|
|
113
|
-
const pi = buildPiLaunch({
|
|
114
|
-
stateDir, cmd: nativeCommand(config.pi.cmd, ["pi"]), mode: mode as "headless" | "tui", backend: backend as "auto" | "dgx" | "mlx",
|
|
115
|
-
...(value("--model") ? { model: REDACTED } : {}),
|
|
116
|
-
...(value("--session-id") ? { sessionId: REDACTED } : {}),
|
|
117
|
-
...(value("--session-file") ? { sessionFile: REDACTED } : {}),
|
|
118
|
-
relay: { url: UNRESOLVED, token: UNRESOLVED, models: models.map(id => ({ id })) },
|
|
119
|
-
tools: [], preamble: REDACTED, maxSteps: config.pi.max_steps,
|
|
120
|
-
}, {}, resolve(join(import.meta.dir, "../pi/extension.ts")), UNRESOLVED, UNRESOLVED);
|
|
121
|
-
launch = pi;
|
|
122
|
-
envNames = Object.keys(pi.env).concat(["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TERM", "TERM_PROGRAM", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "CODEX_HOME"].filter(key => process.env[key] !== undefined));
|
|
123
|
-
if (mode === "tui") envNames.push("AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR");
|
|
124
|
-
unresolved.push({ field: "bridge/relay endpoints and owner credentials", reason: "assigned at runtime; preview does not bind a server or start a model" });
|
|
125
|
-
if (!value("--session-id") && !value("--session-file")) unresolved.push({ field: "sessionId", reason: "new native session identity is not allocated by preview" });
|
|
126
|
-
}
|
|
127
|
-
if (tool === "claude") envNames.push("AGENTHUB_CHANNEL");
|
|
128
|
-
if (tool === "claude" || tool === "codex") {
|
|
129
|
-
envNames.push("AGENTHUB_INSTANCE_ID", "AGENTHUB_LAUNCH_ID");
|
|
130
|
-
unresolved.push({ field: "env.AGENTHUB_INSTANCE_ID", reason: tool === "claude" ? "injected when an existing daemon instance is read at launch; preview does not read runtime state" : "injected when a verified Orca terminal launch is recorded against the daemon; preview does not record it" });
|
|
131
|
-
unresolved.push({ field: "env.AGENTHUB_LAUNCH_ID", reason: "allocated only for a verified Orca terminal launch; preview does not query Orca, allocate identity or write launch records" });
|
|
15
|
+
const selected = instanceLaunchArguments(tool, requestedInstance ? ["--as", requestedInstance, ...raw] : raw, config.instances);
|
|
16
|
+
const args = selected.args.filter(arg => !["--print-command", "--dry-run", "--json"].includes(arg));
|
|
17
|
+
const definition = definitionOf(tool), preview = builtinPreviewOf(tool);
|
|
18
|
+
if (!definition || !preview) throw new Error("native preview definition unavailable");
|
|
19
|
+
if (selected.instance && definition.start.explicitHeadlessMode) {
|
|
20
|
+
const mode = args.indexOf("--mode");
|
|
21
|
+
if (mode >= 0 && args[mode + 1] !== "headless") throw new Error("extra instances require headless mode");
|
|
22
|
+
if (mode < 0 && !args.includes("--headless")) args.push("--mode", "headless");
|
|
132
23
|
}
|
|
133
|
-
|
|
24
|
+
const result = preview({ tool, args, cwd, stateDir, cliDir: import.meta.dir, unattended, config, definition,
|
|
25
|
+
baseEnvNames: Object.keys(childEnv()).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]),
|
|
26
|
+
helpers: { buildLaunch: (native, argv, ctx) => buildLaunch(native, argv, { ...ctx, ...(selected.instance ? { instance: selected.instance } : {}) }), claudeObservationHooks, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, childEnv,
|
|
27
|
+
startModeOf, kimiMcpPlan, jcodeCommand, qwenSessionId, relayModelIds, buildPiLaunch },
|
|
28
|
+
});
|
|
29
|
+
if (!selected.instance) return result;
|
|
30
|
+
return { ...result, instance: selected.instance,
|
|
31
|
+
...(result.mcp ? { mcp: { ...result.mcp, action: "session-scoped MCP identity; shared project file unchanged", changed: false } } : {}),
|
|
32
|
+
unresolved: [...result.unresolved, { field: "executionCwd", reason: "verified by the daemon at instance start; preview does not start or authorize a worktree" }],
|
|
33
|
+
};
|
|
134
34
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { ADAPTERS, definitionOf, type AdapterDefinition, type KindMap } from "../adapters/registry.ts";
|
|
2
|
+
import { readInstanceConfig } from "../hub/instance-config.ts";
|
|
1
3
|
import { recordedFileState } from "./recovery-session-file.ts";
|
|
2
4
|
import { decodeRestoreReceipt } from "./upgrade-restore-state.ts";
|
|
3
5
|
import { shellQuote } from "./terminal-recovery.ts";
|
|
4
6
|
import type { TerminalBinding } from "./terminal-recovery.ts";
|
|
5
7
|
import type { Inspection, PlannedProject, ProjectProgress, RecoveryOperation, RecoveryPeer } from "./upgrade.ts";
|
|
6
8
|
|
|
7
|
-
export type RecoveryNativePeer =
|
|
9
|
+
export type RecoveryNativePeer = string;
|
|
8
10
|
export type RecordedSessionOverride = Pick<TerminalBinding, "sessionId" | "sessionFile">;
|
|
9
11
|
export interface RecordedPeerGuidance {
|
|
10
12
|
peer: RecoveryNativePeer;
|
|
@@ -25,28 +27,35 @@ export interface RecoveryGuidanceOptions {
|
|
|
25
27
|
|
|
26
28
|
const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
27
29
|
const argv = (...words: string[]): string => words.map(shellQuote).join(" ");
|
|
28
|
-
const
|
|
30
|
+
const projectDefinition = (peer: string, root: string) => definitionOf(peer) ?? definitionOf(peer, readInstanceConfig(root).instances);
|
|
31
|
+
const peers = (instances: KindMap | undefined): RecoveryNativePeer[] => [...ADAPTERS.map(adapter => adapter.kind), ...Object.keys(instances ?? {})]
|
|
32
|
+
.filter(peer => definitionOf(peer, instances)?.session.launchRecord)
|
|
33
|
+
.sort((a, b) => Number(definitionOf(a, instances)?.transport === "claude-channel") - Number(definitionOf(b, instances)?.transport === "claude-channel"));
|
|
29
34
|
|
|
30
|
-
function sourceSession(
|
|
31
|
-
const sessionId =
|
|
32
|
-
const sessionFile =
|
|
35
|
+
function sourceSession(adapter: AdapterDefinition, source: RecoveryPeer | undefined): { sessionId?: string; sessionFile?: string } {
|
|
36
|
+
const sessionId = source?.[adapter.session.idField];
|
|
37
|
+
const sessionFile = adapter.session.crashIdentity ? source?.sessionFile ?? source?.args?.sessionFile : undefined;
|
|
33
38
|
return { ...(text(sessionId) ? { sessionId } : {}), ...(text(sessionFile) ? { sessionFile } : {}) };
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/** Fixed person commands for the existing Pi archive exit; this helper performs no lifecycle action. */
|
|
37
|
-
export function piArchiveExit(planned: PlannedProject, stopAndArchiveCommand: string): string {
|
|
38
|
-
|
|
42
|
+
export function piArchiveExit(planned: PlannedProject, stopAndArchiveCommand: string, peer?: RecoveryNativePeer): string {
|
|
43
|
+
const adapter = peer ? projectDefinition(peer, planned.project.root) : undefined;
|
|
44
|
+
if (peer && (!adapter?.session.launchRecord || adapter.session.personFresh !== false)) throw new Error("recorded archive-exit peer has no current trusted Pi recovery declaration");
|
|
45
|
+
const instance = peer && adapter && peer !== adapter.kind ? ["--as", peer] : [];
|
|
46
|
+
return `Run ${stopAndArchiveCommand}; after stop-and-archive, run ${argv("ahub", "--project", planned.project.root, "pi", ...instance)}. A proven missing archived bookmark is retired by the normal Pi start without a fresh fallback in that same call; repeat the normal start as its notice directs.`;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/** Accepted identity outlives an incarnation's repair fence. Malformed evidence never means absence. */
|
|
42
|
-
export function resolveAcceptedBinding(planned: PlannedProject, progress: ProjectProgress, peer: RecoveryNativePeer): TerminalBinding | undefined {
|
|
50
|
+
export function resolveAcceptedBinding(planned: PlannedProject, progress: ProjectProgress, peer: RecoveryNativePeer, options: { authority?: "current" | "historical" } = {}): TerminalBinding | undefined {
|
|
43
51
|
if (progress.id !== planned.project.id) throw new Error("recorded binding project mismatch");
|
|
52
|
+
if (options.authority !== "historical" && !projectDefinition(peer, planned.project.root)?.session.launchRecord) throw new Error("recorded binding peer has no current trusted recovery declaration");
|
|
44
53
|
const binding = (value: unknown): TerminalBinding => {
|
|
45
|
-
const receipt = decodeRestoreReceipt({ state: "done", binding: value });
|
|
54
|
+
const receipt = decodeRestoreReceipt({ state: "done", binding: value }, options);
|
|
46
55
|
if (receipt?.state !== "done" || receipt.binding.peer !== peer || receipt.binding.projectRoot !== planned.project.root) throw new Error("recorded binding does not match its peer and project");
|
|
47
56
|
return receipt.binding;
|
|
48
57
|
};
|
|
49
|
-
const restored = decodeRestoreReceipt(progress.terminals[`restored:${peer}`]);
|
|
58
|
+
const restored = decodeRestoreReceipt(progress.terminals[`restored:${peer}`], options);
|
|
50
59
|
if (restored?.state === "done" || restored?.state === "waiting") return binding(restored.binding);
|
|
51
60
|
if (progress.reattachments !== undefined) {
|
|
52
61
|
if (!progress.reattachments || typeof progress.reattachments !== "object" || Array.isArray(progress.reattachments)) throw new Error("invalid recorded reattachment markers");
|
|
@@ -70,7 +79,18 @@ export function resolveAcceptedBinding(planned: PlannedProject, progress: Projec
|
|
|
70
79
|
* current receipts and the original plan. A malformed current receipt otherwise stays unavailable.
|
|
71
80
|
*/
|
|
72
81
|
export function recordedPeerGuidance(planned: PlannedProject, progress: ProjectProgress, options: RecoveryGuidanceOptions = {}): RecordedPeerGuidance[] {
|
|
73
|
-
|
|
82
|
+
let instances: KindMap | undefined;
|
|
83
|
+
try { instances = readInstanceConfig(planned.project.root).instances; }
|
|
84
|
+
catch {
|
|
85
|
+
// ponytail: failed-config guidance shows at most 64 recorded extra ids, the declaration ceiling;
|
|
86
|
+
// share an exported instance limit if that ceiling grows. These rows confer no launch authority.
|
|
87
|
+
const base = peers(undefined), recorded = [...planned.terminals.map(value => (value as { peer?: unknown })?.peer),
|
|
88
|
+
...Object.keys(progress.reattachments ?? {}), ...Object.keys(progress.terminals).filter(key => key.startsWith("restored:")).map(key => key.slice("restored:".length))];
|
|
89
|
+
const extra = [...new Set(recorded.filter((peer): peer is string => typeof peer === "string" && /^[a-z][a-z0-9-]{0,31}$/.test(peer) && !base.includes(peer)))].slice(0, 64);
|
|
90
|
+
return [...base, ...extra].map(peer => ({ peer, note: `${peer}: recorded session identity unavailable; project instance configuration could not be read; inspect the retained recovery receipt before choosing a conversation` }));
|
|
91
|
+
}
|
|
92
|
+
return peers(instances).map(peer => {
|
|
93
|
+
const adapter = definitionOf(peer, instances)!;
|
|
74
94
|
let session: { sessionId?: string; sessionFile?: string } | undefined;
|
|
75
95
|
let unavailable = false;
|
|
76
96
|
const override = options.recorded && Object.hasOwn(options.recorded, peer) ? options.recorded[peer] : undefined;
|
|
@@ -78,24 +98,25 @@ export function recordedPeerGuidance(planned: PlannedProject, progress: ProjectP
|
|
|
78
98
|
else {
|
|
79
99
|
try {
|
|
80
100
|
session = resolveAcceptedBinding(planned, progress, peer)
|
|
81
|
-
?? sourceSession(
|
|
101
|
+
?? sourceSession(adapter, planned.source.peers.find(row => row.id === peer));
|
|
82
102
|
} catch { unavailable = true; }
|
|
83
103
|
}
|
|
84
104
|
const sessionId = text(session?.sessionId) ? session.sessionId : undefined;
|
|
85
|
-
const sessionFile =
|
|
105
|
+
const sessionFile = adapter.session.crashIdentity && text(session?.sessionFile) ? session.sessionFile : undefined;
|
|
86
106
|
if (unavailable || (!sessionId && !sessionFile)) return { peer, note: `${peer}: recorded session identity unavailable; inspect the retained recovery receipt before choosing a conversation` };
|
|
87
107
|
const prefix = ["ahub", "--project", planned.project.root];
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
const tail = adapter.session.resumeArgv?.({ sessionId: sessionId ?? "", ...(sessionFile ? { sessionFile } : {}) }, false);
|
|
109
|
+
const start = adapter.cli.startWords("tui");
|
|
110
|
+
// Person guidance preserves its legacy implied startup form; terminal creation pins the explicit form.
|
|
111
|
+
const personTail = tail && start.every((word, index) => tail[index] === word) ? [tail[0]!, ...tail.slice(start.length)] : tail;
|
|
112
|
+
const instanceTail = personTail && peer !== adapter.kind ? [personTail[0]!, "--as", peer, ...personTail.slice(1)] : personTail;
|
|
113
|
+
const command = instanceTail ? argv(...prefix, ...instanceTail) : undefined;
|
|
114
|
+
let note = adapter.transport === "claude-channel"
|
|
94
115
|
? "The existing Claude channel may reconnect while its native session remains open. Transcript availability is unknown; the recorded resume command is guidance, not proof that the transcript can be resumed."
|
|
95
|
-
:
|
|
96
|
-
:
|
|
116
|
+
: adapter.session.idField === "threadId" ? `Recorded ${adapter.displayName} thread; leave the old native client before resuming this conversation.`
|
|
117
|
+
: `Recorded ${adapter.displayName} conversation; preserve this session when starting ${adapter.displayName} again.`;
|
|
97
118
|
if (sessionFile && options.stopAndArchiveCommand && recordedFileState(sessionFile) === "missing") {
|
|
98
|
-
note = `Recorded Pi session file is positively missing. Fresh-session is not supported for Pi. ${piArchiveExit(planned, options.stopAndArchiveCommand)}`;
|
|
119
|
+
note = `Recorded Pi session file is positively missing. Fresh-session is not supported for Pi. ${piArchiveExit(planned, options.stopAndArchiveCommand, peer)}`;
|
|
99
120
|
}
|
|
100
121
|
const reattachCommand = command ? options.reattachCommand?.(peer) : undefined;
|
|
101
122
|
return { peer, ...(sessionId ? { sessionId } : {}), ...(sessionFile ? { sessionFile } : {}), ...(command ? { command } : {}), ...(reattachCommand ? { reattachCommand } : {}), note };
|
package/src/cli/setup.ts
CHANGED
|
@@ -1,80 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { realPath } from "../hub/project.ts";
|
|
4
|
-
import { VERSION } from "../version.ts";
|
|
5
|
-
|
|
6
|
-
export const PLUGIN = "agent-hub@agent-hub";
|
|
7
|
-
const MARKETPLACE = "agent-hub";
|
|
8
|
-
|
|
9
|
-
export interface InstalledPlugin {
|
|
10
|
-
id: string;
|
|
11
|
-
version?: string;
|
|
12
|
-
installPath?: string;
|
|
13
|
-
}
|
|
14
|
-
export interface Marketplace {
|
|
15
|
-
name: string;
|
|
16
|
-
path?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** `claude plugin list --json` / `claude plugin marketplace list --json`. Anything that does not parse is an empty list. */
|
|
20
|
-
export function parseList<T>(json: string): T[] {
|
|
21
|
-
try {
|
|
22
|
-
const d = JSON.parse(json);
|
|
23
|
-
return Array.isArray(d) ? d : [];
|
|
24
|
-
} catch {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const real = (p: string) => {
|
|
30
|
-
try {
|
|
31
|
-
return realPath(p);
|
|
32
|
-
} catch {
|
|
33
|
-
return p;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const read = (p: string) => {
|
|
37
|
-
try {
|
|
38
|
-
return readFileSync(p, "utf8");
|
|
39
|
-
} catch {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export type PluginState = { state: "missing" } | { state: "current"; version: string } | { state: "stale"; version: string; why: string };
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Is the installed Claude plugin the one this hub ships? The version is compared, and so is the bundle itself: Claude
|
|
48
|
-
* Code runs a cached copy, and a copy taken from an earlier build of the same version speaks an older wire protocol.
|
|
49
|
-
*/
|
|
50
|
-
export function pluginState(plugins: InstalledPlugin[], packageRoot: string): PluginState {
|
|
51
|
-
const mine = plugins.find((p) => p.id === PLUGIN);
|
|
52
|
-
if (!mine) return { state: "missing" };
|
|
53
|
-
const version = mine.version ?? "unknown";
|
|
54
|
-
if (version !== VERSION) return { state: "stale", version, why: `version ${version}, this hub is ${VERSION}` };
|
|
55
|
-
const shipped = read(join(packageRoot, "plugins", "agent-hub", "server.js"));
|
|
56
|
-
const cached = mine.installPath ? read(join(mine.installPath, "server.js")) : undefined;
|
|
57
|
-
if (shipped !== undefined && cached !== undefined && shipped !== cached) return { state: "stale", version, why: "same version, but the cached bundle differs from the one this hub ships" };
|
|
58
|
-
return { state: "current", version };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface SetupStep {
|
|
62
|
-
why: string;
|
|
63
|
-
argv: string[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The next thing `ahub setup` has to do, or nothing. One step at a time: the state is read again after every step,
|
|
68
|
-
* because what a step leaves behind is Claude Code's business (removing a marketplace may uninstall its plugins).
|
|
69
|
-
*/
|
|
70
|
-
export function nextStep(plugins: InstalledPlugin[], marketplaces: Marketplace[], packageRoot: string): SetupStep | undefined {
|
|
71
|
-
const root = real(packageRoot);
|
|
72
|
-
const market = marketplaces.find((m) => m.name === MARKETPLACE);
|
|
73
|
-
if (market && real(market.path ?? "") !== root) return { why: `the ${MARKETPLACE} marketplace points at ${market.path ?? "another source"}, not at this package`, argv: ["claude", "plugin", "marketplace", "remove", MARKETPLACE] };
|
|
74
|
-
if (!market) return { why: "register this package as a Claude Code plugin marketplace", argv: ["claude", "plugin", "marketplace", "add", root] };
|
|
75
|
-
const state = pluginState(plugins, root);
|
|
76
|
-
if (state.state === "missing") return { why: "install the channel plugin", argv: ["claude", "plugin", "install", PLUGIN] };
|
|
77
|
-
// Uninstall rather than update: with an unchanged version number an update has nothing to do.
|
|
78
|
-
if (state.state === "stale") return { why: `replace the installed plugin (${state.why})`, argv: ["claude", "plugin", "uninstall", PLUGIN] };
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
1
|
+
/** Compatibility exports; native plugin policy belongs to its adapter definition leaf. */
|
|
2
|
+
export { PLUGIN, parseList, pluginState, nextStep, type InstalledPlugin, type Marketplace, type PluginState, type SetupStep } from "../adapters/definitions/native-cli-claude-setup.ts";
|