@staix/agent-hub 0.12.33 → 0.12.34

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 (100) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +3 -3
  3. package/docs/agent-notes/adapters.md +7 -3
  4. package/docs/agent-notes/budget.md +5 -3
  5. package/docs/agent-notes/daemon.md +3 -3
  6. package/docs/agent-notes/tasks.md +2 -1
  7. package/docs/agent-notes/tests.md +2 -1
  8. package/docs/operations.md +20 -10
  9. package/docs/quickstart.md +4 -2
  10. package/docs/security.md +4 -1
  11. package/docs/smoke.md +1 -1
  12. package/docs/specs/2026-09-19-agent-hub-design.md +246 -4
  13. package/docs/verification/2026-10-18-0.12.34.md +48 -0
  14. package/docs/verified.json +29 -18
  15. package/package.json +1 -1
  16. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  17. package/plugins/agent-hub/server.js +22882 -16496
  18. package/src/adapters/codex-appserver.ts +81 -11
  19. package/src/adapters/definitions/builtin-metadata.ts +101 -7
  20. package/src/adapters/definitions/native-cli-claude-launch.ts +90 -0
  21. package/src/adapters/definitions/native-cli-claude-setup.ts +80 -0
  22. package/src/adapters/definitions/native-cli-codex-launch.ts +14 -0
  23. package/src/adapters/definitions/native-cli-doctor.ts +46 -0
  24. package/src/adapters/definitions/native-cli-entry-claude.ts +52 -0
  25. package/src/adapters/definitions/native-cli-entry-codex.ts +26 -0
  26. package/src/adapters/definitions/native-cli-entry-jcode.ts +24 -0
  27. package/src/adapters/definitions/native-cli-entry-kimi.ts +52 -0
  28. package/src/adapters/definitions/native-cli-entry-local.ts +14 -0
  29. package/src/adapters/definitions/native-cli-entry-pi.ts +51 -0
  30. package/src/adapters/definitions/native-cli-entry-qwen.ts +20 -0
  31. package/src/adapters/definitions/native-cli-entry-types.ts +40 -0
  32. package/src/adapters/definitions/native-cli-entry.ts +19 -0
  33. package/src/adapters/definitions/native-cli-jcode-mcp.ts +58 -0
  34. package/src/adapters/definitions/native-cli-launch-common.ts +17 -0
  35. package/src/adapters/definitions/native-cli-pi.ts +33 -0
  36. package/src/adapters/definitions/native-cli-preview-claude.ts +37 -0
  37. package/src/adapters/definitions/native-cli-preview-codex.ts +20 -0
  38. package/src/adapters/definitions/native-cli-preview-common.ts +57 -0
  39. package/src/adapters/definitions/native-cli-preview-jcode.ts +20 -0
  40. package/src/adapters/definitions/native-cli-preview-kimi.ts +28 -0
  41. package/src/adapters/definitions/native-cli-preview-pi.ts +36 -0
  42. package/src/adapters/definitions/native-cli-preview-qwen.ts +22 -0
  43. package/src/adapters/definitions/native-cli-preview.ts +16 -0
  44. package/src/adapters/definitions/native-config-defaults.ts +19 -0
  45. package/src/adapters/definitions/native-config-policy.ts +35 -0
  46. package/src/adapters/definitions/native-context.ts +35 -0
  47. package/src/adapters/definitions/native-daemon-kimi.ts +5 -0
  48. package/src/adapters/definitions/native-daemon-policy.ts +18 -0
  49. package/src/adapters/definitions/native-display.ts +18 -0
  50. package/src/adapters/definitions/native-factories.ts +48 -0
  51. package/src/adapters/definitions/native-policy-aux.ts +130 -0
  52. package/src/adapters/definitions/native-quota.ts +29 -0
  53. package/src/adapters/definitions/native-recovery-continuity.ts +12 -0
  54. package/src/adapters/definitions/native-recovery.ts +88 -0
  55. package/src/adapters/definitions/qwen.ts +8 -4
  56. package/src/adapters/registry-types.ts +97 -10
  57. package/src/adapters/registry.ts +103 -3
  58. package/src/cli/console-state.ts +5 -5
  59. package/src/cli/identity.ts +6 -5
  60. package/src/cli/jcode-mcp.ts +2 -58
  61. package/src/cli/launch.ts +14 -100
  62. package/src/cli/main.ts +35 -272
  63. package/src/cli/output.ts +3 -2
  64. package/src/cli/preview.ts +14 -125
  65. package/src/cli/recovery-guidance.ts +17 -13
  66. package/src/cli/setup.ts +2 -80
  67. package/src/cli/shell-quote.ts +3 -0
  68. package/src/cli/terminal-recovery.ts +17 -10
  69. package/src/cli/upgrade-interactive.ts +4 -2
  70. package/src/cli/upgrade-restore-state.ts +2 -1
  71. package/src/cli/upgrade-runtime.ts +92 -141
  72. package/src/cli/upgrade.ts +8 -7
  73. package/src/hub/budget.ts +6 -30
  74. package/src/hub/conductor.ts +8 -4
  75. package/src/hub/config-apply.ts +9 -13
  76. package/src/hub/config-reload.ts +7 -2
  77. package/src/hub/context-window.ts +3 -27
  78. package/src/hub/crash.ts +2 -35
  79. package/src/hub/daemon.ts +226 -232
  80. package/src/hub/execution-budget.ts +2 -1
  81. package/src/hub/hub-tools.ts +3 -3
  82. package/src/hub/peer-catalog.ts +4 -13
  83. package/src/hub/permission-mode.ts +9 -12
  84. package/src/hub/recovery-continuity.ts +6 -5
  85. package/src/hub/report.ts +9 -8
  86. package/src/hub/roster.ts +6 -7
  87. package/src/hub/routing.ts +21 -10
  88. package/src/hub/settings.ts +6 -5
  89. package/src/hub/settled-roles.ts +1 -1
  90. package/src/hub/start-mode.ts +8 -6
  91. package/src/hub/tasks.ts +13 -10
  92. package/src/hub/tooling.ts +4 -60
  93. package/src/hub/ui.ts +3 -1
  94. package/src/memory/recall.ts +4 -5
  95. package/src/models/route/signals.ts +3 -1
  96. package/src/pi/launch.ts +2 -1
  97. package/src/ui/index.html +3 -2
  98. package/src/ui/office-model.ts +3 -1
  99. package/src/ui/peer-palette.ts +16 -6
  100. package/templates/routing.toml +2 -2
@@ -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,76 @@ 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
  }
@@ -1,10 +1,22 @@
1
- import { BUILTIN_DEFINITIONS_WITHOUT_QWEN } from "./definitions/builtin-metadata.ts";
1
+ import { builtinCliEntries } from "./definitions/native-cli-entry.ts";
2
+ import { builtinPreview } from "./definitions/native-cli-preview.ts";
3
+ import { DAEMON_PORTS } from "./definitions/native-factories.ts";
4
+ export { DAEMON_PORTS };
5
+ import { NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN } from "./definitions/builtin-metadata.ts";
2
6
  import { QWEN_DEFINITION } from "./definitions/qwen.ts";
3
7
  import type { AdapterDefinition } from "./registry-types.ts";
4
8
 
5
9
  export type * from "./registry-types.ts";
6
10
  export const ADAPTER_API = 1;
7
11
 
12
+ /** Ordered compatibility projections stay typed independently of definition implementation inference. */
13
+ export const KNOWN_PEERS = ["claude", "codex", "kimi", "pi", "local"] as const;
14
+ export const OPTIONAL_PEERS = ["qwen", "jcode"] as const;
15
+ export const SUPPORTED_PEERS = [...KNOWN_PEERS, ...OPTIONAL_PEERS] as const;
16
+ export const START_MODE_PEERS = ["pi", "codex", "kimi"] as const;
17
+ export const OFFICE_PEER_ORDER: readonly string[] = KNOWN_PEERS;
18
+
19
+
8
20
  function immutable<T>(value: T, seen = new WeakSet<object>()): T {
9
21
  if (!value || typeof value !== "object" || seen.has(value)) return value;
10
22
  seen.add(value);
@@ -12,18 +24,27 @@ function immutable<T>(value: T, seen = new WeakSet<object>()): T {
12
24
  return Object.freeze(value);
13
25
  }
14
26
 
27
+ const fixedBuiltinDefinitions = new Set<AdapterDefinition>([...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN, QWEN_DEFINITION]);
28
+
15
29
  /** Keep the native catalog's order, independently of the legacy default-role object order. */
16
30
  export const ADAPTERS: readonly AdapterDefinition[] = immutable([
17
- ...BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind !== "jcode"),
31
+ ...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind !== "jcode"),
18
32
  QWEN_DEFINITION,
19
- ...BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind === "jcode"),
33
+ ...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind === "jcode"),
20
34
  ]);
21
35
  const definitions = new Map<string, AdapterDefinition>();
22
36
  for (const definition of ADAPTERS) {
37
+ assertDaemonEngine(definition);
38
+ assertNativeCapabilityScope(definition);
23
39
  if (definition.api !== ADAPTER_API || definitions.has(definition.kind)) throw new Error("invalid builtin adapter registry");
24
40
  definitions.set(definition.kind, definition);
25
41
  }
26
42
 
43
+ /** Native quota handoff platform names, distinct from per-delivery recall injection policy. */
44
+ export const QUOTA_HANDOFF_PLATFORMS: Readonly<Record<string, string>> = Object.freeze(Object.fromEntries(
45
+ ADAPTERS.flatMap(adapter => adapter.hooks?.recallPlatform ? [[adapter.kind, adapter.hooks.recallPlatform]] : []),
46
+ ));
47
+
27
48
  /** A1 registers kinds only. Undeclared suffixes never acquire a native kind or admission. */
28
49
  export function kindOf(id: string): string | undefined {
29
50
  return definitions.has(id) ? id : undefined;
@@ -34,3 +55,82 @@ export function definitionOf(id: string): AdapterDefinition | undefined {
34
55
  export function familyOf(id: string): string | undefined {
35
56
  return definitionOf(id)?.family;
36
57
  }
58
+
59
+ /** Compatibility projection order is observable in default role maps and planner selection. */
60
+ const ROLE_ORDER = ["claude", "codex", "kimi", "local", "pi", "qwen", "jcode"] as const;
61
+ export const PLANNER_PEERS = ["claude", "codex", "kimi", "qwen", "jcode"] as const;
62
+
63
+ /** Callers own their role arrays, independent of immutable native policy. */
64
+ export function defaultPeerRoles(includeOptional = false): Record<string, string[]> {
65
+ return Object.fromEntries(ROLE_ORDER.filter(peer => includeOptional || !definitionOf(peer)!.optional)
66
+ .map(peer => [peer, [...definitionOf(peer)!.roles]]));
67
+ }
68
+
69
+ /** Preserve the tool schema's legacy preferred order without spreading native names into tools. */
70
+ const CONDUCTOR_START_PRIORITY = ["local", "kimi", "pi"] as const;
71
+ export const CONDUCTOR_START_PEERS = [
72
+ ...CONDUCTOR_START_PRIORITY,
73
+ ...ADAPTERS.filter(adapter => !(CONDUCTOR_START_PRIORITY as readonly string[]).includes(adapter.kind)).map(adapter => adapter.kind),
74
+ ];
75
+
76
+ /** A missing definition retains external-id admission; Pi's legacy enabled switch controls its default only. */
77
+ export function defaultRosterEnabled(peer: string, piEnabled: boolean): boolean {
78
+ const definition = definitionOf(peer);
79
+ if (!definition) return true;
80
+ return definition.kind === "pi" ? piEnabled : definition.defaultOnRoster ?? false;
81
+ }
82
+
83
+ /** A factory must name one existing engine; metadata never creates a caller-controlled port. */
84
+ export function assertDaemonEngine(definition: Pick<AdapterDefinition, "start">): void {
85
+ const engine = definition.start.daemonPort;
86
+ if (definition.start.create && engine === undefined || engine !== undefined && !Object.hasOwn(DAEMON_PORTS, engine)) throw new Error("invalid adapter daemon engine");
87
+ }
88
+
89
+ /** Host-global native quirks are not capabilities a config-only or external definition may opt into. */
90
+ export function assertNativeCapabilityScope(definition: AdapterDefinition): void {
91
+ if (fixedBuiltinDefinitions.has(definition)) return;
92
+ const declared = (block: object | undefined, key: string): boolean => block !== undefined && key in block;
93
+ const restricted: [string, boolean][] = [
94
+ ["start.acpCommand", declared(definition.start, "acpCommand")],
95
+ ["attach.admission", declared(definition.attach, "admission")],
96
+ ["budget.resetSoftTokensOnResume", declared(definition.budget, "resetSoftTokensOnResume")],
97
+ ["session.crashIdentity", declared(definition.session, "crashIdentity")],
98
+ ["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
99
+ ["routing", definition.routing !== undefined],
100
+ ];
101
+ const field = restricted.find(([, present]) => present)?.[0];
102
+ if (field) throw new Error(`${field} is restricted to fixed builtin adapter definitions`);
103
+ }
104
+
105
+ /** Ordered catalog projections consumed by generic core hosts. */
106
+ export const STARTABLE_PEERS = SUPPORTED_PEERS;
107
+ export type SupportedPeer = typeof SUPPORTED_PEERS[number];
108
+ export function isSupportedPeer(peer: unknown): peer is SupportedPeer { return typeof peer === "string" && definitionOf(peer) !== undefined; }
109
+ export function isStartablePeer(peer: unknown): peer is SupportedPeer { return typeof peer === "string" && definitionOf(peer)?.start.conductorStartable === true; }
110
+
111
+ /** Body-free browser metadata, derived from the same native participation and managed-tool contracts. */
112
+ export const ADAPTER_DISPLAY = Object.fromEntries(ADAPTERS.map(adapter => [adapter.kind, {
113
+ pullOnlyRead: adapter.attach?.pullOnlyForms?.includes("tui") === true,
114
+ terminalApproval: adapter.trust.hubSandboxed,
115
+ }]));
116
+
117
+ export { NATIVE_CONFIG_BLOCKS, nativeConfigurationDefaults, mergeNativeConfiguration, retainNativeRouting } from "./definitions/native-config-defaults.ts";
118
+
119
+ /** Private fixed-builtin preview implementation; not an adapter extension or configurable callback. */
120
+ export function builtinPreviewOf(id: string) {
121
+ const definition = definitionOf(id);
122
+ return definition ? builtinPreview(definition) : undefined;
123
+ }
124
+
125
+ /** Fixed builtin doctor metadata and pure observed-value projections; the CLI retains all probe effects. */
126
+ export { DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "./definitions/native-cli-doctor.ts";
127
+
128
+ /** Internal fixed builtin CLI entries; public AdapterDefinition and NativeHooks expose no execution extension. */
129
+ export const BUILTIN_CLI_ENTRIES = Object.freeze(builtinCliEntries.map(entry => {
130
+ const definition = definitionOf(entry.command);
131
+ if (!definition) throw new Error("builtin CLI entry has no adapter definition");
132
+ return Object.freeze({ ...entry, definition });
133
+ }));
134
+
135
+ /** Private fixed-builtin stream presentation; does not widen the public palette object. */
136
+ export { nativeStreamTone } from "./definitions/native-display.ts";
@@ -2,7 +2,7 @@ import { consoleNeeds, characterAttentionLine, attentionCommand } from "./attent
2
2
  import { CHARACTERS, petFrame, petSize, petFooterRows, type CharacterId, type PetSize, type PetPose } from "../pet/frame.ts";
3
3
  import { PEER_PALETTE } from "../ui/peer-palette.ts";
4
4
  import { PERMISSION_MODES, permissionBoundary } from "../hub/permission-mode.ts";
5
- import { PLANNER_PEERS } from "../hub/peer-catalog.ts";
5
+ import { PLANNER_PEERS, definitionOf, nativeStreamTone } from "../adapters/registry.ts";
6
6
  import { taskProgress } from "../ui/task-progress.ts";
7
7
  import type { ProgressStage } from "../ui/task-progress.ts";
8
8
  import { sanitize } from "../hub/envelope.ts";
@@ -114,7 +114,6 @@ export function paint(line: Span[], color: boolean): string {
114
114
  export function streamTokens(value: string, eventTone?: Tone, kind?: string): Span[] {
115
115
  const text = terminalText(value);
116
116
  if (kind === "command" || kind === "console") return [{ text }];
117
- const peers: Readonly<Record<string, Tone>> = { claude: "peerClaude", codex: "peerCodex" };
118
117
  // Hub-written peer slots only; words such as local/pi/hub in a title do not identify a speaker.
119
118
  const peerSlots = new Set<number>();
120
119
  const leading = text.match(/^\s*(?:[.?*!]\s+)?([A-Za-z_][A-Za-z_0-9-]*)(?=:| is | asks permission:| permission )/);
@@ -143,6 +142,7 @@ export function streamTokens(value: string, eventTone?: Tone, kind?: string): Sp
143
142
  const gap = text.slice(end, start);
144
143
  if (gap) out.push({ text: gap });
145
144
  let tone: Tone | undefined;
145
+ const peerTone = nativeStreamTone(token);
146
146
  // ponytail: legacy notices have no typed reference ranges. Classify task/review prefixes textually;
147
147
  // assign/which #N remains an issue reference until notices carry structured reference spans.
148
148
  if (token.startsWith("#")) {
@@ -152,7 +152,7 @@ export function streamTokens(value: string, eventTone?: Tone, kind?: string): Sp
152
152
  }
153
153
  else if (start === taskKeywordAt) tone = "taskKeyword";
154
154
  else if (/^[0-9]/.test(token) && !permissionIds.has(start)) tone = "number";
155
- else if (peerSlots.has(start) && Object.hasOwn(peers, token)) tone = peers[token];
155
+ else if (peerSlots.has(start) && peerTone) tone = peerTone;
156
156
  else if (start === markerAt || start === stateAt || (route && token === "->" && start < route[0].length)) tone = eventTone;
157
157
  out.push({ text: token, ...(tone ? { tone } : {}) });
158
158
  previousToken = token; previousStart = start; end = start + token.length;
@@ -749,7 +749,7 @@ export function cells(s: ConsoleState, row: any, selected: boolean, now: number,
749
749
  : row.paused === "manual" ? "user" : row.paused ? String(row.paused).split(":")[0] : "-";
750
750
  return [id(row.planner ? `${row.id} (planner)` : row.id, row.state === "offline" ? undefined : "info"), span(row.rostered === false ? `${row.state} off roster` : row.state, stateTone(row.state)),
751
751
  ...(showPermissionModes(s) ? [span(row.permissionMode && row.permissionMode !== "ask" ? row.permissionMode : "-", row.permissionMode === "never-ask" ? "attention" : undefined)] : []),
752
- span(row.pullOnly && row.id === "kimi" ? "pull-only" : row.toolsOnly ? "tools-only" : row.attached === false ? "detached" : "attached", row.toolsOnly ? "attention" : undefined),
752
+ span(row.pullOnly && definitionOf(row.id)?.attach?.pullOnlyForms?.includes("tui") ? "pull-only" : row.toolsOnly ? "tools-only" : row.attached === false ? "detached" : "attached", row.toolsOnly ? "attention" : undefined),
753
753
  span(count(row.queued)), span(count(row.queuedImportant), row.queuedImportant ? "attention" : undefined),
754
754
  span(count(row.needsReview), row.needsReview ? "failure" : undefined), span(paused, paused === "-" ? undefined : "attention"),
755
755
  span(b?.windows?.length ? b.windows.map((w: any) => `${w.id} ${Math.round(w.used * 100)}%${typeof w.resetsAt === "number" ? ` ${relative(w.resetsAt, now)}` : ""}`).join(", ") : "-"),
@@ -890,7 +890,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
890
890
  if (p.permissionMode && p.permissionMode !== "ask") summary.push(span(` ${p.permissionMode}`, "attention"));
891
891
  if (p.needsReview) summary.push(span(` review${p.needsReview}`, "failure"));
892
892
  if (p.paused) summary.push(span(" paused", "attention"));
893
- if (p.toolsOnly) summary.push(span(id === "kimi" ? ` pull-only, read ${typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never"}` : " tools-only: ahub claude", "attention"));
893
+ if (p.toolsOnly) summary.push(span(definitionOf(id)?.attach?.pullOnlyForms?.includes("tui") ? ` pull-only, read ${typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never"}` : " tools-only: ahub claude", "attention"));
894
894
  }
895
895
  if (s.pendingRestart?.length) summary.push(span(`${summary.length ? " | " : ""}restart pending: ${s.pendingRestart.map(row => row.key).join(", ")}`, "attention"));
896
896
  const quotas = Object.entries(s.budget).map(([id, b]) => `${id}:${b.windows?.map((w: any) => `${w.id} ${Math.round(w.used * 100)}%`).join("/") ?? "?"}`).join(" ");
@@ -1,5 +1,5 @@
1
1
  import { recoveryPresentationFlag } from "./recovery-presentation-options.ts";
2
- import { isSupportedPeer } from "../hub/peer-catalog.ts";
2
+ import { definitionOf } from "../adapters/registry.ts";
3
3
 
4
4
  /** Recovery reads this declaration before delegating to another package's CLI. */
5
5
  export const LIVE_TEST_SUPPORTED = true;
@@ -52,7 +52,8 @@ export type PeerCommandAccess = "allowed" | "conductor" | "console";
52
52
  /** A closed list: unknown commands cannot acquire console authority through an agent shell. */
53
53
  export function classifyPeerCommand(command: string, args: readonly string[]): PeerCommandAccess {
54
54
  if (command === "recovery" && recoveryAction(args) === "reattach") return "console";
55
- if (command === "pi" && args.includes("--replace-session")) return "console";
55
+ const native = definitionOf(command);
56
+ if (native?.cli.personOnlyFlags?.some(flag => args.includes(flag))) return "console";
56
57
  if (command === "pet" && args.includes("--window")) return "console";
57
58
  if (command === "permission" || command === "stop" || command === "watch") return "console";
58
59
  if (args.some((arg) => arg === "--as-user" || arg.startsWith("--as-user="))) return "console";
@@ -72,8 +73,8 @@ export function classifyPeerCommand(command: string, args: readonly string[]): P
72
73
  // The subcommand is the first argument that is not --json, which the command accepts anywhere.
73
74
  if (command === "bench") return args.find((arg) => arg !== "--json") === "run" ? "console" : "allowed";
74
75
  if (command === "budget") return !args.length || args.every((arg) => arg === "--json" || arg === "--full" || arg.startsWith("--color=")) ? "allowed" : "console";
75
- if (command === "jcode") return "console"; // the person launcher alone writes its project MCP entry
76
- if (isSupportedPeer(command) || ["pause", "resume"].includes(command)) return "conductor";
76
+ if (native?.cli.authority === "person") return "console";
77
+ if (native || ["pause", "resume"].includes(command)) return "conductor";
77
78
  if (command === "board" && args[0] === "import") return "console";
78
79
  if (["help", "version", "--version", "say", "status", "pet", "board", "report", "turns", "review", "remember", "facts", "check-path"].includes(command)) return "allowed";
79
80
  return "console";
@@ -84,7 +85,7 @@ export function cliCommandLabel(command: string, args: readonly string[]): strin
84
85
  if (command === "backlog") return ["list", "add", "pull", "pull-ready", "split", "return", "plan", "pin", "undo"].includes(args[0] ?? "") ? `backlog ${args[0]}` : "backlog";
85
86
  if (command === "board" && args[0] === "import") return "board import";
86
87
  if (command === "release") return ["new", "list", "show", "plan", "activate", "freeze", "ship", "drop"].includes(args[0] ?? "") ? `release ${args[0]}` : "release";
87
- if (!["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "claude", "codex", "kimi", "qwen", "jcode", "pi", "local", "stop", "models", "say", "queue", "tail", "watch", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "permission", "settings", "roster", "status", "pet", "logs", "export", "report", "research", "memory", "goal", "bench", "sdlc", "role", "facts", "check-path", "turns", "undo", "kill", "reset", "doctor"].includes(command)) return "unknown";
88
+ if (!definitionOf(command) && !["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "stop", "models", "say", "queue", "tail", "watch", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "permission", "settings", "roster", "status", "pet", "logs", "export", "report", "research", "memory", "goal", "bench", "sdlc", "role", "facts", "check-path", "turns", "undo", "kill", "reset", "doctor"].includes(command)) return "unknown";
88
89
  if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
89
90
  const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
90
91
  if (sub && ["propose", "show", "assign", "unassign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "reattach", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "dedupe", "decay", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
@@ -1,58 +1,2 @@
1
- import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { randomUUID } from "node:crypto";
3
- import { join } from "node:path";
4
- import { hubGitSync } from "../hub/git.ts";
5
-
6
- export const JCODE_MCP_FILE = ".jcode/mcp.json", JCODE_MCP_NAME = "agent-hub-jcode";
7
- type Document = Record<string, unknown>;
8
- const object = (value: unknown): value is Document => !!value && typeof value === "object" && !Array.isArray(value);
9
-
10
- function document(file: string): Document {
11
- const stat = lstatSync(file, { throwIfNoEntry: false });
12
- if (!stat) return {};
13
- if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 256 * 1024) throw new Error("Jcode MCP configuration must be a bounded regular file, not a link");
14
- let value: unknown; try { value = JSON.parse(readFileSync(file, "utf8")); } catch { throw new Error("Jcode MCP configuration is invalid JSON"); }
15
- if (!object(value)) throw new Error("Jcode MCP configuration must be an object");
16
- if (value.servers !== undefined && value.mcpServers !== undefined) throw new Error("Jcode MCP configuration has ambiguous server maps");
17
- for (const key of ["servers", "mcpServers"]) if (value[key] !== undefined && !object(value[key])) throw new Error("Jcode MCP server map must be an object");
18
- return value;
19
- }
20
- const map = (doc: Document): Document => (doc.mcpServers ?? doc.servers ?? {}) as Document;
21
- const owned = (entry: unknown, cwd: string): entry is Document => object(entry) && object(entry.env) && entry.env.AGENTHUB_MODE === "tools" && entry.env.AGENTHUB_PEER_ID === "jcode" && entry.env.AGENTHUB_PROJECT_DIR === cwd;
22
-
23
- export function jcodeMcpEntry(cwd: string, stateDir: string, bundle: string) {
24
- return { command: "bun", args: ["run", bundle], shared: false, env: { AGENTHUB_MODE: "tools", AGENTHUB_DELIVERY_ONLY: "true", AGENTHUB_PEER_ID: "jcode", AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } };
25
- }
26
- /** The native loader reads later project files last. Never shadow a foreign definition. */
27
- function inspect(cwd: string): Document {
28
- const directory = lstatSync(join(cwd, ".jcode"), { throwIfNoEntry: false });
29
- if (directory && (!directory.isDirectory() || directory.isSymbolicLink())) throw new Error(".jcode must be a project directory, not a link");
30
- for (const relative of [".mcp.json", ".claude/mcp.json"]) {
31
- const parent = relative.startsWith(".claude/") ? lstatSync(join(cwd, ".claude"), { throwIfNoEntry: false }) : undefined;
32
- if (parent?.isSymbolicLink()) throw new Error("Jcode MCP override layer is a link; inspect it before launching");
33
- if (Object.hasOwn(map(document(join(cwd, relative))), JCODE_MCP_NAME)) throw new Error("a later project MCP layer overrides agent-hub-jcode; resolve it in your editor");
34
- }
35
- return document(join(cwd, JCODE_MCP_FILE));
36
- }
37
- export function jcodeMcpReady(cwd: string, stateDir: string, bundle: string): boolean {
38
- const entry = map(inspect(cwd))[JCODE_MCP_NAME];
39
- return JSON.stringify(entry) === JSON.stringify(jcodeMcpEntry(cwd, stateDir, bundle));
40
- }
41
- /** Only the console launcher calls this; daemon starts use jcodeMcpReady read-only. */
42
- export function configureJcodeMcp(cwd: string, stateDir: string, bundle: string, remove = false): { changed: boolean; tracked: boolean } {
43
- const current = inspect(cwd), servers = { ...map(current) }, existing = servers[JCODE_MCP_NAME];
44
- if (existing !== undefined && !owned(existing, cwd)) throw new Error("agent-hub-jcode is a foreign MCP entry; it was not changed");
45
- if (remove) delete servers[JCODE_MCP_NAME]; else servers[JCODE_MCP_NAME] = jcodeMcpEntry(cwd, stateDir, bundle);
46
- const key = current.servers !== undefined ? "servers" : "mcpServers";
47
- const next = { ...current, ...(current[key] !== undefined || !remove ? { [key]: servers } : {}) };
48
- const tracked = hubGitSync(["ls-files", "--error-unmatch", "--", JCODE_MCP_FILE], { cwd, encoding: "utf8", timeout: 3000 }).status === 0;
49
- if (JSON.stringify(next) === JSON.stringify(current)) return { changed: false, tracked };
50
- mkdirSync(join(cwd, ".jcode"), { recursive: true });
51
- const file = join(cwd, JCODE_MCP_FILE), temporary = `${file}.${randomUUID()}.tmp`;
52
- try {
53
- const fd = openSync(temporary, "wx", 0o600);
54
- try { writeFileSync(fd, `${JSON.stringify(next, null, 2)}\n`); } finally { closeSync(fd); }
55
- renameSync(temporary, file);
56
- } finally { if (existsSync(temporary)) unlinkSync(temporary); }
57
- return { changed: true, tracked };
58
- }
1
+ /** Compatibility exports; the project MCP layer is native adapter policy. */
2
+ export { JCODE_MCP_FILE, JCODE_MCP_NAME, jcodeMcpEntry, jcodeMcpReady, configureJcodeMcp } from "../adapters/definitions/native-cli-jcode-mcp.ts";