@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
|
@@ -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/
|
|
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("
|
|
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
|
-
|
|
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:
|
|
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);
|
package/src/adapters/pi.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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>>):
|
|
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?: {
|
|
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:
|
|
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
|
-
|
|
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?: {
|
|
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 }>>;
|