@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,130 @@
1
+ import { closeSync, constants, fstatSync, openSync, readdirSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join, normalize } from "node:path";
3
+ import type { Addon, AddonName } from "../../hub/tooling.ts";
4
+
5
+ /** Native crash recovery keeps each adapter's exact restart and guidance contract. */
6
+ interface NativeCrashPlan { resume?: Record<string, string>; how: string; fresh?: Record<string, string>; tui?: true }
7
+ const str = (value: unknown): string | undefined => typeof value === "string" && value ? value : undefined;
8
+
9
+ export function claudeCrashPlan(_meta: Record<string, unknown>): NativeCrashPlan {
10
+ return { how: "claude: the Claude Code plugin reconnects by itself while that session is still open" };
11
+ }
12
+
13
+ export function codexCrashPlan(meta: Record<string, unknown>): NativeCrashPlan {
14
+ return { how: `codex: its app-server died with the hub; run ahub codex again${str(meta.threadId) ? ` (its conversation was thread ${meta.threadId})` : ""}` };
15
+ }
16
+
17
+ export function jcodeCrashPlan(_meta: Record<string, unknown>): NativeCrashPlan {
18
+ return { how: "jcode: native 0.94.0 load does not prove the restored id; reopen the original conversation with its native client, no automatic fresh replacement" };
19
+ }
20
+
21
+ export function kimiCrashPlan(meta: Record<string, unknown>): NativeCrashPlan {
22
+ const launch = (meta.launch && typeof meta.launch === "object" ? meta.launch : {}) as Record<string, unknown>;
23
+ const model = str(launch.model);
24
+ const sessionId = str(meta.sessionId);
25
+ if (!sessionId) return { how: "kimi: no session id was recorded; start it again with ahub kimi" };
26
+ return { resume: { sessionId, ...(model ? { model } : {}) }, how: `kimi: session ${sessionId} can be loaded again (ACP session/load)` };
27
+ }
28
+
29
+ export function piCrashPlan(meta: Record<string, unknown>): NativeCrashPlan {
30
+ const launch = (meta.launch && typeof meta.launch === "object" ? meta.launch : {}) as Record<string, unknown>;
31
+ const model = str(launch.model);
32
+ // `fresh`: what a new headless session keeps of the recorded one (pi.auto_start's fallback, #66).
33
+ const fresh: Record<string, string> = {};
34
+ for (const k of ["backend", "model"] as const) if (str(launch[k])) fresh[k] = str(launch[k])!;
35
+ const sessionFile = str(meta.sessionFile) ?? str(launch.sessionFile);
36
+ // #269: a terminal Pi stays marked as one without a session file too, so nothing headless is started in its place.
37
+ if (!sessionFile) return { fresh, ...(str(launch.mode) === "tui" ? { tui: true } : {}), how: "pi: no session file was recorded; start it again with ahub pi" };
38
+ // A terminal Pi is run by the CLI that launched it, not by the hub: nothing here could start it again.
39
+ if (str(launch.mode) === "tui") return { fresh, tui: true, how: `pi: it ran in a terminal; start it again with ahub pi --mode tui --session-file ${sessionFile}` };
40
+ const args: Record<string, string> = { sessionFile };
41
+ for (const k of ["mode", "backend", "model"] as const) if (str(launch[k])) args[k] = str(launch[k])!;
42
+ return { fresh, resume: args, how: `pi: its session file can be resumed (${sessionFile})` };
43
+ }
44
+
45
+ export function localCrashPlan(meta: Record<string, unknown>): NativeCrashPlan {
46
+ const launch = (meta.launch && typeof meta.launch === "object" ? meta.launch : {}) as Record<string, unknown>;
47
+ const model = str(launch.model);
48
+ // `model` is also recorded as the route's fallback, and a model given at start pins it: pass one or the other.
49
+ const route = str(launch.route);
50
+ const args: Record<string, string> = route ? { route } : model ? { model } : {};
51
+ return { resume: args, how: "local: starts again without its history (the worker keeps none across a hub stop)" };
52
+ }
53
+
54
+ /** Plugin names (before the `@marketplace`) that stand for an add-on. */
55
+ // Maps, not objects: a plugin named `constructor` or `toString` must find nothing, not an inherited property.
56
+ const PLUGINS = new Map<string, AddonName>([["claude-mem", "claude-mem"], ["ponytail", "ponytail"], ["open-code-review", "open-code-review"]]);
57
+ /** Claude Code only: the Codex plugin is a Claude plugin named `codex` from the `openai-codex` marketplace. */
58
+ const CLAUDE_ONLY = new Map<string, AddonName>([["codex@openai-codex", "codex-plugin"]]);
59
+ /** A regular file of at most 1 MiB, opened without blocking: a FIFO or device under a project's `.claude/` reads as absent. */
60
+ const text = (file: string): string | undefined => {
61
+ let fd: number | undefined;
62
+ try {
63
+ fd = openSync(file, constants.O_RDONLY | constants.O_NONBLOCK);
64
+ const st = fstatSync(fd);
65
+ if (!st.isFile() || st.size > 1024 * 1024) return undefined;
66
+ return readFileSync(fd, "utf8");
67
+ } catch { return undefined; } finally { if (fd !== undefined) closeSync(fd); }
68
+ };
69
+ const json = (file: string): Record<string, unknown> | undefined => {
70
+ try { const v = JSON.parse(text(file) ?? ""); return v && typeof v === "object" ? v as Record<string, unknown> : undefined; } catch { return undefined; }
71
+ };
72
+ const version = (v: unknown): string | null => typeof v === "string" && /^[0-9A-Za-z.+_-]{1,40}$/.test(v) ? v : null;
73
+
74
+ function claudePlugins(root: string, cwd: string | undefined): Addon[] {
75
+ const enabled = new Map<string, boolean>();
76
+ for (const file of [join(root, "settings.json"), ...(cwd ? [join(cwd, ".claude", "settings.json"), join(cwd, ".claude", "settings.local.json")] : [])]) {
77
+ const map = json(file)?.enabledPlugins;
78
+ if (map && typeof map === "object") for (const [key, on] of Object.entries(map)) enabled.set(key, on === true);
79
+ }
80
+ const installed = (json(join(root, "plugins", "installed_plugins.json"))?.plugins ?? {}) as Record<string, { scope?: string; projectPath?: string; version?: unknown }[]>;
81
+ const out: Addon[] = [];
82
+ for (const [key, on] of enabled) {
83
+ if (!on) continue;
84
+ const name = CLAUDE_ONLY.get(key) ?? PLUGINS.get(key.split("@")[0]!);
85
+ const entries = Array.isArray(installed[key]) ? installed[key]! : [];
86
+ // An install for another project is not this one's.
87
+ const entry = entries.find((e) => e?.scope === "user") ?? entries.find((e) => e?.projectPath === cwd);
88
+ if (name && entry) out.push({ name, version: version(entry.version) });
89
+ }
90
+ return out;
91
+ }
92
+
93
+ function codexPlugins(root: string): Addon[] {
94
+ const config = text(join(root, "config.toml"));
95
+ if (config === undefined) return [];
96
+ const found = new Map<string, string>(), disabled = new Set<string>(); // plugin name -> marketplace
97
+ // `[plugins."name@market"]` with no `enabled = false` below it, and plugins whose hooks Codex was told to trust
98
+ // unless their plugin entry disables them: a trust entry outlives the plugin's own switch.
99
+ for (const m of config.matchAll(/^\[plugins\."([^"@]+)@([^"]+)"\]\s*\n((?:(?!\[)[^\n]*\n?)*)/gm)) if (!/^\s*enabled\s*=\s*false\b/m.test(m[3] ?? "")) found.set(m[1]!, m[2]!); else disabled.add(m[1]! + "@" + m[2]!);
100
+ for (const m of config.matchAll(/^\[hooks\.state\."([^"@]+)@([^":]+):/gm)) if (!disabled.has(m[1]! + "@" + m[2]!)) found.set(m[1]!, m[2]!);
101
+ const out: Addon[] = [];
102
+ for (const [plugin, market] of found) {
103
+ const name = PLUGINS.get(plugin);
104
+ if (!name) continue;
105
+ let versions: string[] = [];
106
+ try { versions = readdirSync(join(root, "plugins", "cache", market, plugin)); } catch { /* no cache: version unknown */ }
107
+ out.push({ name, version: versions.length === 1 ? version(versions[0]) : null });
108
+ }
109
+ return out;
110
+ }
111
+
112
+ interface PluginInventoryContext { home: string; cwd?: string; env: Readonly<Record<string, string | undefined>> }
113
+ export const claudePluginInventory = (ctx: PluginInventoryContext): Addon[] => claudePlugins(ctx.env.CLAUDE_CONFIG_DIR?.trim() || join(ctx.home, ".claude"), ctx.cwd);
114
+ export const codexPluginInventory = (ctx: PluginInventoryContext): Addon[] => codexPlugins(ctx.env.CODEX_HOME?.trim() || join(ctx.home, ".codex"));
115
+
116
+ /** A persisted native identity decoder; the coordinator retains envelope, path and operation checks. */
117
+ export function parsePiContinuity(value: unknown): { sessionId: string; sessionFile?: string; backend?: "auto" | "dgx" | "mlx"; model?: string } | undefined {
118
+ const record = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);
119
+ const text = (v: unknown, max: number): v is string => typeof v === "string" && v.length > 0 && v.length <= max && !/[\x00-\x1f\x7f]/.test(v);
120
+ const id = (v: unknown): v is string => text(v, 256) && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(v);
121
+ if (!record(value) || Object.keys(value).some(key => !["sessionId", "sessionFile", "backend", "model"].includes(key)) || !id(value.sessionId)
122
+ || value.sessionFile !== undefined && !absoluteContinuityPath(value.sessionFile) || value.backend !== undefined && !["auto", "dgx", "mlx"].includes(String(value.backend))
123
+ || value.model !== undefined && !text(value.model, 4096)) return undefined;
124
+ return { sessionId: value.sessionId, ...(value.sessionFile === undefined ? {} : { sessionFile: value.sessionFile as string }), ...(value.backend === undefined ? {} : { backend: value.backend as "auto" | "dgx" | "mlx" }), ...(value.model === undefined ? {} : { model: value.model as string }) };
125
+ }
126
+ function absoluteContinuityPath(value: unknown): value is string {
127
+ return typeof value === "string" && value.length > 0 && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute(value) && normalize(value) === value;
128
+ }
129
+
130
+ export const codexToolSemantic = (name: string, _command?: string): "edit" | undefined => name.toLowerCase() === "filechange" ? "edit" : undefined;
@@ -0,0 +1,29 @@
1
+ import type { UsageWindow } from "../../hub/budget.ts";
2
+
3
+ /** Codex `rateLimits` snapshot (account/rateLimits/read, account/rateLimits/updated) -> windows. */
4
+ export function codexWindows(rateLimits: any): UsageWindow[] {
5
+ const out: UsageWindow[] = [];
6
+ // The slot decides (primary = short window, secondary = long one) and the duration overrides it when present:
7
+ // readings are keyed by window id, so two windows must never both come out as "5h".
8
+ for (const [w, slot] of [[rateLimits?.primary, "5h"], [rateLimits?.secondary, "week"]] as const) {
9
+ if (!w || typeof w.usedPercent !== "number") continue;
10
+ const mins = typeof w.windowDurationMins === "number" ? w.windowDurationMins : undefined;
11
+ const id = mins === undefined ? slot : mins > 600 ? "week" : "5h";
12
+ if (out.some((o) => o.id === id)) continue; // both slots claim the same window: keep the first, never overwrite
13
+ out.push({ id, used: w.usedPercent / 100, ...(w.resetsAt ? { resetsAt: w.resetsAt * 1000 } : {}), ...(mins ? { windowMins: mins } : {}), source: "codex rateLimits" });
14
+ }
15
+ if (rateLimits?.rateLimitReachedType && !out.some((w) => w.used >= 1)) out.push({ id: "5h", used: 1, source: `codex ${rateLimits.rateLimitReachedType}` });
16
+ return out;
17
+ }
18
+
19
+ /** Claude Code status line `rate_limits` -> windows. */
20
+ export function claudeWindows(rateLimits: any): UsageWindow[] {
21
+ const out: UsageWindow[] = [];
22
+ for (const [key, id, mins] of [["five_hour", "5h", 300], ["seven_day", "week", 10_080]] as const) {
23
+ const w = rateLimits?.[key];
24
+ if (!w || typeof w.used_percentage !== "number") continue;
25
+ const resets = Number(w.resets_at);
26
+ out.push({ id, used: w.used_percentage / 100, ...(resets ? { resetsAt: resets < 1e12 ? resets * 1000 : resets } : {}), windowMins: mins, source: "claude status line" });
27
+ }
28
+ return out;
29
+ }
@@ -0,0 +1,12 @@
1
+ import type { RecoveryPeer } from "../../cli/upgrade.ts";
2
+ import type { TerminalBinding } from "../../cli/terminal-recovery.ts";
3
+ import type { RecoveryContinuityBookmark } from "../../hub/recovery-continuity.ts";
4
+
5
+ /** Native continuity payload only; archival evidence and persistence remain with the recovery coordinator. */
6
+ export function piRecoveryContinuity(peer: RecoveryPeer, binding?: TerminalBinding): Pick<RecoveryContinuityBookmark, "pi"> {
7
+ const sessionId = binding?.sessionId ?? peer.sessionId;
8
+ if (!sessionId) throw new Error("Pi recorded session unavailable; archive continuity bookmark was not written");
9
+ const backend = binding?.backend ?? peer.args?.backend;
10
+ return { pi: { sessionId, sessionFile: binding?.sessionFile ?? peer.sessionFile ?? peer.args?.sessionFile,
11
+ ...(backend === "auto" || backend === "dgx" || backend === "mlx" ? { backend } : {}), model: binding?.model ?? peer.args?.model } };
12
+ }
@@ -0,0 +1,88 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { readEvents } from "../../hub/events.ts";
4
+ import { checkedSessionPath, recordedFileState } from "../../cli/recovery-session-file.ts";
5
+ import type { AdapterDefinition } from "../registry-types.ts";
6
+ import type { TerminalBinding } from "../../cli/terminal-recovery.ts";
7
+ import type { RecoveryPeer } from "../../cli/upgrade.ts";
8
+
9
+ type SessionPolicy = Partial<AdapterDefinition["session"]>;
10
+ const rolloutState = (binding: TerminalBinding): boolean | "unknown" => {
11
+ try { return readdirSync(checkedSessionPath(binding, "rollout"), { recursive: true }).some(name => String(name).endsWith(".jsonl") && basename(String(name)).includes(binding.sessionId)); }
12
+ catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? false : "unknown"; }
13
+ };
14
+ const transcriptState = (binding: TerminalBinding): boolean | "unknown" => {
15
+ const state = recordedFileState(checkedSessionPath(binding, "transcript"));
16
+ return state === "unknown" ? "unknown" : state === "found";
17
+ };
18
+ /** Preserve the driver's explicit evidence-kind dispatch, including historical typed receipt reads. */
19
+ export function nativeRecordedSessionState(binding: TerminalBinding, kind: "rollout" | "transcript"): "found" | "missing" | "unknown" {
20
+ const state = kind === "rollout" ? rolloutState(binding) : transcriptState(binding);
21
+ return state === "unknown" ? "unknown" : state ? "found" : "missing";
22
+ }
23
+ const startupArgs = (peer: RecoveryPeer): Record<string, string> => Object.fromEntries(["mode", "backend", "model", "route"]
24
+ .flatMap(key => typeof peer.args?.[key] === "string" ? [[key, peer.args[key]!]] : []));
25
+ const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
26
+ const object = (value: unknown): Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
27
+
28
+ export const CODEX_RECOVERY_SESSION: SessionPolicy = {
29
+ resumeArgv: (binding, fresh) => ["codex", ...(fresh ? [] : ["resume", binding.sessionId])],
30
+ transcriptPath: binding => checkedSessionPath(binding, "rollout"), transcriptExists: rolloutState,
31
+ personFresh: true, missingEvidence: "rollout",
32
+ zeroTurn: ({ stateDir, binding, phase }) => {
33
+ if (phase !== "plan") return false;
34
+ let current: string | undefined, started = false, turned = false;
35
+ for (const e of readEvents(join(stateDir, "events.jsonl"))) {
36
+ if (e.type === "native_thread" && e.peer === binding.peer) {
37
+ current = e.thread;
38
+ if (e.thread === binding.sessionId && e.fresh) started = true;
39
+ } else if (e.type === "turn_start" && e.peer === binding.peer && current === binding.sessionId) turned = true;
40
+ }
41
+ return started && !turned;
42
+ },
43
+ };
44
+ export const CLAUDE_RECOVERY_SESSION: SessionPolicy = {
45
+ resumeArgv: (binding, fresh) => ["claude", ...(fresh ? [] : ["--resume", binding.sessionId])],
46
+ transcriptPath: binding => checkedSessionPath(binding, "transcript"), transcriptExists: transcriptState,
47
+ zeroTurn: ({ binding, phase }) => phase !== "plan" && transcriptState(binding) !== true,
48
+ personFresh: true, missingEvidence: "transcript",
49
+ };
50
+ export const PI_RECOVERY_SESSION: SessionPolicy = {
51
+ resumeArgv: binding => ["pi", "--mode", "tui", ...(binding.backend ? ["--backend", binding.backend] : []), ...(binding.model ? ["--model", binding.model] : []),
52
+ ...(binding.sessionFile ? ["--session-file", binding.sessionFile] : ["--session-id", binding.sessionId])],
53
+ personFresh: false,
54
+ restoreArgs: peer => {
55
+ const args = startupArgs(peer);
56
+ if (typeof peer.args?.mode !== "string") args.mode = "headless";
57
+ const file = peer.sessionFile ?? peer.args?.sessionFile;
58
+ if (typeof file === "string") args["session-file"] = file;
59
+ else if (peer.sessionId) args["session-id"] = peer.sessionId;
60
+ return args;
61
+ },
62
+ crashIdentity: row => {
63
+ const value = object(row), args = object(value.args), session = object(value.session);
64
+ const sessionId = [value.sessionId, value.agentSessionId, value.resumeSessionId, session.id, session.sessionId].find(text);
65
+ const sessionFile = [value.sessionFile, value.sessionPath, args.sessionFile, session.file, session.path].find(text);
66
+ return { ...(sessionId ? { sessionId } : {}), ...(sessionFile ? { sessionFile } : {}) };
67
+ },
68
+ };
69
+ export const KIMI_RECOVERY_SESSION: SessionPolicy = {
70
+ legacyHeadlessArgv: { file: "src/hub/start-mode.ts", exportName: "START_MODE_PEERS", argv: ["--mode", "headless"] },
71
+ restoreArgs: peer => { const args = startupArgs(peer); delete args.mode; return args; },
72
+ recoveryRefusal: (peer, phase = "plan") => peer.args?.mode === "tui"
73
+ ? phase === "restore" ? "Kimi's pull-only terminal cannot be restored as a headless worker; end it in that terminal and make a new recovery plan"
74
+ : "kimi: its pull-only terminal has no automatic restore; end it in that terminal, then make a new recovery plan (its session is not resumed by the hub)" : undefined,
75
+ };
76
+ export const LOCAL_RECOVERY_SESSION: SessionPolicy = { restoreArgs: startupArgs };
77
+ export const JCODE_RECOVERY_SESSION: SessionPolicy = {
78
+ legacyTargetSupport: { file: "src/hub/peer-catalog.ts", exportName: "OPTIONAL_PEERS", refusal: "target does not support Jcode; end it and make a new plan, or choose a supporting target; runtimes unchanged" },
79
+ recoveryRefusal: (_peer, phase = "plan") => phase === "restore" ? undefined : "jcode: source-pinned native load cannot prove conversation identity; end the peer and make a new recovery plan before effects",
80
+ restoreArgs: peer => ({ ...startupArgs(peer), ...(peer.sessionId ? { "session-id": peer.sessionId } : {}), ...(peer.args?.providerProfile ? { "provider-profile": peer.args.providerProfile } : {}) }),
81
+ };
82
+ export const QWEN_RECOVERY_SESSION: SessionPolicy = {
83
+ legacyTargetSupport: { file: "src/hub/peer-catalog.ts", exportName: "OPTIONAL_PEERS", refusal: "target does not support Qwen; end Qwen and make a new plan, or choose a supporting target; runtimes unchanged" },
84
+ restoreArgs: peer => ({ ...startupArgs(peer), ...(peer.sessionId ? { "session-id": peer.sessionId } : {}) }),
85
+ };
86
+
87
+ /** Fixed legacy native probes: callers retain their existing source-text capability checks. */
88
+ export const SHARED_CHANNEL_VERSION_ARGV = ["claude", "--version"] as const;
@@ -7,9 +7,9 @@ export class QwenIdentityError extends Error {
7
7
  constructor() { super("Qwen Code 0.25.0 native identity is not verified; check the executable and its advertised ACP version"); }
8
8
  }
9
9
 
10
- export function qwenNativeIdentity(instance: string, production: boolean | undefined, init: unknown): boolean {
10
+ export function qwenNativeIdentity(instance: string, production: boolean | undefined, init: unknown, kind?: string): boolean {
11
11
  const native = init as { agentInfo?: { name?: unknown; version?: unknown } } | null | undefined;
12
- const verified = production === true && instance === "qwen" && native?.agentInfo?.name === "qwen-code" && native?.agentInfo?.version === "0.25.0";
12
+ const verified = production === true && (kind ?? instance) === "qwen" && native?.agentInfo?.name === "qwen-code" && native?.agentInfo?.version === "0.25.0";
13
13
  if (production && !verified) throw new QwenIdentityError();
14
14
  return verified;
15
15
  }
@@ -1,7 +1,9 @@
1
+ import { QWEN_DISPLAY } from "./native-display.ts";
2
+ import { QWEN_RECOVERY_SESSION } from "./native-recovery.ts";
1
3
  import { AcpPeer } from "../acp.ts";
2
4
  import type { AdapterDefinition } from "../registry-types.ts";
3
5
  import { buildQwenLaunch, qwenNativeIdentity, qwenSessionId, QWEN_MODE_IDS } from "./qwen-native.ts";
4
- import { shellQuote } from "../../cli/terminal-recovery.ts";
6
+ import { shellQuote } from "../../cli/shell-quote.ts";
5
7
 
6
8
  /** Production Qwen policy. Legacy AcpPeer callers opt in separately through qwen025. */
7
9
  export const QWEN_DEFINITION: AdapterDefinition = {
@@ -15,6 +17,7 @@ export const QWEN_DEFINITION: AdapterDefinition = {
15
17
  defaultOnRoster: false,
16
18
  trust: { onPrem: false, hubSandboxed: false },
17
19
  start: {
20
+ daemonPort: "acp",
18
21
  forms: ["headless"],
19
22
  defaultForm: "headless",
20
23
  ownsTerminal: () => false,
@@ -34,7 +37,7 @@ export const QWEN_DEFINITION: AdapterDefinition = {
34
37
  if (context.permission === "read-only") throw new Error("Qwen has no verified read-only permission mapping");
35
38
  return new AcpPeer(context.instance, { ...context.daemon.acp, qwen025: true, permissionMode: context.permission, mcpServers: context.mcpServers });
36
39
  },
37
- conductorStartArgs: args => args.length ? new Error("Qwen start takes no conductor overrides") : {},
40
+ conductorStartArgs: args => args.length ? new Error("conductor starts accept no launch overrides; use ahub qwen") : {},
38
41
  },
39
42
  identity: { mcp: "acp-session" },
40
43
  permission: {
@@ -55,8 +58,9 @@ export const QWEN_DEFINITION: AdapterDefinition = {
55
58
  const sessionId = peer.recoveryMetadata?.().sessionId;
56
59
  return typeof sessionId === "string" ? sessionId : undefined;
57
60
  },
61
+ ...QWEN_RECOVERY_SESSION,
58
62
  launchKind: "acp",
59
- restoreOnStart: true,
63
+ resumeById: true,
60
64
  crashIdentity: row => {
61
65
  const sessionId = row && typeof row === "object" ? (row as { sessionId?: unknown }).sessionId : undefined;
62
66
  return qwenSessionId(sessionId) ? { sessionId } : {};
@@ -66,6 +70,6 @@ export const QWEN_DEFINITION: AdapterDefinition = {
66
70
  budget: { account: "qwen" },
67
71
  capabilities: { watch: true },
68
72
  cli: { command: "qwen", startWords: () => ["qwen"] },
69
- display: { palette: { fg: "#8a8f99", bg: "transparent" } },
73
+ display: QWEN_DISPLAY,
70
74
  hooks: { agentIdentity: init => { qwenNativeIdentity("qwen", true, init); }, recallPlatform: "qwen" },
71
75
  };
@@ -18,6 +18,7 @@ import type { ExecutionBudgetDecision } from "../hub/execution-budget.ts";
18
18
  export interface LocalOptions {
19
19
  profile?: import("../hub/profiles.ts").ProfileSelection;
20
20
  sdlcStages?: readonly string[];
21
+ sdlcCriteria?: boolean;
21
22
  cwd: string;
22
23
  omni: OmniRoute;
23
24
  /** Absent = always the fixed model on OmniRoute. */
@@ -108,7 +109,7 @@ export class LocalPeer extends BasePeer {
108
109
  await this.requireBudget(envs, "model_calls");
109
110
  if (generation !== this.turn) throw new Error("route belongs to an ended turn");
110
111
  if (signal.aborted) throw new Error("turn cancelled before model request");
111
- const tools = judge ? undefined : [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : [])];
112
+ const tools = judge ? undefined : [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : [])];
112
113
  const result = await this.opts.omni.chat({ model, messages, ...(tools ? { tools } : {}), ...(judge ? { max_tokens: maxTokens ?? 2048 } : {}) }, { signal, ...(pii ? { onCampusOnly: true } : {}) });
113
114
  this.recordUsage(result, model, task);
114
115
  if (generation !== this.turn) throw new Error("route belongs to an ended turn");
@@ -378,7 +379,7 @@ export class LocalPeer extends BasePeer {
378
379
  const route = policy?.route ?? this.opts.route;
379
380
  const fixedModel = policy?.fixedModel ?? this.opts.fixedModel;
380
381
  const task = policy?.task === undefined ? undefined : Number(policy.task);
381
- const tools = [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : [])];
382
+ const tools = [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : [])];
382
383
  const signal = this.abort!.signal;
383
384
  const messages: ChatMessage[] = [{ role: "system", content: system(this.opts.cwd, this.opts.preamble) }, ...this.history, ...turnMsgs];
384
385
  const routed = await this.hubCall(route, messages, policy, signal);
@@ -33,6 +33,8 @@ class PiPromptNotStarted extends Error {
33
33
  }
34
34
  class PiPromptUncertain extends Error {}
35
35
  export interface PiOptions {
36
+ /** stateDir owns this native instance; hubStateDir retains the canonical daemon binding. */
37
+ hubStateDir?: string;
36
38
  cwd: string; stateDir: string; cmd?: string[]; mode: "headless" | "tui"; backend: "auto" | "dgx" | "mlx"; sessionFile?: string; sessionId?: string;
37
39
  model?: string;
38
40
  relay: PiRelay; executeTool: (name: string, args: unknown, toolCallId: string, sessionId?: string, signal?: AbortSignal, context?: PiToolContext) => Promise<string | PiToolResult>; tools: PiToolSchema[];
@@ -385,7 +387,7 @@ export class PiPeer extends BasePeer {
385
387
  const extension = resolve(join(import.meta.dir, "../pi/extension.ts"));
386
388
  const inherited: NodeJS.ProcessEnv = {};
387
389
  for (const key of ["PATH", "HOME", "USER", "SHELL", "TMPDIR", "TERM", "TERM_PROGRAM", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "CODEX_HOME"]) if (process.env[key]) inherited[key] = process.env[key];
388
- this._tuiLaunch = buildPiLaunch(this.opts, inherited, extension, bridge.port!, token);
390
+ this._tuiLaunch = buildPiLaunch({ ...this.opts, instance: this.id }, inherited, extension, bridge.port!, token);
389
391
  const { cmd: bin, args, env } = this._tuiLaunch;
390
392
  if (this.opts.mode === "tui") { return; }
391
393
  // Its own process group, stopped as a whole (#115, as Codex's in #113).
@@ -1,5 +1,9 @@
1
+ import type { SettingDef } from "../hub/settings.ts";
2
+ import type { Addon } from "../hub/tooling.ts";
3
+ import type { RecoveryContinuityBookmark } from "../hub/recovery-continuity.ts";
4
+ import type { RecoveryPeer } from "../cli/upgrade.ts";
1
5
  import type { Launch, StatusLineTee, FactsHook } from "../cli/launch.ts";
2
- import type { LaunchMetadata, TerminalBinding } from "../cli/terminal-recovery.ts";
6
+ import type { LaunchMetadata, TerminalBinding, SessionRef } from "../cli/terminal-recovery.ts";
3
7
  import type { UsageWindow } from "../hub/budget.ts";
4
8
  import type { ContextReading } from "../hub/context-window.ts";
5
9
  import type { PeerAdapter } from "../hub/peers.ts";
@@ -11,6 +15,7 @@ import type { LocalOptions } from "./local-worker.ts";
11
15
  import type { PiOptions } from "./pi.ts";
12
16
 
13
17
  export type StartForm = "tui" | "headless";
18
+ export type DaemonEngine = "codex-appserver" | "acp" | "owned-acp" | "pi-rpc" | "local-worker";
14
19
 
15
20
  /** Existing buildLaunch inputs plus the startup-bound instance and project identity. */
16
21
  export interface LaunchContext {
@@ -31,9 +36,16 @@ export interface LaunchContext {
31
36
  env: Readonly<Record<string, string>>;
32
37
  /** Existing validated config argv (or the existing redacted preview), never model input. */
33
38
  nativeCommand?: readonly string[];
39
+ /** Existing trusted Pi builder inputs; never populated from a request or model. */
40
+ pi?: {
41
+ options: Pick<PiOptions, "stateDir" | "cmd" | "mode" | "backend" | "model" | "relay" | "tools" | "preamble" | "maxSteps" | "sessionFile" | "sessionId">;
42
+ extension: string;
43
+ bridgePort: number | string;
44
+ bridgeToken: string;
45
+ };
34
46
  }
35
47
 
36
- export type LaunchPlan = Launch & { metadata: LaunchMetadata; mcpFile?: { path: string; entry: unknown } };
48
+ export type LaunchPlan = Launch & { env?: NodeJS.ProcessEnv; metadata: LaunchMetadata; mcpFile?: { path: string; entry: unknown } };
37
49
 
38
50
  /** A factory receives exactly its existing transport options, with no cross-transport callback widening. */
39
51
  export type DaemonPorts =
@@ -65,6 +77,9 @@ export interface StartRequest {
65
77
 
66
78
  export interface VersionContext {
67
79
  instance: string;
80
+ observedVersion?: string;
81
+ nativeCommand?: readonly string[];
82
+ projectRoot?: string;
68
83
  peer?: PeerAdapter;
69
84
  env: Readonly<Record<string, string>>;
70
85
  }
@@ -78,13 +93,13 @@ export type NativeModeSpec =
78
93
 
79
94
  export interface QuotaReader {
80
95
  source: string;
81
- read(peer: PeerAdapter): UsageWindow[] | undefined;
96
+ read(value: unknown): UsageWindow[];
82
97
  resetOnWindow?: boolean;
83
98
  }
84
99
 
85
100
  export interface ContextReader {
86
101
  source: ContextReading["source"];
87
- read(peer: PeerAdapter): ContextReading | undefined;
102
+ read(value: unknown, sessionId: string, measuredAt: number): ContextReading;
88
103
  }
89
104
 
90
105
  export interface CapabilityManifest {
@@ -106,10 +121,21 @@ export interface NativeHooks {
106
121
  factsHook: (msg: unknown) => unknown;
107
122
  conversationRevert(turnId: string): Promise<void>;
108
123
  replaceSession(req: StartRequest): Promise<unknown>;
109
- pluginInventory(env: Readonly<Record<string, string>>): string[];
124
+ pluginInventory(ctx: { home: string; cwd?: string; env: Readonly<Record<string, string | undefined>> }): Addon[];
125
+ recallSource: string;
126
+ recallInjected: boolean;
127
+ classifyTool(name: string, command?: string): "edit" | undefined;
110
128
  recallPlatform: string;
111
129
  }
112
130
 
131
+ export type NativeConfigCheck = (value: unknown, key: string) => void;
132
+ export interface NativeConfigValidators {
133
+ bool: NativeConfigCheck; text: NativeConfigCheck; strings: NativeConfigCheck; argv: NativeConfigCheck;
134
+ number(min?: number, max?: number, integer?: boolean): NativeConfigCheck;
135
+ words(values: readonly string[]): NativeConfigCheck;
136
+ block(fields: Record<string, NativeConfigCheck>): NativeConfigCheck;
137
+ }
138
+
113
139
  /** Adapter API 1, including the merged revision-3 members frozen in issue #451. */
114
140
  export interface AdapterDefinition {
115
141
  api: 1;
@@ -120,6 +146,8 @@ export interface AdapterDefinition {
120
146
  roles: readonly string[];
121
147
  optional?: boolean;
122
148
  defaultOnRoster?: boolean;
149
+ /** A legacy enable switch gates new starts, not retained sockets or queues. */
150
+ enabledSwitchStartOnly?: boolean;
123
151
  trust: { onPrem: boolean; hubSandboxed: boolean };
124
152
  start: {
125
153
  forms: readonly StartForm[];
@@ -127,17 +155,30 @@ export interface AdapterDefinition {
127
155
  ownsTerminal(form: StartForm): boolean;
128
156
  launch(ctx: LaunchContext): LaunchPlan;
129
157
  create?(ctx: CreateContext): PeerAdapter;
158
+ daemonPort?: DaemonEngine;
130
159
  preflight?(ctx: StartRequest): string | undefined;
131
160
  version?(ctx: VersionContext): string | undefined;
132
161
  conductorStartable: boolean;
162
+ passthrough?: boolean;
133
163
  autoStart?: "pi.auto_start";
164
+ exclusiveStartRefusal?: string;
165
+ refreshTools?: boolean;
166
+ explicitHeadlessMode?: boolean;
167
+ planCommand?(form: StartForm): string;
168
+ acpCommand?(command: readonly string[], model?: string): string[];
169
+ reconfigureWhileAttached?: boolean;
134
170
  unattended?: boolean;
135
171
  hubSwitches: readonly string[];
136
172
  ownedFlags: readonly string[];
137
173
  conductorStartArgs?(args: string[]): { mode?: StartForm } | Error;
138
174
  };
139
175
  identity: { mcp: "acp-session" | "project-file" | "env" | "channel-plugin" };
140
- attach?: { pullOnlyForms?: readonly StartForm[] };
176
+ attach?: {
177
+ pullOnlyForms?: readonly StartForm[];
178
+ recoveryLaunch?(instance: string): Record<string, unknown>;
179
+ pullOnlyNotice?: string;
180
+ admission?: { reservedRefusal: string; watchRefusal: string };
181
+ };
141
182
  permission: {
142
183
  modes: Partial<Record<PermissionMode, NativeModeSpec>>;
143
184
  grants?: Partial<Record<PermissionMode, string>>;
@@ -145,30 +186,79 @@ export interface AdapterDefinition {
145
186
  unmanaged?: string;
146
187
  unmanagedForms?: readonly StartForm[];
147
188
  unmanagedNote?: string;
189
+ unmanagedStatusNote?: string;
190
+ trackUnattended?: boolean;
148
191
  };
149
192
  session: {
193
+ nativeRevert?: boolean;
194
+ crashPlan?(meta: Record<string, unknown>): { resume?: Record<string, string>; how: string; fresh?: Record<string, string>; tui?: true };
195
+ unrecordedTerminal?: "always" | "tui";
196
+ continuityBookmark?(peer: RecoveryPeer, binding?: TerminalBinding): Pick<RecoveryContinuityBookmark, "pi">;
197
+ continuity?: { key: string; parse(value: unknown): unknown | undefined };
150
198
  idField: "sessionId" | "threadId";
151
199
  currentId(peer: PeerAdapter): string | undefined;
152
200
  configDirEnv?: "CODEX_HOME" | "CLAUDE_CONFIG_DIR";
153
- resumeArgv?(binding: TerminalBinding, fresh: boolean): string[];
201
+ resumeArgv?(binding: SessionRef, fresh: boolean): string[];
154
202
  transcriptPath?(binding: TerminalBinding): string | undefined;
155
203
  transcriptExists?(binding: TerminalBinding): boolean | "unknown";
156
204
  zeroTurnFresh?: boolean;
205
+ zeroTurn?(ctx: { stateDir: string; binding: TerminalBinding; phase: "plan" | "attach" | "verify" }): boolean;
206
+ restoreArgs?(peer: RecoveryPeer): Record<string, string>;
207
+ recoveryRefusal?(peer: RecoveryPeer, phase?: "plan" | "restore"): string | undefined;
208
+ personFresh?: boolean;
209
+ missingEvidence?: "rollout" | "transcript";
210
+ /** Fixed source-only compatibility reads used by released recovery coordinators. */
211
+ legacyTargetSupport?: { file: "src/hub/peer-catalog.ts"; exportName: "OPTIONAL_PEERS"; refusal: string };
212
+ legacyHeadlessArgv?: { file: "src/hub/start-mode.ts"; exportName: "START_MODE_PEERS"; argv: readonly string[] };
157
213
  freshOnRestore?: boolean;
214
+ /** The daemon restores recorded startup options before auto-starting this kind. */
158
215
  restoreOnStart?: boolean;
216
+ /** Recovery passes the verified native session id to this kind's start command. */
217
+ resumeById?: boolean;
159
218
  launchKind?: string;
160
219
  launchRecord?: boolean;
161
220
  crashIdentity?(row: unknown): { sessionId?: string; sessionFile?: string };
162
221
  };
163
222
  turnEnd: "adapter" | "native-hook";
164
- budget?: { account: string; reader?: QuotaReader; context?: ContextReader; checkpoint?: boolean };
223
+ reporting?: { tokensNotReported?: boolean };
224
+ /** Legacy native routing policy; external definitions cannot claim these host-specific policies. */
225
+ routing?: {
226
+ localAllowedRefusal?: string;
227
+ longContext?: "backend-limit" | "skip-owner";
228
+ preferredTier?: boolean;
229
+ handoffPriority?: number;
230
+ trace?: "route" | "backend";
231
+ backendLabel?: string;
232
+ };
233
+ budget?: { account: string; quotaExempt?: boolean; instrumented?: boolean; reader?: QuotaReader; context?: ContextReader; checkpoint?: boolean; resetSoftTokensOnResume?: boolean };
234
+ configuration?: {
235
+ schema?(ports: NativeConfigValidators): Record<string, NativeConfigCheck>;
236
+ routingSchema?(ports: NativeConfigValidators): Record<string, NativeConfigCheck>;
237
+ settings?: readonly SettingDef[];
238
+ restartReasons?: Readonly<Record<string, string>>;
239
+ validateEffective?(raw: Record<string, unknown>, fail: (key: string, reason: string) => never): void;
240
+ };
165
241
  capabilities?: { manifest?(peer: PeerAdapter): Promise<CapabilityManifest>; watch?: boolean };
166
- init?: { templateFragment?: string; mcpFile?: string };
242
+ init?: {
243
+ templateFragment?: string;
244
+ mcpFile?: string;
245
+ /** Pure planner; the CLI host owns every write and cleanup. */
246
+ mcpPlan?(ctx: { projectRoot: string; stateDir: string; entrypoint: string; form: StartForm }): { file: string; tracked: boolean; changed: boolean; action: string };
247
+ };
167
248
  cli: {
168
249
  command: string;
250
+ /** Restrictions only; core identity remains authoritative. */
251
+ authority?: "person" | "conductor";
252
+ personOnlyFlags?: readonly string[];
169
253
  startWords(form: StartForm): string[];
170
254
  stopNotice?(form: StartForm, owner: PeerAdapter | undefined): string;
255
+ previewArgs?: readonly string[];
256
+ retainedTerminalNotice?(instance: string, form: StartForm | undefined, resumeCommand: string): string | undefined;
257
+ rosterReturnNotice?(resumeCommand: string): string;
171
258
  };
172
- display: { palette: { fg: string; bg: string } };
259
+ display: { palette: { fg: string; bg: string; tone?: string; ansi?: string } };
173
260
  hooks?: Partial<NativeHooks>;
174
261
  }
262
+
263
+ /** Explicit project declarations, passed by the bound host. No process-global instance registration. */
264
+ export type KindMap = Readonly<Record<string, { readonly kind: string }>>;