@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.
- package/CHANGELOG.md +9 -0
- package/README.md +3 -3
- package/docs/agent-notes/adapters.md +7 -3
- package/docs/agent-notes/budget.md +5 -3
- package/docs/agent-notes/daemon.md +3 -3
- package/docs/agent-notes/tasks.md +2 -1
- package/docs/agent-notes/tests.md +2 -1
- package/docs/operations.md +20 -10
- package/docs/quickstart.md +4 -2
- package/docs/security.md +4 -1
- package/docs/smoke.md +1 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +246 -4
- package/docs/verification/2026-10-18-0.12.34.md +48 -0
- package/docs/verified.json +29 -18
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +22882 -16496
- package/src/adapters/codex-appserver.ts +81 -11
- 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 +26 -0
- package/src/adapters/definitions/native-cli-entry-jcode.ts +24 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +52 -0
- package/src/adapters/definitions/native-cli-entry-local.ts +14 -0
- package/src/adapters/definitions/native-cli-entry-pi.ts +51 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +20 -0
- package/src/adapters/definitions/native-cli-entry-types.ts +40 -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.ts +8 -4
- package/src/adapters/registry-types.ts +97 -10
- package/src/adapters/registry.ts +103 -3
- package/src/cli/console-state.ts +5 -5
- package/src/cli/identity.ts +6 -5
- package/src/cli/jcode-mcp.ts +2 -58
- package/src/cli/launch.ts +14 -100
- package/src/cli/main.ts +35 -272
- package/src/cli/output.ts +3 -2
- package/src/cli/preview.ts +14 -125
- package/src/cli/recovery-guidance.ts +17 -13
- package/src/cli/setup.ts +2 -80
- package/src/cli/shell-quote.ts +3 -0
- package/src/cli/terminal-recovery.ts +17 -10
- package/src/cli/upgrade-interactive.ts +4 -2
- package/src/cli/upgrade-restore-state.ts +2 -1
- package/src/cli/upgrade-runtime.ts +92 -141
- package/src/cli/upgrade.ts +8 -7
- package/src/hub/budget.ts +6 -30
- package/src/hub/conductor.ts +8 -4
- package/src/hub/config-apply.ts +9 -13
- package/src/hub/config-reload.ts +7 -2
- package/src/hub/context-window.ts +3 -27
- package/src/hub/crash.ts +2 -35
- package/src/hub/daemon.ts +226 -232
- package/src/hub/execution-budget.ts +2 -1
- package/src/hub/hub-tools.ts +3 -3
- package/src/hub/peer-catalog.ts +4 -13
- package/src/hub/permission-mode.ts +9 -12
- package/src/hub/recovery-continuity.ts +6 -5
- package/src/hub/report.ts +9 -8
- package/src/hub/roster.ts +6 -7
- package/src/hub/routing.ts +21 -10
- package/src/hub/settings.ts +6 -5
- package/src/hub/settled-roles.ts +1 -1
- package/src/hub/start-mode.ts +8 -6
- package/src/hub/tasks.ts +13 -10
- package/src/hub/tooling.ts +4 -60
- package/src/hub/ui.ts +3 -1
- package/src/memory/recall.ts +4 -5
- package/src/models/route/signals.ts +3 -1
- package/src/pi/launch.ts +2 -1
- package/src/ui/index.html +3 -2
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +16 -6
- 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(
|
|
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,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:
|
|
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
|
}
|
package/src/adapters/registry.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
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
|
-
...
|
|
31
|
+
...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind !== "jcode"),
|
|
18
32
|
QWEN_DEFINITION,
|
|
19
|
-
...
|
|
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";
|
package/src/cli/console-state.ts
CHANGED
|
@@ -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 "../
|
|
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) &&
|
|
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
|
|
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
|
|
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(" ");
|
package/src/cli/identity.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { recoveryPresentationFlag } from "./recovery-presentation-options.ts";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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 (
|
|
76
|
-
if (
|
|
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", "
|
|
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}`;
|
package/src/cli/jcode-mcp.ts
CHANGED
|
@@ -1,58 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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";
|