@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -3
  3. package/docs/agent-notes/adapters.md +9 -3
  4. package/docs/agent-notes/budget.md +7 -3
  5. package/docs/agent-notes/bus.md +2 -1
  6. package/docs/agent-notes/daemon.md +7 -3
  7. package/docs/agent-notes/local-worker.md +1 -1
  8. package/docs/agent-notes/tasks.md +6 -2
  9. package/docs/agent-notes/tests.md +2 -1
  10. package/docs/gsd-collaboration-analysis.md +5 -1
  11. package/docs/operations.md +80 -16
  12. package/docs/quickstart.md +10 -2
  13. package/docs/sdlc.md +59 -34
  14. package/docs/security.md +7 -4
  15. package/docs/smoke.md +1 -1
  16. package/docs/specs/2026-09-19-agent-hub-design.md +407 -10
  17. package/docs/verification/2026-10-18-0.12.34.md +48 -0
  18. package/docs/verification/2026-10-18-0.12.35.md +49 -0
  19. package/docs/verification/2026-10-18-493-claude-busy-channel.md +91 -0
  20. package/docs/verified.json +35 -19
  21. package/package.json +1 -1
  22. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  23. package/plugins/agent-hub/server.js +23567 -16606
  24. package/src/adapters/acp.ts +3 -1
  25. package/src/adapters/claude-channel.ts +59 -29
  26. package/src/adapters/codex-appserver.ts +82 -12
  27. package/src/adapters/definitions/builtin-metadata.ts +101 -7
  28. package/src/adapters/definitions/native-cli-claude-launch.ts +90 -0
  29. package/src/adapters/definitions/native-cli-claude-setup.ts +80 -0
  30. package/src/adapters/definitions/native-cli-codex-launch.ts +14 -0
  31. package/src/adapters/definitions/native-cli-doctor.ts +46 -0
  32. package/src/adapters/definitions/native-cli-entry-claude.ts +52 -0
  33. package/src/adapters/definitions/native-cli-entry-codex.ts +34 -0
  34. package/src/adapters/definitions/native-cli-entry-jcode.ts +27 -0
  35. package/src/adapters/definitions/native-cli-entry-kimi.ts +57 -0
  36. package/src/adapters/definitions/native-cli-entry-local.ts +15 -0
  37. package/src/adapters/definitions/native-cli-entry-pi.ts +53 -0
  38. package/src/adapters/definitions/native-cli-entry-qwen.ts +21 -0
  39. package/src/adapters/definitions/native-cli-entry-types.ts +41 -0
  40. package/src/adapters/definitions/native-cli-entry.ts +19 -0
  41. package/src/adapters/definitions/native-cli-jcode-mcp.ts +58 -0
  42. package/src/adapters/definitions/native-cli-launch-common.ts +17 -0
  43. package/src/adapters/definitions/native-cli-pi.ts +33 -0
  44. package/src/adapters/definitions/native-cli-preview-claude.ts +37 -0
  45. package/src/adapters/definitions/native-cli-preview-codex.ts +20 -0
  46. package/src/adapters/definitions/native-cli-preview-common.ts +57 -0
  47. package/src/adapters/definitions/native-cli-preview-jcode.ts +20 -0
  48. package/src/adapters/definitions/native-cli-preview-kimi.ts +28 -0
  49. package/src/adapters/definitions/native-cli-preview-pi.ts +36 -0
  50. package/src/adapters/definitions/native-cli-preview-qwen.ts +22 -0
  51. package/src/adapters/definitions/native-cli-preview.ts +16 -0
  52. package/src/adapters/definitions/native-config-defaults.ts +19 -0
  53. package/src/adapters/definitions/native-config-policy.ts +35 -0
  54. package/src/adapters/definitions/native-context.ts +35 -0
  55. package/src/adapters/definitions/native-daemon-kimi.ts +5 -0
  56. package/src/adapters/definitions/native-daemon-policy.ts +18 -0
  57. package/src/adapters/definitions/native-display.ts +18 -0
  58. package/src/adapters/definitions/native-factories.ts +48 -0
  59. package/src/adapters/definitions/native-policy-aux.ts +130 -0
  60. package/src/adapters/definitions/native-quota.ts +29 -0
  61. package/src/adapters/definitions/native-recovery-continuity.ts +12 -0
  62. package/src/adapters/definitions/native-recovery.ts +88 -0
  63. package/src/adapters/definitions/qwen-native.ts +2 -2
  64. package/src/adapters/definitions/qwen.ts +8 -4
  65. package/src/adapters/local-worker.ts +3 -2
  66. package/src/adapters/pi.ts +3 -1
  67. package/src/adapters/registry-types.ts +100 -10
  68. package/src/adapters/registry.ts +127 -11
  69. package/src/cli/console-state.ts +5 -5
  70. package/src/cli/identity.ts +8 -7
  71. package/src/cli/jcode-mcp.ts +2 -58
  72. package/src/cli/launch.ts +41 -102
  73. package/src/cli/main.ts +126 -290
  74. package/src/cli/output.ts +3 -2
  75. package/src/cli/preview.ts +26 -126
  76. package/src/cli/recovery-guidance.ts +43 -22
  77. package/src/cli/setup.ts +2 -80
  78. package/src/cli/shell-quote.ts +3 -0
  79. package/src/cli/terminal-recovery.ts +81 -22
  80. package/src/cli/upgrade-interactive.ts +4 -2
  81. package/src/cli/upgrade-restore-state.ts +17 -7
  82. package/src/cli/upgrade-runtime.ts +126 -147
  83. package/src/cli/upgrade.ts +44 -27
  84. package/src/hub/budget.ts +59 -31
  85. package/src/hub/bus.ts +1 -1
  86. package/src/hub/conductor.ts +19 -9
  87. package/src/hub/config-apply.ts +22 -15
  88. package/src/hub/config-reload.ts +18 -3
  89. package/src/hub/config-trust.ts +1 -1
  90. package/src/hub/context-window.ts +3 -27
  91. package/src/hub/control-client.ts +1 -1
  92. package/src/hub/crash.ts +15 -39
  93. package/src/hub/daemon.ts +506 -341
  94. package/src/hub/execution-budget.ts +3 -2
  95. package/src/hub/hub-tools.ts +19 -8
  96. package/src/hub/instance-budget.ts +73 -0
  97. package/src/hub/instance-config.ts +47 -0
  98. package/src/hub/instance-execution.ts +50 -0
  99. package/src/hub/instances.ts +100 -0
  100. package/src/hub/lifecycle-schema.ts +7 -3
  101. package/src/hub/peer-catalog.ts +4 -13
  102. package/src/hub/permission-mode.ts +14 -17
  103. package/src/hub/recovery-continuity.ts +6 -5
  104. package/src/hub/report.ts +9 -8
  105. package/src/hub/roster.ts +9 -9
  106. package/src/hub/routing.ts +94 -19
  107. package/src/hub/sdlc-queue.ts +2 -2
  108. package/src/hub/sdlc.ts +84 -38
  109. package/src/hub/settings.ts +39 -10
  110. package/src/hub/settled-roles.ts +1 -1
  111. package/src/hub/snapshots.ts +40 -9
  112. package/src/hub/start-mode.ts +13 -8
  113. package/src/hub/status-roster.ts +8 -0
  114. package/src/hub/tasks.ts +60 -27
  115. package/src/hub/text-projection.ts +7 -3
  116. package/src/hub/tooling.ts +4 -60
  117. package/src/hub/ui.ts +3 -1
  118. package/src/hub/workflow.ts +30 -52
  119. package/src/memory/recall.ts +4 -5
  120. package/src/models/route/signals.ts +3 -1
  121. package/src/pi/launch.ts +5 -3
  122. package/src/ui/index.html +4 -3
  123. package/src/ui/office-model.ts +3 -1
  124. package/src/ui/peer-palette.ts +16 -6
  125. package/templates/AGENTS.block.md +2 -1
  126. package/templates/routing.toml +2 -2
@@ -0,0 +1,90 @@
1
+ import { mkdirSync, writeFileSync, renameSync, unlinkSync, readFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { resolve, join } from "node:path";
4
+ import { CLAUDE_CHANNEL, sessionSettings, UNATTENDED_WARNING, type Launch } from "../../cli/launch.ts";
5
+ import type { LaunchContext, LaunchPlan } from "../registry-types.ts";
6
+ import { nativeLaunchArguments, nativeLaunchPlan } from "./native-cli-launch-common.ts";
7
+
8
+ export const CLAUDE_LAUNCH_POLICY = Object.freeze({ hubSwitches: Object.freeze(["--unattended", "--safe", "--new"]), ownedFlags: Object.freeze(["--dangerously-load-development-channels", "--dangerously-skip-permissions"]), unattended: true, passthrough: true });
9
+
10
+ function claudeChannel(userArgs: string[]): string {
11
+ const bundle = resolve(import.meta.dir, "../../../plugins/agent-hub/server.js");
12
+ for (let i = 0; i < userArgs.length; i++) {
13
+ const arg = userArgs[i]!;
14
+ const path = arg === "--mcp-config" ? userArgs[i + 1] : arg.startsWith("--mcp-config=") ? arg.slice("--mcp-config=".length) : undefined;
15
+ if (!path) continue;
16
+ try {
17
+ const server = JSON.parse(readFileSync(resolve(path), "utf8")).mcpServers?.["agent-hub"];
18
+ if (server?.command === "bun" && Array.isArray(server.args) && server.args.some((value: unknown) => typeof value === "string" && resolve(value) === bundle)) return "server:agent-hub";
19
+ } catch { /* The native CLI reports invalid user-supplied MCP configuration. */ }
20
+ }
21
+ return CLAUDE_CHANNEL;
22
+ }
23
+
24
+ export function claudeNativeLaunch(ctx: LaunchContext): LaunchPlan {
25
+ const { passthrough, unattended } = nativeLaunchArguments("claude", ctx, CLAUDE_LAUNCH_POLICY);
26
+ const build = (): Launch => {
27
+ // `--settings` takes one value, so a user-supplied one wins and Claude has no quota source for that session.
28
+ const own = passthrough.some((a) => a === "--settings" || a.startsWith("--settings="));
29
+ let settingsArgs = ctx.statusLine && !own ? ["--settings", sessionSettings(ctx.statusLine, ctx.facts)] : [];
30
+ let nativeArgs = passthrough;
31
+ let settingsFile: string | undefined;
32
+ if (own) {
33
+ if (passthrough.filter(a => a === "--settings" || a.startsWith("--settings=")).length > 1) throw new Error("pass --settings only once so the hub permission hook stays installed");
34
+ const index = passthrough.findIndex(a => a === "--settings" || a.startsWith("--settings="));
35
+ const arg = passthrough[index]!;
36
+ const value = arg === "--settings" ? passthrough[index + 1] : arg.slice("--settings=".length);
37
+ if (!value) throw new Error("--settings requires a JSON object or readable file so the hub permission hook can be installed");
38
+ let caller: Record<string, unknown>;
39
+ try {
40
+ caller = JSON.parse(value.trim().startsWith("{") ? value : readFileSync(resolve(value), "utf8"));
41
+ if (!caller || typeof caller !== "object" || Array.isArray(caller)) throw new Error("not an object");
42
+ } catch { throw new Error("cannot read --settings as a JSON object; the hub permission hook must be installed"); }
43
+ if (ctx.facts && caller.disableAllHooks === true) throw new Error("--settings disableAllHooks prevents the required hub permission hook; enable hooks to launch");
44
+ const injected = JSON.parse(sessionSettings(undefined, ctx.facts));
45
+ const callerHooks = caller.hooks;
46
+ if (callerHooks !== undefined && (!callerHooks || typeof callerHooks !== "object" || Array.isArray(callerHooks))) throw new Error("--settings hooks must be an object");
47
+ const hooks = { ...(callerHooks as Record<string, unknown> | undefined) };
48
+ for (const [event, entries] of Object.entries(injected.hooks ?? {})) {
49
+ const existing = hooks[event];
50
+ if (existing !== undefined && !Array.isArray(existing)) throw new Error(`--settings ${event} hooks must be an array`);
51
+ hooks[event] = [...(existing as unknown[] | undefined ?? []), ...(entries as unknown[])];
52
+ }
53
+ settingsArgs = ["--settings", JSON.stringify({ ...caller, ...(ctx.facts ? { hooks } : {}) })];
54
+ nativeArgs = passthrough.filter((_, i) => i !== index && (arg !== "--settings" || i !== index + 1));
55
+ } else if (ctx.facts && !ctx.statusLine) {
56
+ const injected = JSON.parse(sessionSettings(undefined, ctx.facts));
57
+ settingsArgs = ["--settings", JSON.stringify({ hooks: injected.hooks })];
58
+ }
59
+ if (settingsArgs.length && !ctx.preview) {
60
+ const stateDir = ctx.facts?.stateDir ?? ctx.statusLine?.stateDir;
61
+ if (!stateDir) throw new Error("Claude session settings require the hub state directory");
62
+ const file = join(stateDir, `claude-settings-${randomUUID()}.json`);
63
+ try {
64
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
65
+ writeFileSync(`${file}.tmp`, settingsArgs[1]!, { mode: 0o600, flag: "wx" });
66
+ renameSync(`${file}.tmp`, file);
67
+ } catch {
68
+ try { unlinkSync(`${file}.tmp`); } catch { /* no temp file was written */ }
69
+ throw new Error("cannot write private Claude session settings in the hub state directory");
70
+ }
71
+ settingsArgs[1] = file;
72
+ settingsFile = file;
73
+ }
74
+ const notes = [
75
+ unattended ? UNATTENDED_WARNING : "",
76
+ ctx.statusLine && own ? "note: you passed --settings, so the hub's status line tee is off and the budget coordinator cannot see Claude's quota (ahub budget set claude <0..1> still works)." : "",
77
+ ].filter(Boolean);
78
+ return {
79
+ cmd: "claude",
80
+ args: ["--dangerously-load-development-channels", claudeChannel(passthrough), ...(unattended ? ["--dangerously-skip-permissions"] : []), ...settingsArgs, ...nativeArgs],
81
+ ...(settingsFile ? { settingsFile } : {}),
82
+ permissionHook: !!ctx.facts,
83
+ ...(ctx.facts ? { hookPurpose: ctx.facts.purpose ?? "facts" } : {}),
84
+ unattended,
85
+ ...(notes.length ? { warning: notes.join("\n") } : {}),
86
+ };
87
+
88
+ };
89
+ return nativeLaunchPlan(build(), ctx);
90
+ }
@@ -0,0 +1,80 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
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
+ }
@@ -0,0 +1,14 @@
1
+ import type { LaunchContext, LaunchPlan } from "../registry-types.ts";
2
+ import { nativeLaunchArguments, nativeLaunchPlan } from "./native-cli-launch-common.ts";
3
+
4
+ export const CODEX_LAUNCH_POLICY = Object.freeze({ hubSwitches: Object.freeze(["--unattended", "--safe", "--new"]), ownedFlags: Object.freeze(["--remote", "--yolo", "--dangerously-bypass-approvals-and-sandbox"]), unattended: true, passthrough: true });
5
+ export function codexNativeLaunch(ctx: LaunchContext): LaunchPlan {
6
+ const { passthrough, unattended, warning } = nativeLaunchArguments("codex", ctx, CODEX_LAUNCH_POLICY);
7
+ if (!ctx.proxyUrl) throw new Error("codex proxy url is missing");
8
+ return nativeLaunchPlan({
9
+ cmd: ctx.codexBin ?? "codex",
10
+ args: ["--enable", "tui_app_server", "--remote", ctx.proxyUrl, ...(unattended ? ["--dangerously-bypass-approvals-and-sandbox"] : []), ...passthrough],
11
+ unattended,
12
+ ...warning,
13
+ }, ctx);
14
+ }
@@ -0,0 +1,46 @@
1
+ import type { DoctorCheck } from "../../cli/output.ts";
2
+
3
+ // Fixed builtin doctor policy only. These projections neither execute probes nor extend AdapterDefinition.
4
+ const nativeTools = [
5
+ { id: "claude", initial: true }, { id: "codex", initial: true }, { id: "kimi", initial: true },
6
+ { id: "qwen", initial: false }, { id: "jcode", initial: false },
7
+ ] as const;
8
+ export const DOCTOR_TOOL_PROBES: readonly string[] = Object.freeze(["bun", ...nativeTools.filter(tool => tool.initial).map(tool => tool.id)]);
9
+ export const DOCTOR_MEMORY_CAPTURE_PEERS = Object.freeze(["codex", "kimi"] as const);
10
+ export const DOCTOR_PLUGIN_PROBE = Object.freeze({ command: "claude", argv: Object.freeze(["plugin", "list", "--json"] as const), name: "claude plugin" });
11
+
12
+ /** Exact legacy section and severity classification, including non-native and unknown diagnostic names. */
13
+ export function nativeDoctorCheck(ok: boolean | undefined, name: string, detail: string): DoctorCheck {
14
+ const nativeTool = nativeTools.some(tool => tool.id === name);
15
+ const section: DoctorCheck["section"] = name === "bun" || nativeTool ? "Tools"
16
+ : name === "config" || name === "retired setting" ? "Config"
17
+ : name.startsWith("memory") ? "Memory"
18
+ : ["omniroute", "omniroute key", "local fixed_model", "switchyard", "pi mlx"].includes(name) ? "Models" : "Hub";
19
+ const optional = nativeTool || ["ahub daemon", "switchyard", "pi mlx"].includes(name) || section === "Memory";
20
+ const level: DoctorCheck["level"] = ok === undefined ? (["switchyard", "omniroute"].includes(name) ? "warn" : "unknown") : ok ? "ok" : optional ? "warn" : "fail";
21
+ return { section, level, name, detail };
22
+ }
23
+
24
+ /** Recovery IDs are checked only for the two legacy native conversation surfaces. */
25
+ export function nativeDoctorRecoveryIdentity(peer: { id: string; state: string; sessionId?: string; threadId?: string }): { ok: boolean; name: string; detail: string } | undefined {
26
+ const fields = { claude: "sessionId", codex: "threadId" } as const;
27
+ const field = Object.hasOwn(fields, peer.id) ? fields[peer.id as keyof typeof fields] : undefined;
28
+ if (!field || peer.state === "offline") return undefined;
29
+ const identity = peer[field];
30
+ return { ok: !!identity, name: `${peer.id} recovery ID`, detail: identity ? "recorded" : `missing; verify the current conversation and reconnect with ahub ${peer.id} before upgrading` };
31
+ }
32
+
33
+ export type NativeDoctorOptionalProbe =
34
+ | { kind: "source-identity"; name: string; command: string[]; detail: string }
35
+ | { kind: "pinned-version"; name: string; command: string[]; expected: string; confirmed: string; unconfirmed: string };
36
+
37
+ /** The host runs these existing checks in order; no process, filesystem or caller callback enters this planner. */
38
+ export function nativeDoctorOptionalProbes(config: { roster: Readonly<Record<string, boolean | undefined>>; jcode_cmd: string[]; qwen_cmd: string[] }): NativeDoctorOptionalProbe[] {
39
+ const probes: NativeDoctorOptionalProbe[] = [];
40
+ if (config.roster.jcode === true) probes.push({ kind: "source-identity", name: "jcode", command: config.jcode_cmd,
41
+ detail: "CLI/source identity only; provider, project MCP and native teardown require separate receipts; native policy unmanaged" });
42
+ if (config.roster.qwen === true) probes.push({ kind: "pinned-version", name: "qwen", command: config.qwen_cmd, expected: "0.25.0",
43
+ confirmed: "CLI reports 0.25.0; native ACP identity/modes and provider access require a separate live check",
44
+ unconfirmed: "Qwen Code 0.25.0 executable not confirmed; provider and ACP readiness unknown" });
45
+ return probes;
46
+ }
@@ -0,0 +1,52 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import type { ClaudeLaunchRecord } from "../../cli/launch.ts";
5
+ import type { NativeCliEntryContext } from "./native-cli-entry-types.ts";
6
+
7
+ export async function runClaudeEntry(ctx: NativeCliEntryContext): Promise<void> {
8
+ const { args, cwd, stateDir, cliDir, unattendedEnv, projectConfig, fail } = ctx;
9
+ const { launcherPreview, readControl, recordTerminalLaunch, claudeObservationHooks, buildLaunch, runClaudeLaunch, processSignature, recordClaudeLaunch, cleanupStaleClaudeSettings, nativeLaunchEnv, childEnv, cleanupClaudeSettings } = ctx.helpers;
10
+ if (args.includes("--print-command") || args.includes("--dry-run")) {
11
+ try { return console.log(JSON.stringify(launcherPreview("claude", args, cwd, stateDir, unattendedEnv), null, 2)); }
12
+ catch { fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)"); }
13
+ }
14
+ const control = readControl(stateDir);
15
+ if (control?.instanceId) {
16
+ process.env.AGENTHUB_INSTANCE_ID = control.instanceId;
17
+ const terminal = await recordTerminalLaunch("claude", cwd, stateDir, control.instanceId);
18
+ if (!terminal) process.env.AGENTHUB_LAUNCH_ID = randomUUID();
19
+ }
20
+ // `--settings` outranks project and user settings, so the tee has to wrap whichever status line would have won:
21
+ // project local, then project, then user.
22
+ let original: { command?: string; refreshInterval?: number; padding?: number } | undefined;
23
+ for (const file of [join(cwd, ".claude", "settings.local.json"), join(cwd, ".claude", "settings.json"), join(process.env.HOME ?? "", ".claude", "settings.json")]) {
24
+ try {
25
+ original ??= JSON.parse(readFileSync(file, "utf8")).statusLine;
26
+ } catch {
27
+ // no such file, or no status line in it
28
+ }
29
+ }
30
+ // Turn-free facts and opted-in task sweeps share native PreToolUse/PostToolUse/Stop observations.
31
+ const facts = claudeObservationHooks(projectConfig(), { script: join(cliDir, "facts-hook.ts"), stateDir });
32
+ let previousLaunch: unknown;
33
+ try { previousLaunch = JSON.parse(readFileSync(join(stateDir, "claude-launch.json"), "utf8")); } catch { /* no previous managed launch */ }
34
+ const launch = buildLaunch("claude", args, { unattended: unattendedEnv, statusLine: { script: join(cliDir, "statusline-tee.ts"), stateDir, ...(original ? { original } : {}) }, ...(facts ? { facts } : {}) });
35
+ let result: Awaited<ReturnType<typeof runClaudeLaunch>>;
36
+ try {
37
+ let launchRecord: ClaudeLaunchRecord | undefined;
38
+ if (control?.instanceId) {
39
+ // Publish only the final launch's installed hook and effective permission flags.
40
+ launchRecord = { instanceId: control.instanceId, launchId: process.env.AGENTHUB_LAUNCH_ID!, permissionHook: launch.permissionHook === true, hookPurpose: launch.hookPurpose, settingsFile: launch.settingsFile, launcherPid: process.pid, launcherSignature: processSignature(process.pid), unattended: launch.unattended === true };
41
+ recordClaudeLaunch(stateDir, launchRecord);
42
+ cleanupStaleClaudeSettings(stateDir, previousLaunch);
43
+ }
44
+ if (launch.warning) console.error(launch.warning);
45
+ const env = nativeLaunchEnv("claude", childEnv());
46
+ result = await runClaudeLaunch(launch, { cwd, stateDir, env: { ...env, AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd }, ...(launchRecord ? { record: launchRecord } : {}) });
47
+ } finally {
48
+ cleanupClaudeSettings(stateDir, launch.settingsFile);
49
+ }
50
+ if (result.error) fail(`cannot run ${launch.cmd}: ${result.error.message}`);
51
+ process.exit(result.status ?? 1);
52
+ }
@@ -0,0 +1,34 @@
1
+ import type { NativeCliEntryContext } from "./native-cli-entry-types.ts";
2
+
3
+ export async function runCodexEntry(ctx: NativeCliEntryContext): Promise<void> {
4
+ const peer = ctx.instance ?? "codex";
5
+ const { args, cwd, stateDir, unattendedEnv, projectConfig, connect, fail, exec } = ctx;
6
+ const { launcherPreview, buildLaunch, recoveryLock, readControl, recordTerminalLaunch } = ctx.helpers;
7
+ if (args.includes("--print-command") || args.includes("--dry-run")) {
8
+ try { return console.log(JSON.stringify(launcherPreview("codex", args, cwd, stateDir, unattendedEnv), null, 2)); }
9
+ catch { fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)"); }
10
+ }
11
+ const launch0 = buildLaunch("codex", args, { unattended: unattendedEnv, instance: peer, proxyUrl: "pending" }); // refuse bad flags before starting anything
12
+ // #215: a launch by a recovery operation records itself before the hub round trip, so the coordinator never takes
13
+ // a launcher still starting for one that never ran. An ordinary launch records only once the hub accepted it: a
14
+ // refused start must not replace the record of a Codex already running here (one row per peer and instance).
15
+ const recovering = !!process.env.AGENTHUB_RECOVERY_OPERATION && process.env.AGENTHUB_RECOVERY_OPERATION === recoveryLock(); // not an id a restored session inherited
16
+ const before = readControl(stateDir);
17
+ // ponytail: recovering extras have no authenticated execution-root binding before start replies, so their row
18
+ // is recorded afterward. The coordinator retains its pending creation/wait evidence; add pre-start recording
19
+ // only when a future recovery receipt supplies independently verified exact-instance execution-root evidence.
20
+ if (recovering && !ctx.instance && before?.instanceId) await recordTerminalLaunch(peer, cwd, stateDir, before.instanceId);
21
+ const hub = await connect();
22
+ const res = await hub.request({ t: "start", peer, args: { unattended: launch0.unattended === true }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
23
+ hub.close();
24
+ if (!res.ok) fail(res.error);
25
+ if (ctx.instance) {
26
+ if (typeof res.executionCwd !== "string" || !res.executionCwd.startsWith("/")) fail("instance execution directory was not verified");
27
+ ctx.executionCwd = res.executionCwd;
28
+ }
29
+ const control = readControl(stateDir);
30
+ if ((!recovering || ctx.instance) && control?.instanceId) await recordTerminalLaunch(peer, cwd, stateDir, control.instanceId, ctx.instance ? { executionCwd: ctx.executionCwd } : undefined);
31
+ const launch = buildLaunch("codex", args, { unattended: unattendedEnv, instance: peer, proxyUrl: res.proxyUrl, codexBin: projectConfig().codex_bin });
32
+ if (launch0.warning) console.error(launch0.warning);
33
+ exec(launch.cmd, launch.args, "codex");
34
+ }
@@ -0,0 +1,27 @@
1
+ import { resolve } from "node:path";
2
+ import type { NativeCliEntryContext } from "./native-cli-entry-types.ts";
3
+ import { rosterInStatus } from "../../hub/status-roster.ts";
4
+
5
+ export async function runJcodeEntry(ctx: NativeCliEntryContext): Promise<void> {
6
+ const { args, cwd, stateDir, cliDir, unattendedEnv, connect, fail, takeFlags } = ctx;
7
+ const { launcherPreview, qwenSessionId, configureJcodeMcp } = ctx.helpers;
8
+ if (args.includes("--print-command") || args.includes("--dry-run")) {
9
+ try { return console.log(JSON.stringify(launcherPreview("jcode", args, cwd, stateDir, unattendedEnv), null, 2)); }
10
+ catch { fail("cannot preview Jcode launch: invalid arguments or configuration (details withheld)"); }
11
+ }
12
+ const { one, rest } = takeFlags(args, ["--mode", "--model", "--provider-profile", "--session-id"], []);
13
+ if (rest.length || (one["--mode"] !== undefined && one["--mode"] !== "headless")) fail("usage: ahub jcode [--mode headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>]");
14
+ if (one["--session-id"] !== undefined && !qwenSessionId(one["--session-id"])) fail("invalid Jcode session identity");
15
+ const hub = await connect();
16
+ try {
17
+ const snapshot = (await hub.request({ t: "status" })).status;
18
+ const rostered = rosterInStatus(snapshot, "jcode");
19
+ if (rostered === undefined) fail("cannot read jcode's roster membership from the running hub; upgrade or restart it, then retry (nothing was started)");
20
+ if (!rostered) fail("jcode is off roster; add it with ahub roster add jcode");
21
+ const written = configureJcodeMcp(cwd, stateDir, resolve(cliDir, "../../plugins/agent-hub/server.js"));
22
+ if (written.tracked) console.error(".jcode/mcp.json is tracked; review this local machine-path change before committing it");
23
+ const reply = await hub.request({ t: "start", peer: "jcode", args: { mode: "headless", model: one["--model"], providerProfile: one["--provider-profile"], sessionId: one["--session-id"] }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 65_000);
24
+ if (!reply.ok) fail(reply.error);
25
+ console.log(reply.already ? "jcode is already attached" : "jcode attached (headless); native policy unmanaged, NO hub sandbox");
26
+ } finally { hub.close(); }
27
+ }
@@ -0,0 +1,57 @@
1
+ import { resolve } from "node:path";
2
+ import { spawnSync } from "node:child_process";
3
+ import type { NativeCliEntryContext } from "./native-cli-entry-types.ts";
4
+ import { rosterInStatus } from "../../hub/status-roster.ts";
5
+
6
+ export async function runKimiEntry(ctx: NativeCliEntryContext): Promise<void> {
7
+ const peer = ctx.instance ?? "kimi";
8
+ const { args, cwd, stateDir, cliDir, unattendedEnv, projectConfig, connect, fail, takeFlags } = ctx;
9
+ const { launcherPreview, startModeOf, buildKimiTuiLaunch, configureKimiMcp, nativeLaunchEnv, childEnv } = ctx.helpers;
10
+ if (args.includes("--print-command") || args.includes("--dry-run")) {
11
+ try { return console.log(JSON.stringify(launcherPreview("kimi", args, cwd, stateDir, unattendedEnv), null, 2)); }
12
+ catch { fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)"); }
13
+ }
14
+ const { one, rest } = takeFlags(args, ["--mode", "--model"], []);
15
+ const mode = one["--mode"] ?? (ctx.instance ? "headless" : startModeOf(projectConfig().peers, "kimi")), model = one["--model"];
16
+ if (ctx.instance && mode !== "headless") fail("extra Kimi instances require headless mode");
17
+ if (rest.length || !["tui", "headless"].includes(mode)) fail("usage: ahub kimi [--mode tui|headless] [--model <alias>]");
18
+ if (mode === "tui" && one["--mode"] === undefined && !(process.stdin.isTTY && process.stdout.isTTY)) {
19
+ if (model) fail("Kimi's TUI needs a terminal for these options; run in a terminal or add --mode headless");
20
+ const hub = await connect();
21
+ try {
22
+ const reply = await hub.request({ t: "peer_start", peer }, 35_000);
23
+ if (!reply.ok) fail(reply.error);
24
+ return console.log(reply.already ? "kimi is already attached" : `kimi: opened a terminal running ${reply.command}; it attaches pull-only when its TUI is ready`);
25
+ } finally { hub.close(); }
26
+ }
27
+ const hub = await connect();
28
+ try {
29
+ if (mode === "tui") {
30
+ const launch = buildKimiTuiLaunch(projectConfig().kimi_cmd, model);
31
+ const snapshot = (await hub.request({ t: "status" })).status;
32
+ const rostered = rosterInStatus(snapshot, peer);
33
+ if (rostered === undefined) fail("cannot read kimi's roster membership from the running hub; upgrade or restart it, then retry (nothing was started)");
34
+ if (!rostered) fail("kimi is off roster; add it with ahub roster add kimi");
35
+ const owner = snapshot?.peers?.[peer];
36
+ if (owner && !["offline"].includes(owner.state) && !owner.pullOnly) fail("Kimi is running headless; run ahub stop kimi before starting its TUI");
37
+ const admission = await hub.request({ t: "start", peer, args: { mode: "tui" }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
38
+ if (!admission.ok) fail(admission.error);
39
+ let result: ReturnType<typeof spawnSync>;
40
+ try {
41
+ const written = configureKimiMcp(cwd, stateDir, resolve(cliDir, "../../plugins/agent-hub/server.js"), "tui");
42
+ if (written.tracked) console.error(".kimi-code/mcp.json is tracked; the agent-hub entry contains machine paths. Review this local change before committing it.");
43
+ result = spawnSync(launch.cmd, launch.args, { cwd, stdio: "inherit", env: { ...nativeLaunchEnv("kimi", childEnv()), AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } });
44
+ } finally {
45
+ if (admission.admissionId) await hub.request({ t: "kimi_launch_end", admissionId: admission.admissionId }).catch(() => {});
46
+ }
47
+ if (result.error) fail(`cannot run ${launch.cmd}: ${result.error.message}`);
48
+ process.exit(result.status ?? 1);
49
+ }
50
+ const owner = (await hub.request({ t: "status" })).status?.peers?.[peer];
51
+ if (owner?.pullOnly && owner.state !== "offline") fail("Kimi runs in a terminal; end it there before starting a headless one");
52
+ if (!ctx.instance) configureKimiMcp(cwd, stateDir, resolve(cliDir, "../../plugins/agent-hub/server.js"), "headless");
53
+ const res = await hub.request({ t: "start", peer, args: { mode: "headless", model }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
54
+ if (!res.ok) fail(res.error);
55
+ console.log(res.already ? `${peer} is already attached` : `${peer} attached (headless). Talk to it with: ahub say @${peer} "..."`);
56
+ } finally { hub.close(); }
57
+ }
@@ -0,0 +1,15 @@
1
+ import type { NativeCliEntryContext } from "./native-cli-entry-types.ts";
2
+
3
+ export async function runLocalEntry(ctx: NativeCliEntryContext): Promise<void> {
4
+ const peer = ctx.instance ?? "local";
5
+ const { args, connect, fail } = ctx;
6
+ const opt = (flag: string) => (args.includes(flag) ? args[args.indexOf(flag) + 1] ?? fail(`${flag} needs a value`) : undefined);
7
+ for (const flag of ["--route", "--model"]) if (args.includes(flag) && (!opt(flag) || opt(flag)!.startsWith("--"))) fail(`${flag} needs a value`);
8
+ if (args.includes("--route") && args.includes("--model")) fail("use --route or --model, not both");
9
+ if (args.some((a, i) => i % 2 === 0 && !["--route", "--model"].includes(a))) fail("usage: ahub local [--route <id> | --model <id>]");
10
+ const hub = await connect();
11
+ const res = await hub.request({ t: "start", peer, args: { route: opt("--route"), model: opt("--model") }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
12
+ hub.close();
13
+ if (!res.ok) fail(res.error);
14
+ console.log(res.already ? `${peer} is already attached` : `${peer} attached on ${res.model}. Give it work with: ahub say @${peer} "..."`);
15
+ }
@@ -0,0 +1,53 @@
1
+ import { parsePiFlags } from "./native-cli-pi.ts";
2
+ import type { NativeCliEntryContext } from "./native-cli-entry-types.ts";
3
+
4
+ export async function runPiEntry(ctx: NativeCliEntryContext): Promise<void> {
5
+ const peer = ctx.instance ?? "pi";
6
+ const { args, cwd, stateDir, unattendedEnv, projectConfig, connect, fail, execWithEnv } = ctx;
7
+ const { launcherPreview, startModeOf, recoveryLock, readControl, recordTerminalLaunch } = ctx.helpers;
8
+ // --headless is --mode headless everywhere this command goes: the preview, the hub and the conductor path.
9
+ if (args.includes("--headless") && !args.includes("--mode")) args.splice(args.indexOf("--headless"), 1, "--mode", "headless");
10
+ if (args.includes("--print-command") || args.includes("--dry-run")) {
11
+ try { return console.log(JSON.stringify(launcherPreview("pi", args, cwd, stateDir, unattendedEnv), null, 2)); }
12
+ catch { fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)"); }
13
+ }
14
+ const flags = parsePiFlags(args, fail);
15
+ // #269: with no flag, Pi's start mode decides, and that is its TUI unless a person set headless.
16
+ const options = { ...flags, mode: flags.mode ?? (ctx.instance ? "headless" : startModeOf(projectConfig().peers, "pi")) };
17
+ if (ctx.instance && options.mode !== "headless") fail("extra Pi instances require headless mode");
18
+ if (flags.mode === undefined && options.mode === "tui" && !(process.stdin.isTTY && process.stdout.isTTY)) {
19
+ // The default asked for the TUI and there is no terminal here (the console's command line, a script): the hub
20
+ // opens one, or says how to. It is never downgraded to headless. The fixed command carries no option, so one
21
+ // that was given needs a terminal of its own. An explicit --mode tui is taken at its word, as before.
22
+ if (Object.keys(flags).length) fail("Pi's TUI needs a terminal, and these options are not passed to one the hub opens: run this in a terminal, or add --mode headless");
23
+ const hub = await connect();
24
+ try {
25
+ const reply = await hub.request({ t: "peer_start", peer }, 35_000);
26
+ if (reply.ok !== true) {
27
+ const error = String(reply.error ?? "pi start refused");
28
+ fail(/unknown (?:control )?(?:message|request|command)(?:\b|:)|this hub does not know "peer_start"/i.test(error) ? `${error}; upgrade the running hub, or run ahub pi --mode headless` : error);
29
+ }
30
+ return console.log(reply.already ? "pi is already attached" : reply.opened ? `pi: opened a terminal running ${reply.command}; it attaches when its TUI is ready` : 'pi attached (headless). Talk to it with: ahub say @pi "..."');
31
+ } finally { hub.close(); }
32
+ }
33
+ // #215: as for Codex, a TUI launch by the recovery operation holding the lock records itself before the hub round
34
+ // trip, so a resume in that window never reads it as gone; an ordinary launch records only after the hub accepted it.
35
+ const recovering = options.mode === "tui" && !!process.env.AGENTHUB_RECOVERY_OPERATION && process.env.AGENTHUB_RECOVERY_OPERATION === recoveryLock();
36
+ const before = readControl(stateDir);
37
+ if (recovering && before?.instanceId) await recordTerminalLaunch(peer, cwd, stateDir, before.instanceId);
38
+ const hub = await connect();
39
+ const res = await hub.request({ t: "start", peer, args: options, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
40
+ hub.close();
41
+ if (!res.ok) fail(res.error);
42
+ if (options.mode === "tui") {
43
+ const launch = res.launch;
44
+ if (!launch || typeof launch.cmd !== "string" || !Array.isArray(launch.args) || !launch.args.every((a: unknown) => typeof a === "string") || !launch.env || typeof launch.env !== "object") fail("Pi TUI launch metadata was not verified");
45
+ const launchEnv = { ...(launch.env as NodeJS.ProcessEnv) };
46
+ delete launchEnv.AGENTHUB_RECOVERY_OPERATION;
47
+ delete launchEnv.AGENTHUB_UNATTENDED;
48
+ const control = readControl(stateDir);
49
+ if (!recovering && control?.instanceId) await recordTerminalLaunch(peer, cwd, stateDir, control.instanceId);
50
+ return execWithEnv(launch.cmd, launch.args, launchEnv);
51
+ }
52
+ console.log(res.already ? `${peer} is already attached` : `${peer} attached (headless). Talk to it with: ahub say @${peer} "..."`);
53
+ }
@@ -0,0 +1,21 @@
1
+ import type { NativeCliEntryContext } from "./native-cli-entry-types.ts";
2
+
3
+ export async function runQwenEntry(ctx: NativeCliEntryContext): Promise<void> {
4
+ const peer = ctx.instance ?? "qwen";
5
+ const { args, cwd, stateDir, unattendedEnv, connect, fail, takeFlags } = ctx;
6
+ const { launcherPreview } = ctx.helpers;
7
+ if (args.includes("--print-command") || args.includes("--dry-run")) {
8
+ try { return console.log(JSON.stringify(launcherPreview("qwen", args, cwd, stateDir, unattendedEnv), null, 2)); }
9
+ catch { fail("cannot preview Qwen launch: invalid arguments or configuration (details withheld)"); }
10
+ }
11
+ const { one, rest } = takeFlags(args, ["--mode", "--model", "--session-id"], []);
12
+ const adapter = ctx.definition;
13
+ if (one["--session-id"] !== undefined && !adapter.session.crashIdentity?.({ sessionId: one["--session-id"] }).sessionId) fail("invalid Qwen session identity");
14
+ if (rest.length || (one["--mode"] !== undefined && !(adapter.start.forms as readonly string[]).includes(one["--mode"]))) fail("Qwen TUI is not offered before verification; usage: ahub qwen [--mode headless] [--model <alias>] [--session-id <id>]");
15
+ const hub = await connect();
16
+ try {
17
+ const reply = await hub.request({ t: "start", peer, args: { mode: "headless", ...(one["--model"] ? { model: one["--model"] } : {}), ...(one["--session-id"] ? { sessionId: one["--session-id"] } : {}) }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 35_000);
18
+ if (!reply.ok) fail(reply.error);
19
+ console.log(reply.already ? `${peer} is already attached` : `${peer} attached (headless); use ahub say @${peer} and the console for approvals`);
20
+ } finally { hub.close(); }
21
+ }
@@ -0,0 +1,41 @@
1
+ import type { HubConfig } from "../../hub/daemon.ts";
2
+ import type { ControlClient } from "../../hub/control-client.ts";
3
+ import type { AdapterDefinition } from "../registry-types.ts";
4
+
5
+ /** Private static builtin relocation ports. The CLI binds identity, project and authority before invocation. */
6
+ export interface NativeCliEntryContext {
7
+ instance?: string; executionCwd?: string;
8
+ args: string[]; cwd: string; stateDir: string; cliDir: string; unattendedEnv: boolean; definition: AdapterDefinition;
9
+ projectConfig(): HubConfig;
10
+ connect(): Promise<ControlClient>;
11
+ fail(message: string): never;
12
+ exec(bin: string, argv: string[], tool?: "claude" | "codex" | "kimi"): never;
13
+ execWithEnv(bin: string, argv: string[], extra: NodeJS.ProcessEnv): never;
14
+ takeFlags(argv: string[], single: string[], repeated: string[]): { one: Record<string, string>; many: Record<string, string[]>; rest: string[] };
15
+ helpers: {
16
+ launcherPreview: typeof import("../../cli/preview.ts").launcherPreview;
17
+ readControl: typeof import("../../hub/control-client.ts").readControl;
18
+ recordTerminalLaunch: typeof import("../../cli/terminal-recovery.ts").recordTerminalLaunch;
19
+ recoveryLock: typeof import("../../hub/recovery-store.ts").recoveryLock;
20
+ processSignature: typeof import("../../pi/process-signature.ts").processSignature;
21
+ buildLaunch: typeof import("../../cli/launch.ts").buildLaunch;
22
+ claudeObservationHooks: typeof import("../../cli/launch.ts").claudeObservationHooks;
23
+ recordClaudeLaunch: typeof import("../../cli/launch.ts").recordClaudeLaunch;
24
+ cleanupStaleClaudeSettings: typeof import("../../cli/launch.ts").cleanupStaleClaudeSettings;
25
+ cleanupClaudeSettings: typeof import("../../cli/launch.ts").cleanupClaudeSettings;
26
+ nativeLaunchEnv: typeof import("../../cli/launch.ts").nativeLaunchEnv;
27
+ runClaudeLaunch: typeof import("../../cli/launch.ts").runClaudeLaunch;
28
+ childEnv: typeof import("../../hub/child-process.ts").childEnv;
29
+ qwenSessionId: typeof import("../../hub/crash.ts").qwenSessionId;
30
+ configureJcodeMcp: typeof import("../../cli/jcode-mcp.ts").configureJcodeMcp;
31
+ configureKimiMcp: typeof import("../../cli/kimi-mcp.ts").configureKimiMcp;
32
+ buildKimiTuiLaunch: typeof import("../../cli/launch.ts").buildKimiTuiLaunch;
33
+ startModeOf: typeof import("../../hub/start-mode.ts").startModeOf;
34
+ };
35
+ }
36
+ export interface BuiltinCliEntry {
37
+ command: string;
38
+ /** Original CLI guard timing: previews precede it; Pi/local retain their daemon-enforced start admission. */
39
+ lifecycleAdmission: boolean;
40
+ execute(context: NativeCliEntryContext): Promise<void>;
41
+ }
@@ -0,0 +1,19 @@
1
+ import type { BuiltinCliEntry } from "./native-cli-entry-types.ts";
2
+ import { runClaudeEntry } from "./native-cli-entry-claude.ts";
3
+ import { runCodexEntry } from "./native-cli-entry-codex.ts";
4
+ import { runJcodeEntry } from "./native-cli-entry-jcode.ts";
5
+ import { runQwenEntry } from "./native-cli-entry-qwen.ts";
6
+ import { runKimiEntry } from "./native-cli-entry-kimi.ts";
7
+ import { runPiEntry } from "./native-cli-entry-pi.ts";
8
+ import { runLocalEntry } from "./native-cli-entry-local.ts";
9
+
10
+ // Fixed trusted builtins only. No project configuration, caller payload or external module can register an entry.
11
+ export const builtinCliEntries: readonly BuiltinCliEntry[] = Object.freeze([
12
+ { command: "claude", lifecycleAdmission: true, execute: runClaudeEntry },
13
+ { command: "codex", lifecycleAdmission: true, execute: runCodexEntry },
14
+ { command: "jcode", lifecycleAdmission: true, execute: runJcodeEntry },
15
+ { command: "qwen", lifecycleAdmission: true, execute: runQwenEntry },
16
+ { command: "kimi", lifecycleAdmission: true, execute: runKimiEntry },
17
+ { command: "pi", lifecycleAdmission: false, execute: runPiEntry },
18
+ { command: "local", lifecycleAdmission: false, execute: runLocalEntry },
19
+ ]);