@staix/agent-hub 0.12.43 → 0.12.45
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 +38 -3
- package/README.md +3 -3
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/docs/adding-an-ai-tool.md +228 -0
- package/docs/agent-notes/README.md +14 -0
- package/docs/agent-notes/adapters.md +61 -42
- package/docs/agent-notes/budget.md +2 -2
- package/docs/agent-notes/daemon.md +16 -10
- package/docs/agent-notes/local-worker.md +4 -3
- package/docs/agent-notes/tasks.md +13 -6
- package/docs/agent-notes/tests.md +9 -7
- package/docs/operations.md +163 -12
- package/docs/quickstart.md +10 -2
- package/docs/sdlc.md +26 -8
- package/docs/security.md +11 -6
- package/docs/specs/2026-09-19-agent-hub-design.md +273 -8
- package/docs/verification/2026-10-21-0.12.44.md +54 -0
- package/docs/verification/2026-10-21-469-r1-dashboard-tokens.md +43 -0
- package/docs/verification/2026-10-21-469-r2-dashboard-shell.md +54 -0
- package/docs/verification/2026-10-21-469-r3-dashboard-board.md +51 -0
- package/docs/verification/2026-10-21-469-r4-dashboard-views.md +77 -0
- package/docs/verification/2026-10-21-469-r6-dashboard-motion-states.md +82 -0
- package/docs/verification/2026-10-21-grok-config-acp-t0.md +30 -0
- package/docs/verification/2026-10-22-0.12.45.md +60 -0
- package/docs/verification/assets/469-r1-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r1-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-agents-768-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1024-light.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r2-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-board-768-light.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-board-390-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-390-dark.png +0 -0
- package/docs/verification/assets/469-r3-plan-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-plan-390-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-memory-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-message-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-390-light.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-light.png +0 -0
- package/docs/verification/assets/469-r6-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-toasts-390-light.png +0 -0
- package/docs/verified.json +38 -20
- package/package.json +5 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +10287 -9560
- package/src/adapters/acp-owned-group.ts +124 -0
- package/src/adapters/acp.ts +92 -25
- package/src/adapters/adapter-api.ts +2 -0
- package/src/adapters/agy/definition.ts +4 -4
- package/src/adapters/agy/entry.ts +4 -1
- package/src/adapters/channel-peer.ts +143 -0
- package/src/adapters/claude-channel.ts +21 -12
- package/src/adapters/codex-appserver.ts +17 -2
- package/src/adapters/configured-acp.ts +124 -0
- package/src/adapters/conformance/acp.ts +114 -0
- package/src/adapters/conformance/codex.ts +191 -0
- package/src/adapters/conformance/enrollment.ts +43 -0
- package/src/adapters/conformance/factories.ts +17 -0
- package/src/adapters/conformance/fake-paths.ts +12 -0
- package/src/adapters/conformance/fakes/acp-server.ts +267 -0
- package/src/adapters/conformance/fakes/acp-watchdog-agent.ts +10 -0
- package/src/adapters/conformance/fakes/app-server.ts +119 -0
- package/src/adapters/conformance/fakes/codex-bin.ts +15 -0
- package/src/adapters/conformance/fakes/jcode-agent.ts +91 -0
- package/src/adapters/conformance/fakes/model-server.ts +39 -0
- package/src/adapters/conformance/fakes/pi-rpc.ts +32 -0
- package/src/adapters/conformance/index.ts +11 -0
- package/src/adapters/conformance/kit.ts +50 -0
- package/src/adapters/conformance/local-pi.ts +100 -0
- package/src/adapters/conformance/owned-acp.ts +44 -0
- package/src/adapters/definitions/builtin-metadata.ts +60 -39
- package/src/adapters/definitions/native-channel-state.ts +41 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +9 -1
- package/src/adapters/definitions/native-cli-entry.ts +7 -19
- package/src/adapters/definitions/native-cli-preview-qwen.ts +1 -1
- package/src/adapters/definitions/native-cli-preview.ts +2 -11
- package/src/adapters/definitions/native-identity-defaults.ts +14 -0
- package/src/adapters/definitions/qwen.ts +7 -9
- package/src/adapters/definitions/reviewer-variants.ts +3 -7
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-module-scan.ts +126 -0
- package/src/adapters/external-modules.ts +240 -0
- package/src/adapters/jcode.ts +4 -0
- package/src/adapters/local-worker.ts +4 -0
- package/src/adapters/pi.ts +6 -0
- package/src/adapters/public.ts +7 -0
- package/src/adapters/registry-types.ts +28 -16
- package/src/adapters/registry.ts +115 -17
- package/src/adapters/reviewer-print.ts +4 -0
- package/src/cli/agy-mcp.ts +3 -2
- package/src/cli/configured-acp-entry.ts +58 -0
- package/src/cli/console-state.ts +9 -4
- package/src/cli/facts-hook.ts +2 -1
- package/src/cli/help.ts +27 -16
- package/src/cli/identity.ts +8 -7
- package/src/cli/launch.ts +5 -4
- package/src/cli/main.ts +101 -32
- package/src/cli/memory.ts +13 -10
- package/src/cli/output.ts +1 -0
- package/src/cli/preview.ts +9 -0
- package/src/cli/recovery-guidance.ts +1 -1
- package/src/cli/terminal-recovery.ts +16 -13
- package/src/cli/upgrade-labels.ts +1 -1
- package/src/cli/upgrade-presentation.ts +1 -1
- package/src/cli/upgrade-restore-state.ts +6 -4
- package/src/cli/upgrade-runtime.ts +25 -7
- package/src/cli/upgrade.ts +3 -2
- package/src/hub/board.ts +37 -8
- package/src/hub/budget.ts +9 -2
- package/src/hub/bus.ts +6 -2
- package/src/hub/claude-state.ts +2 -37
- package/src/hub/conductor.ts +8 -6
- package/src/hub/config-apply.ts +37 -17
- package/src/hub/config-reload.ts +42 -11
- package/src/hub/config-trust.ts +1 -1
- package/src/hub/context-window.ts +6 -0
- package/src/hub/control-client.ts +4 -1
- package/src/hub/crash.ts +8 -6
- package/src/hub/daemon.ts +412 -328
- package/src/hub/facts.ts +76 -13
- package/src/hub/hub-tools.ts +57 -5
- package/src/hub/instance-config.ts +33 -12
- package/src/hub/instance-execution.ts +23 -11
- package/src/hub/instances.ts +47 -22
- package/src/hub/lifecycle-schema.ts +5 -3
- package/src/hub/memory-profile.ts +16 -3
- package/src/hub/no-writes.ts +50 -0
- package/src/hub/peers.ts +9 -0
- package/src/hub/permission-mode.ts +9 -7
- package/src/hub/plan-conformance.ts +83 -0
- package/src/hub/review-findings.ts +104 -0
- package/src/hub/roster.ts +35 -6
- package/src/hub/routing.ts +21 -20
- package/src/hub/sdlc-queue.ts +19 -4
- package/src/hub/sdlc-release.ts +16 -1
- package/src/hub/sdlc.ts +27 -12
- package/src/hub/settings.ts +54 -16
- package/src/hub/snapshots.ts +32 -6
- package/src/hub/start-mode.ts +10 -8
- package/src/hub/task-sweep.ts +8 -2
- package/src/hub/tasks.ts +358 -60
- package/src/hub/ui.ts +2 -2
- package/src/memory/capture.ts +2 -1
- package/src/ui/index.html +563 -77
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +44 -0
- package/templates/AGENTS.block.md +1 -1
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +1 -1
- package/templates/profiles/writing.json +1 -1
|
@@ -16,6 +16,9 @@ import type { LocalOptions } from "./local-worker.ts";
|
|
|
16
16
|
import type { PiOptions } from "./pi.ts";
|
|
17
17
|
import type { ClaudePrintOptions } from "./claude-print.ts";
|
|
18
18
|
|
|
19
|
+
import type { BuiltinCliEntry } from "./definitions/native-cli-entry-types.ts";
|
|
20
|
+
import type { NativePreviewBuilder } from "./definitions/native-cli-preview-common.ts";
|
|
21
|
+
|
|
19
22
|
export type StartForm = "tui" | "headless";
|
|
20
23
|
export type DaemonEngine = "codex-appserver" | "acp" | "owned-acp" | "pi-rpc" | "local-worker" | "claude-print" | "reviewer-print";
|
|
21
24
|
|
|
@@ -90,6 +93,7 @@ export interface VersionContext {
|
|
|
90
93
|
env: Readonly<Record<string, string>>;
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
/** Compatibility type for frozen A1 permission metadata, not a runtime permission port. */
|
|
93
97
|
export type NativeModeSpec =
|
|
94
98
|
| { kind: "acp-mode"; modeId: string }
|
|
95
99
|
| { kind: "codex-approval"; policy: string }
|
|
@@ -100,7 +104,6 @@ export type NativeModeSpec =
|
|
|
100
104
|
export interface QuotaReader {
|
|
101
105
|
source: string;
|
|
102
106
|
read(value: unknown): UsageWindow[];
|
|
103
|
-
resetOnWindow?: boolean;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
export interface ContextReader {
|
|
@@ -145,12 +148,7 @@ export interface NativeFactsPort {
|
|
|
145
148
|
|
|
146
149
|
/** Only the frozen native seams are extensible; a definition cannot add an arbitrary daemon callback. */
|
|
147
150
|
export interface NativeHooks {
|
|
148
|
-
agentIdentity(init: unknown): void;
|
|
149
|
-
onAttach(instance: string): void;
|
|
150
|
-
onDetach(instance: string): void;
|
|
151
151
|
factsHook: NativeFactsPort;
|
|
152
|
-
conversationRevert(turnId: string): Promise<void>;
|
|
153
|
-
replaceSession(req: StartRequest): Promise<unknown>;
|
|
154
152
|
pluginInventory(ctx: { home: string; cwd?: string; env: Readonly<Record<string, string | undefined>> }): Addon[];
|
|
155
153
|
recallSource: string;
|
|
156
154
|
recallInjected: boolean;
|
|
@@ -182,8 +180,8 @@ export interface AdapterDefinition {
|
|
|
182
180
|
start: {
|
|
183
181
|
forms: readonly StartForm[];
|
|
184
182
|
defaultForm: StartForm;
|
|
185
|
-
|
|
186
|
-
launch(ctx: LaunchContext): LaunchPlan;
|
|
183
|
+
/** A real native argv planner; absent when startup belongs to a separate verified host port. */
|
|
184
|
+
launch?(ctx: LaunchContext): LaunchPlan;
|
|
187
185
|
create?(ctx: CreateContext): PeerAdapter;
|
|
188
186
|
daemonPort?: DaemonEngine;
|
|
189
187
|
preflight?(ctx: StartRequest): string | undefined;
|
|
@@ -203,16 +201,20 @@ export interface AdapterDefinition {
|
|
|
203
201
|
conductorStartArgs?(args: string[]): { mode?: StartForm } | Error;
|
|
204
202
|
};
|
|
205
203
|
identity: { mcp: "acp-session" | "project-file" | "env" | "channel-plugin" };
|
|
204
|
+
/** Declaration admission only; execution-root and runtime owner proof remain host responsibilities. */
|
|
205
|
+
instances?: { mode: "external" | "headless"; roles?: readonly string[] };
|
|
206
206
|
attach?: {
|
|
207
|
+
/** The authenticated external hello may claim this id; independent of MCP configuration shape. */
|
|
208
|
+
externalHello?: boolean;
|
|
207
209
|
pullOnlyForms?: readonly StartForm[];
|
|
208
210
|
recoveryLaunch?(instance: string): Record<string, unknown>;
|
|
209
211
|
pullOnlyNotice?: string;
|
|
210
212
|
admission?: { reservedRefusal: string; watchRefusal: string };
|
|
211
213
|
};
|
|
212
214
|
permission: {
|
|
213
|
-
|
|
215
|
+
/** Consumed configured-ACP mode capability; native runtime and instance ceiling use the same validated mapping. */
|
|
216
|
+
genericAcp?: { modes?: Partial<Record<PermissionMode, string>>; readOnly?: string };
|
|
214
217
|
grants?: Partial<Record<PermissionMode, string>>;
|
|
215
|
-
readOnly?: NativeModeSpec;
|
|
216
218
|
unmanaged?: string;
|
|
217
219
|
unmanagedForms?: readonly StartForm[];
|
|
218
220
|
unmanagedNote?: string;
|
|
@@ -273,14 +275,14 @@ export interface AdapterDefinition {
|
|
|
273
275
|
validateEffective?(raw: Record<string, unknown>, fail: (key: string, reason: string) => never): void;
|
|
274
276
|
};
|
|
275
277
|
capabilities?: { manifest?(peer: PeerAdapter): Promise<CapabilityManifest>; watch?: boolean };
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
mcpFile?: string;
|
|
279
|
-
/** Pure planner; the CLI host owns every write and cleanup. */
|
|
280
|
-
mcpPlan?(ctx: { projectRoot: string; stateDir: string; entrypoint: string; form: StartForm }): { file: string; tracked: boolean; changed: boolean; action: string };
|
|
281
|
-
};
|
|
278
|
+
/** Stable host projection order and planner eligibility, independent of default task roles. */
|
|
279
|
+
projection?: { roleRank?: number; startRank?: number; helpRank?: number; plannerEligible?: boolean };
|
|
282
280
|
cli: {
|
|
283
281
|
command: string;
|
|
282
|
+
/** Fixed builtin CLI ports; project definitions cannot supply these callbacks. */
|
|
283
|
+
entry?: Readonly<Omit<BuiltinCliEntry, "command">>;
|
|
284
|
+
preview?: NativePreviewBuilder;
|
|
285
|
+
usage?: readonly (readonly [usage: string, description: string])[];
|
|
284
286
|
/** Configuration leaves that select this peer's next native launch command or provider profile. */
|
|
285
287
|
launchConfigKeys?: readonly string[];
|
|
286
288
|
/** Restrictions only; core identity remains authoritative. */
|
|
@@ -302,3 +304,13 @@ export interface AdapterDefinition {
|
|
|
302
304
|
|
|
303
305
|
/** Explicit project declarations, passed by the bound host. No process-global instance registration. */
|
|
304
306
|
export type KindMap = Readonly<Record<string, { readonly kind: string; readonly roles?: readonly string[]; readonly start_mode?: "tui" | "headless"; readonly review_mode?: "fresh" }>>;
|
|
307
|
+
|
|
308
|
+
/** Immutable per-project resolver; exported globals remain builtin-only. */
|
|
309
|
+
export interface ProjectAdapterCatalog {
|
|
310
|
+
readonly definitions: readonly AdapterDefinition[];
|
|
311
|
+
readonly externalDefinitions: readonly AdapterDefinition[];
|
|
312
|
+
definitionOf(id: string, instances?: KindMap): AdapterDefinition | undefined;
|
|
313
|
+
kindOf(id: string, instances?: KindMap): string | undefined;
|
|
314
|
+
familyOf(id: string, instances?: KindMap): string | undefined;
|
|
315
|
+
isSupportedPeer(id: unknown, instances?: KindMap): id is string;
|
|
316
|
+
}
|
package/src/adapters/registry.ts
CHANGED
|
@@ -6,18 +6,14 @@ import { DAEMON_PORTS } from "./definitions/native-factories.ts";
|
|
|
6
6
|
export { DAEMON_PORTS };
|
|
7
7
|
import { NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN } from "./definitions/builtin-metadata.ts";
|
|
8
8
|
import { QWEN_DEFINITION } from "./definitions/qwen.ts";
|
|
9
|
-
import
|
|
9
|
+
import { configuredAcpDefinitions } from "./configured-acp.ts";
|
|
10
|
+
import type { AdapterDefinition, KindMap, ProjectAdapterCatalog } from "./registry-types.ts";
|
|
10
11
|
import { types } from "node:util";
|
|
11
12
|
|
|
12
13
|
export type * from "./registry-types.ts";
|
|
13
|
-
|
|
14
|
+
import { ADAPTER_API } from "./adapter-api.ts";
|
|
15
|
+
export { ADAPTER_API };
|
|
14
16
|
|
|
15
|
-
/** Ordered compatibility projections stay typed independently of definition implementation inference. */
|
|
16
|
-
export const KNOWN_PEERS = ["claude", "codex", "kimi", "pi", "local"] as const;
|
|
17
|
-
export const OPTIONAL_PEERS = ["qwen", "jcode", "agy"] as const;
|
|
18
|
-
export const SUPPORTED_PEERS = [...KNOWN_PEERS, ...OPTIONAL_PEERS] as const;
|
|
19
|
-
export const START_MODE_PEERS = ["pi", "codex", "kimi", "jcode"] as const;
|
|
20
|
-
export const OFFICE_PEER_ORDER: readonly string[] = KNOWN_PEERS;
|
|
21
17
|
|
|
22
18
|
|
|
23
19
|
function immutable<T>(value: T, seen = new WeakSet<object>()): T {
|
|
@@ -37,13 +33,38 @@ export const ADAPTERS: readonly AdapterDefinition[] = immutable([
|
|
|
37
33
|
...NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind === "jcode"),
|
|
38
34
|
AGY_DEFINITION,
|
|
39
35
|
]);
|
|
36
|
+
export interface AdapterLists {
|
|
37
|
+
readonly KNOWN_PEERS: readonly string[]; readonly OPTIONAL_PEERS: readonly string[]; readonly SUPPORTED_PEERS: readonly string[];
|
|
38
|
+
readonly START_MODE_PEERS: readonly string[]; readonly ROLE_ORDER: readonly string[]; readonly HELP_ORDER: readonly string[]; readonly PLANNER_PEERS: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/** Pure builtin projections. Unranked definitions retain their source order after ranked definitions. */
|
|
41
|
+
export function deriveAdapterLists(definitions: readonly AdapterDefinition[]): AdapterLists {
|
|
42
|
+
const ordered = (field: "roleRank" | "startRank" | "helpRank", eligible = (_definition: AdapterDefinition) => true) => definitions
|
|
43
|
+
.map((definition, index) => ({ definition, index })).filter(({ definition }) => eligible(definition))
|
|
44
|
+
.sort((left, right) => {
|
|
45
|
+
const a = left.definition.projection?.[field], b = right.definition.projection?.[field];
|
|
46
|
+
if (a === undefined) return b === undefined ? left.index - right.index : 1;
|
|
47
|
+
if (b === undefined) return -1;
|
|
48
|
+
return a - b || left.index - right.index;
|
|
49
|
+
}).map(({ definition }) => definition.kind);
|
|
50
|
+
const KNOWN_PEERS = definitions.filter(definition => !definition.optional).map(definition => definition.kind);
|
|
51
|
+
const OPTIONAL_PEERS = definitions.filter(definition => definition.optional).map(definition => definition.kind);
|
|
52
|
+
return immutable({ KNOWN_PEERS, OPTIONAL_PEERS, SUPPORTED_PEERS: [...KNOWN_PEERS, ...OPTIONAL_PEERS],
|
|
53
|
+
START_MODE_PEERS: ordered("startRank", definition => definition.start.forms.includes("tui") && definition.start.forms.includes("headless")),
|
|
54
|
+
ROLE_ORDER: ordered("roleRank"), HELP_ORDER: ordered("helpRank"),
|
|
55
|
+
PLANNER_PEERS: definitions.filter(definition => definition.projection?.plannerEligible === true).map(definition => definition.kind),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export const { KNOWN_PEERS, OPTIONAL_PEERS, SUPPORTED_PEERS, START_MODE_PEERS, ROLE_ORDER, PLANNER_PEERS } = deriveAdapterLists(ADAPTERS);
|
|
59
|
+
export const OFFICE_PEER_ORDER: readonly string[] = KNOWN_PEERS;
|
|
60
|
+
|
|
40
61
|
// Unavailable declarations are metadata, not supported native peers or launchers.
|
|
41
62
|
const declarationOnlyDefinitions: readonly AdapterDefinition[] = immutable([]);
|
|
42
63
|
const definitions = new Map<string, AdapterDefinition>();
|
|
43
64
|
for (const definition of [...ADAPTERS, ...declarationOnlyDefinitions]) {
|
|
44
|
-
|
|
65
|
+
assertDefinitionCoherence(definition);
|
|
45
66
|
assertNativeCapabilityScope(definition);
|
|
46
|
-
if (definition.reviewerVariant) {
|
|
67
|
+
if (definition.reviewerVariant) { assertDefinitionCoherence(definition.reviewerVariant); assertNativeCapabilityScope(definition.reviewerVariant); }
|
|
47
68
|
if (definition.api !== ADAPTER_API || definitions.has(definition.kind)) throw new Error("invalid builtin adapter registry");
|
|
48
69
|
definitions.set(definition.kind, definition);
|
|
49
70
|
}
|
|
@@ -65,7 +86,7 @@ function ownField(value: unknown, key: string): unknown {
|
|
|
65
86
|
return field && Object.hasOwn(field, "value") && field.enumerable ? field.value : undefined;
|
|
66
87
|
}
|
|
67
88
|
/** Declaration reads never execute caller accessors or inherit another project's mapping. */
|
|
68
|
-
function declaredKind(id: string, instances: KindMap | undefined): string | undefined {
|
|
89
|
+
function declaredKind(id: string, instances: KindMap | undefined, catalog: ReadonlyMap<string, AdapterDefinition> = definitions): string | undefined {
|
|
69
90
|
if (!instances || typeof instances !== "object" || types.isProxy(instances) || Array.isArray(instances)
|
|
70
91
|
|| ![Object.prototype, null].includes(Object.getPrototypeOf(instances))) return undefined;
|
|
71
92
|
const field = Object.getOwnPropertyDescriptor(instances, id);
|
|
@@ -74,7 +95,7 @@ function declaredKind(id: string, instances: KindMap | undefined): string | unde
|
|
|
74
95
|
if (!entry || typeof entry !== "object" || types.isProxy(entry) || Array.isArray(entry)
|
|
75
96
|
|| ![Object.prototype, null].includes(Object.getPrototypeOf(entry))) return undefined;
|
|
76
97
|
const kind = Object.getOwnPropertyDescriptor(entry, "kind");
|
|
77
|
-
if (!kind || !Object.hasOwn(kind, "value") || !kind.enumerable || typeof kind.value !== "string" || !
|
|
98
|
+
if (!kind || !Object.hasOwn(kind, "value") || !kind.enumerable || typeof kind.value !== "string" || !catalog.has(kind.value)) return undefined;
|
|
78
99
|
const prefix = `${kind.value}-`;
|
|
79
100
|
return /^[a-z][a-z0-9-]{0,31}$/.test(id) && id.startsWith(prefix) && /^[a-z0-9][a-z0-9-]*$/.test(id.slice(prefix.length)) ? kind.value : undefined;
|
|
80
101
|
}
|
|
@@ -84,7 +105,10 @@ export function kindOf(id: string, instances?: KindMap): string | undefined {
|
|
|
84
105
|
}
|
|
85
106
|
export function definitionOf(id: string, instances?: KindMap): AdapterDefinition | undefined {
|
|
86
107
|
const kind = kindOf(id, instances);
|
|
87
|
-
|
|
108
|
+
return resolvedDefinition(id, kind, instances, definitions);
|
|
109
|
+
}
|
|
110
|
+
function resolvedDefinition(id: string, kind: string | undefined, instances: KindMap | undefined, catalog: ReadonlyMap<string, AdapterDefinition>): AdapterDefinition | undefined {
|
|
111
|
+
const definition = kind === undefined ? undefined : catalog.get(kind);
|
|
88
112
|
// #223: only a declared extra (never the builtin id) with start_mode "headless" runs as the kind's reviewer variant.
|
|
89
113
|
const declaration = ownField(instances, id), roles = ownField(declaration, "roles");
|
|
90
114
|
const reviewerOnly = Array.isArray(roles) && !types.isProxy(roles) && roles.length === 1 && Object.getOwnPropertyDescriptor(roles, "0")?.value === "reviewer";
|
|
@@ -96,9 +120,33 @@ export function familyOf(id: string, instances?: KindMap): string | undefined {
|
|
|
96
120
|
return definitionOf(id, instances)?.family;
|
|
97
121
|
}
|
|
98
122
|
|
|
123
|
+
/** Compile one captured, host-trusted declaration set; never register native policy globally. */
|
|
124
|
+
export function createProjectAdapterCatalog(rawAdapters?: unknown, externalDefinitions: readonly AdapterDefinition[] = []): ProjectAdapterCatalog {
|
|
125
|
+
const configured = configuredAcpDefinitions(rawAdapters, [...definitions.keys()]);
|
|
126
|
+
for (const definition of configured) { assertDefinitionCoherence(definition); assertNativeCapabilityScope(definition); }
|
|
127
|
+
const seen = new Set([...definitions.keys(), ...configured.map(item => item.kind)]);
|
|
128
|
+
for (const definition of externalDefinitions) {
|
|
129
|
+
if (definition.api !== ADAPTER_API || seen.has(definition.kind)) throw new Error("external adapter kind or API collision");
|
|
130
|
+
assertDefinitionCoherence(definition); assertNativeCapabilityScope(definition);
|
|
131
|
+
seen.add(definition.kind);
|
|
132
|
+
}
|
|
133
|
+
const external = immutable([...externalDefinitions]);
|
|
134
|
+
const entries = Object.freeze([...ADAPTERS, ...configured, ...external]);
|
|
135
|
+
const catalog = new Map([...definitions, ...[...configured, ...external].map(definition => [definition.kind, definition] as const)]);
|
|
136
|
+
const kindOf = (id: string, instances?: KindMap): string | undefined => catalog.has(id) ? id : typeof id === "string" ? declaredKind(id, instances, catalog) : undefined;
|
|
137
|
+
const definitionOf = (id: string, instances?: KindMap): AdapterDefinition | undefined => resolvedDefinition(id, kindOf(id, instances), instances, catalog);
|
|
138
|
+
return Object.freeze({ definitions: entries, externalDefinitions: external, kindOf, definitionOf,
|
|
139
|
+
familyOf: (id: string, instances?: KindMap) => definitionOf(id, instances)?.family,
|
|
140
|
+
isSupportedPeer: (id: unknown, instances?: KindMap): id is string => {
|
|
141
|
+
if (typeof id !== "string") return false;
|
|
142
|
+
const kind = kindOf(id, instances), runtime = kind === undefined ? undefined : catalog.get(kind), resolved = definitionOf(id, instances);
|
|
143
|
+
return runtime !== undefined && resolved !== undefined && (kind === id || resolved === runtime || resolved.reviewer?.unavailable === undefined);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
99
148
|
/** Compatibility projection order is observable in default role maps and planner selection. */
|
|
100
|
-
|
|
101
|
-
export const PLANNER_PEERS = ["claude", "codex", "kimi", "qwen", "jcode"] as const;
|
|
149
|
+
|
|
102
150
|
|
|
103
151
|
/** Callers own their role arrays, independent of immutable native policy. */
|
|
104
152
|
export function defaultPeerRoles(includeOptional = false): Record<string, string[]> {
|
|
@@ -126,6 +174,52 @@ export function assertDaemonEngine(definition: Pick<AdapterDefinition, "start">)
|
|
|
126
174
|
if (definition.start.create && engine === undefined || engine !== undefined && !Object.hasOwn(DAEMON_PORTS, engine)) throw new Error("invalid adapter daemon engine");
|
|
127
175
|
}
|
|
128
176
|
|
|
177
|
+
/** Structural registry contract only: native telemetry readers are never executed during loading. */
|
|
178
|
+
export function assertDefinitionCoherence(definition: AdapterDefinition): void {
|
|
179
|
+
const fail = (field: string, reason: string): never => { throw new Error(`${definition.kind}.${field}: ${reason}`); };
|
|
180
|
+
const nonempty = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
|
|
181
|
+
assertDaemonEngine(definition);
|
|
182
|
+
if (definition.identity.mcp === "channel-plugin" && definition.transport !== "claude-channel" && definition.transport !== "ws-pull") {
|
|
183
|
+
fail("identity.mcp", "channel-plugin requires claude-channel or ws-pull transport");
|
|
184
|
+
}
|
|
185
|
+
if (definition.turnEnd === "native-hook" && definition.transport !== "claude-channel" && !definition.hooks?.factsHook) {
|
|
186
|
+
fail("turnEnd", "native-hook requires the Claude transport or a native factsHook");
|
|
187
|
+
}
|
|
188
|
+
const projection = definition.projection;
|
|
189
|
+
if (projection !== undefined) {
|
|
190
|
+
if (!projection || typeof projection !== "object" || Array.isArray(projection) || Object.keys(projection).some(key => !["roleRank", "startRank", "helpRank", "plannerEligible"].includes(key))) fail("projection", "invalid projection metadata");
|
|
191
|
+
for (const field of ["roleRank", "startRank", "helpRank"] as const) if (projection[field] !== undefined && (typeof projection[field] !== "number" || !Number.isFinite(projection[field]))) fail(`projection.${field}`, "rank must be finite");
|
|
192
|
+
if (projection.plannerEligible !== undefined && typeof projection.plannerEligible !== "boolean") fail("projection.plannerEligible", "boolean required");
|
|
193
|
+
}
|
|
194
|
+
const attach = definition.attach, pullOnly = attach?.pullOnlyForms;
|
|
195
|
+
if (pullOnly !== undefined) {
|
|
196
|
+
if (!Array.isArray(pullOnly) || pullOnly.some(form => !definition.start.forms.includes(form))) fail("attach.pullOnlyForms", "each pull-only form must be a supported start form");
|
|
197
|
+
}
|
|
198
|
+
if (definition.transport === "ws-pull" || definition.transport === "claude-channel" || definition.instances?.mode === "external") {
|
|
199
|
+
if (attach?.externalHello !== true) fail("attach.externalHello", "external transport or instance admission requires an explicit hello policy");
|
|
200
|
+
}
|
|
201
|
+
if (definition.transport === "ws-pull" && !pullOnly?.length) fail("attach.pullOnlyForms", "ws-pull requires a nonempty pull-only contract");
|
|
202
|
+
if (attach?.admission !== undefined) {
|
|
203
|
+
if (!attach.admission || typeof attach.admission !== "object") fail("attach.admission", "admission must declare its refusal policy");
|
|
204
|
+
if (!definition.start.forms.includes("tui") || !pullOnly?.includes("tui")) fail("attach.admission", "native admission requires a supported pull-only TUI form");
|
|
205
|
+
for (const field of ["reservedRefusal", "watchRefusal"] as const) {
|
|
206
|
+
if (!nonempty(attach.admission[field])) fail(`attach.admission.${field}`, "admission refusal text must be nonempty");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (definition.budget !== undefined) {
|
|
210
|
+
if (!nonempty(definition.budget?.account)) fail("budget.account", "declared budget account must be a nonempty string");
|
|
211
|
+
for (const field of ["reader", "context"] as const) {
|
|
212
|
+
const reader = definition.budget[field];
|
|
213
|
+
if (reader === undefined) continue;
|
|
214
|
+
if (!reader || typeof reader !== "object") fail(`budget.${field}`, "telemetry reader must be an object");
|
|
215
|
+
if (!nonempty(reader.source)) fail(`budget.${field}.source`, "telemetry source must be a nonempty string");
|
|
216
|
+
if (typeof reader.read !== "function") fail(`budget.${field}.read`, "telemetry read must be callable");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (definition.reviewerVariant && !definition.reviewerVariant.reviewer) fail("reviewerVariant.reviewer", "reviewer variant lacks reviewer policy");
|
|
220
|
+
if (definition.reviewer?.unavailable && (definition.start.create || definition.reviewer.backend)) fail("reviewer", "unavailable reviewer must remain metadata-only");
|
|
221
|
+
}
|
|
222
|
+
|
|
129
223
|
/** Host-global native quirks are not capabilities a config-only or external definition may opt into. */
|
|
130
224
|
export function assertNativeCapabilityScope(definition: AdapterDefinition): void {
|
|
131
225
|
if (fixedBuiltinDefinitions.has(definition)) return;
|
|
@@ -137,9 +231,13 @@ export function assertNativeCapabilityScope(definition: AdapterDefinition): void
|
|
|
137
231
|
["session.crashIdentity", declared(definition.session, "crashIdentity")],
|
|
138
232
|
["start.daemonPort pi-rpc", definition.start.daemonPort === "pi-rpc"],
|
|
139
233
|
["routing", definition.routing !== undefined],
|
|
234
|
+
["projection", definition.projection !== undefined],
|
|
140
235
|
["reviewerVariant", definition.reviewerVariant !== undefined],
|
|
141
236
|
["reviewer", definition.reviewer !== undefined],
|
|
142
237
|
["hooks.factsHook", declared(definition.hooks, "factsHook")],
|
|
238
|
+
["cli.entry", declared(definition.cli, "entry")],
|
|
239
|
+
["cli.preview", declared(definition.cli, "preview")],
|
|
240
|
+
["cli.usage", declared(definition.cli, "usage")],
|
|
143
241
|
];
|
|
144
242
|
const field = restricted.find(([, present]) => present)?.[0];
|
|
145
243
|
if (field) throw new Error(`${field} is restricted to fixed builtin adapter definitions`);
|
|
@@ -174,8 +272,8 @@ export function builtinPreviewOf(id: string) {
|
|
|
174
272
|
/** Fixed builtin doctor metadata and pure observed-value projections; the CLI retains all probe effects. */
|
|
175
273
|
export { DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "./definitions/native-cli-doctor.ts";
|
|
176
274
|
|
|
177
|
-
/**
|
|
178
|
-
export const BUILTIN_CLI_ENTRIES = Object.freeze(builtinCliEntries.map(entry => {
|
|
275
|
+
/** Definition-owned CLI entries are restricted to fixed builtins, never config or external execution extensions. */
|
|
276
|
+
export const BUILTIN_CLI_ENTRIES = Object.freeze(builtinCliEntries(ADAPTERS).map(entry => {
|
|
179
277
|
const definition = definitionOf(entry.command);
|
|
180
278
|
if (!definition) throw new Error("builtin CLI entry has no adapter definition");
|
|
181
279
|
return Object.freeze({ ...entry, definition });
|
|
@@ -380,3 +380,7 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
380
380
|
if (this.child === child) { await this.childCleanup?.(); this.child = undefined; this.childStop = undefined; this.childCleanup = undefined; this.childCurrent = undefined; }
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
|
+
|
|
384
|
+
export function isReviewerPrintPeer(value: unknown): value is ReviewerPrintPeer {
|
|
385
|
+
return value instanceof ReviewerPrintPeer;
|
|
386
|
+
}
|
package/src/cli/agy-mcp.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AGY_DEFINITION } from "../adapters/agy/definition.ts";
|
|
1
2
|
import type { Stats } from "node:fs";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -53,7 +54,7 @@ function shellUnquote(value: string): string | undefined {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function validateOptions(options: AgyMcpOptions) {
|
|
56
|
-
const peer = options.peer ??
|
|
57
|
+
const peer = options.peer ?? AGY_DEFINITION.kind;
|
|
57
58
|
if (!/^[a-z][a-z0-9-]{0,31}$/.test(peer)) throw new Error("agy peer id is invalid");
|
|
58
59
|
for (const [name, value] of Object.entries({ projectRoot: options.projectRoot, stateDir: options.stateDir, serverJs: options.serverJs, hookScript: options.hookScript })) {
|
|
59
60
|
if (typeof value !== "string" || !isAbsolute(value) || value.length > 4096 || /[\u0000-\u001f\u007f]/.test(value)) throw new Error(`${name} must be an absolute control-free path`);
|
|
@@ -326,7 +327,7 @@ export function cleanupAgyMcp(options: AgyMcpOptions): void {
|
|
|
326
327
|
if (!present.length) return;
|
|
327
328
|
if (present.length !== FILES.length) throw new Error("managed agy plugin is incomplete; inspect it before cleanup");
|
|
328
329
|
const currentHooks = parseJson(readFileSync(join(plan.root, HOOKS_FILE)), HOOKS_FILE);
|
|
329
|
-
const launch = managedHookLaunch(currentHooks, options, options.peer ??
|
|
330
|
+
const launch = managedHookLaunch(currentHooks, options, options.peer ?? AGY_DEFINITION.kind);
|
|
330
331
|
if (!launch || launch.instanceId !== options.instanceId || launch.launchId !== options.launchId) return;
|
|
331
332
|
// A stale cleanup never removes files a newer config update has replaced.
|
|
332
333
|
if (plan.changed) return;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ProjectAdapterCatalog } from "../adapters/registry.ts";
|
|
2
|
+
import type { InstancesConfig } from "../hub/instances.ts";
|
|
3
|
+
import { requireActiveInstance, requirePeerInstances } from "./launch.ts";
|
|
4
|
+
import { configuredAcpPreview } from "./preview.ts";
|
|
5
|
+
|
|
6
|
+
export interface ConfiguredAcpSelection { peer: string; mode: "headless"; sessionId?: string; preview: boolean }
|
|
7
|
+
export function parseConfiguredAcpArguments(kind: string, raw: readonly string[], catalog: ProjectAdapterCatalog, instances?: InstancesConfig): ConfiguredAcpSelection {
|
|
8
|
+
const definition = catalog.definitionOf(kind);
|
|
9
|
+
if (!definition?.permission.genericAcp) throw new Error("unknown configured ACP kind");
|
|
10
|
+
const values = new Map<string, string>();
|
|
11
|
+
let preview = false;
|
|
12
|
+
for (let index = 0; index < raw.length; index++) {
|
|
13
|
+
const arg = raw[index]!;
|
|
14
|
+
if (arg === "--print-command" || arg === "--dry-run") { if (preview) throw new Error("preview option may appear only once"); preview = true; continue; }
|
|
15
|
+
const equal = arg.indexOf("="), flag = equal < 0 ? arg : arg.slice(0, equal);
|
|
16
|
+
if (!["--as", "--mode", "--session-id"].includes(flag) || values.has(flag)) throw new Error("configured ACP accepts only --mode headless, --as, --session-id and one preview option");
|
|
17
|
+
const value = equal < 0 ? raw[++index] : arg.slice(equal + 1);
|
|
18
|
+
if (!value || value.startsWith("--") || Buffer.byteLength(value) > 4096 || /[\p{Cc}\p{Cf}]/u.test(value)) throw new Error(`${flag} needs a bounded control-free value`);
|
|
19
|
+
values.set(flag, value);
|
|
20
|
+
}
|
|
21
|
+
if (values.has("--mode") && values.get("--mode") !== "headless") throw new Error("configured ACP supports headless only");
|
|
22
|
+
const peer = values.get("--as") ?? kind;
|
|
23
|
+
if (values.has("--as") && (!instances || !Object.hasOwn(instances, peer) || instances[peer]?.kind !== kind || !catalog.definitionOf(peer, instances))) throw new Error("--as must name a declared instance of this configured kind");
|
|
24
|
+
return { peer, mode: "headless", ...(values.has("--session-id") ? { sessionId: values.get("--session-id")! } : {}), preview };
|
|
25
|
+
}
|
|
26
|
+
export interface ConfiguredAcpControl {
|
|
27
|
+
features: readonly string[];
|
|
28
|
+
request(message: Record<string, unknown>, timeout?: number): Promise<any>;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
export async function requireActiveConfiguredAcp(hub: ConfiguredAcpControl, kind: string, peer: string, warn?: (text: string) => void): Promise<void> {
|
|
32
|
+
if (!hub.features.includes("configured_acp")) throw new Error("running hub lacks configured_acp; upgrade and restart it first");
|
|
33
|
+
if (peer !== kind) requirePeerInstances(hub.features);
|
|
34
|
+
const reply = await hub.request({ t: "status" });
|
|
35
|
+
const active = reply.status?.configuredAdapters;
|
|
36
|
+
if (!Array.isArray(active) || !active.includes(kind)) throw new Error(`running hub has not loaded ${kind}; restart it after declaring the adapter`);
|
|
37
|
+
if (peer !== kind) requireActiveInstance(reply.status, peer);
|
|
38
|
+
const namespace = `adapters.${kind}`;
|
|
39
|
+
const retained = [reply.status?.retainedConfig, reply.status?.pendingRestart].some(rows => Array.isArray(rows) && rows.some(row => {
|
|
40
|
+
const key = row && typeof row === "object" ? row.key : undefined;
|
|
41
|
+
return typeof key === "string" && (key === "adapters" || key === namespace || key.startsWith(`${namespace}.`));
|
|
42
|
+
}));
|
|
43
|
+
if (retained) warn?.(`${kind}: configured adapter changes are retained; this start uses the running definition. Restart the hub to apply the file's definition.`);
|
|
44
|
+
}
|
|
45
|
+
export async function runConfiguredAcpEntry(context: {
|
|
46
|
+
kind: string; raw: readonly string[]; catalog: ProjectAdapterCatalog; instances?: InstancesConfig;
|
|
47
|
+
connect(): Promise<ConfiguredAcpControl>; print(text: string): void; operationId?: string;
|
|
48
|
+
}): Promise<void> {
|
|
49
|
+
const selected = parseConfiguredAcpArguments(context.kind, context.raw, context.catalog, context.instances);
|
|
50
|
+
if (selected.preview) { context.print(JSON.stringify(configuredAcpPreview(context.kind, selected.peer, selected.sessionId !== undefined), null, 2)); return; }
|
|
51
|
+
const hub = await context.connect();
|
|
52
|
+
try {
|
|
53
|
+
await requireActiveConfiguredAcp(hub, context.kind, selected.peer, context.print);
|
|
54
|
+
const reply = await hub.request({ t: "start", peer: selected.peer, args: { mode: "headless", ...(selected.sessionId ? { sessionId: selected.sessionId } : {}) }, ...(context.operationId ? { operationId: context.operationId } : {}) }, 70_000);
|
|
55
|
+
if (!reply.ok) throw new Error(typeof reply.error === "string" ? reply.error : "configured ACP start refused");
|
|
56
|
+
context.print(reply.already ? `${selected.peer} is already attached` : `${selected.peer} attached (headless); use ahub say @${selected.peer} and the console for approvals`);
|
|
57
|
+
} finally { hub.close(); }
|
|
58
|
+
}
|
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, definitionOf, nativeStreamTone } from "../adapters/registry.ts";
|
|
5
|
+
import { PLANNER_PEERS, definitionOf, nativeStreamTone, createProjectAdapterCatalog } 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";
|
|
@@ -91,7 +91,7 @@ export function syncPermissionDefaults(s: ConsoleState, rows: unknown): boolean
|
|
|
91
91
|
return true;
|
|
92
92
|
}
|
|
93
93
|
export function permissionDefaultText(row: Pick<PermissionDefault, "peer" | "source">): string {
|
|
94
|
-
return `never-ask default for ${row.peer} from ${quoted(row.source)}\n${permissionBoundary(row.peer)}\nUntil you answer y, this default stays ask. y enables; n cancels for this hub; Esc defers.`;
|
|
94
|
+
return `never-ask default for ${row.peer} from ${quoted(row.source)}\n${permissionBoundary(row.peer, undefined, createProjectAdapterCatalog())}\nUntil you answer y, this default stays ask. y enables; n cancels for this hub; Esc defers.`;
|
|
95
95
|
}
|
|
96
96
|
/** Strip terminal controls before any daemon or child output reaches a terminal. Preserve printable Unicode. */
|
|
97
97
|
export function terminalText(value: unknown): string {
|
|
@@ -471,6 +471,8 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
471
471
|
if (s.settingsEdit) {
|
|
472
472
|
const edit = { ...s.settingsEdit }; s.settingsEdit = edit;
|
|
473
473
|
if (key === "\x1b") { s.settingsEdit = undefined; return done(); }
|
|
474
|
+
const locked = s.settingsRows?.find(row => row.key === edit.key)?.locked;
|
|
475
|
+
if (locked) { s.settingsEdit = undefined; notify(s, locked, now); return done(); }
|
|
474
476
|
if (edit.phase === "preview") return done();
|
|
475
477
|
if (edit.phase === "confirm") {
|
|
476
478
|
s.settingsEdit = undefined;
|
|
@@ -632,6 +634,8 @@ export function reduceConsole(state: ConsoleState, key: string, now = Date.now()
|
|
|
632
634
|
const row = selectedPanelRow(s);
|
|
633
635
|
if (key === "u" && !s.settingsUndo) { notify(s, "nothing to undo", now); return done(); }
|
|
634
636
|
if (key === "e" && !row) return done();
|
|
637
|
+
const locked = (key === "e" ? row : s.settingsRows?.find(row => row.key === s.settingsUndo))?.locked;
|
|
638
|
+
if (locked) { notify(s, locked, now); return done(); }
|
|
635
639
|
const token = (s.settingsSequence ?? 0) + 1; s.settingsSequence = token;
|
|
636
640
|
s.settingsEdit = { token, undo: key === "u", ...(key === "e" ? { key: row.key } : {}), phase: key === "u" ? "preview" : "value", text: key === "e" ? settingValueText(row.fileValue) : "" };
|
|
637
641
|
if (key === "u") effects.push({ type: "settings_preview", token, undo: true });
|
|
@@ -975,6 +979,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
975
979
|
if (p.paused) summary.push(span(" paused", "attention"));
|
|
976
980
|
if (typeof p.importantWaitingSince === "number") summary.push(span(` important waiting ${duration(now - p.importantWaitingSince)}`, "attention")); // #572 AC1: a busy ACP peer that cannot be steered
|
|
977
981
|
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"));
|
|
982
|
+
if (typeof p.statusNotice?.message === "string" && p.statusNotice.message) summary.push(span(` ${flat(p.statusNotice.message)}`, "attention"));
|
|
978
983
|
}
|
|
979
984
|
if (s.pendingRestart?.length) summary.push(span(`${summary.length ? " | " : ""}restart pending: ${s.pendingRestart.map(row => row.key).join(", ")}`, "attention"));
|
|
980
985
|
if (s.retainedConfig?.length) summary.push(span(`${summary.length ? " | " : ""}config retained: ${s.retainedConfig.map(row => row.key).join(", ")}; run ahub settings reload`, "attention"));
|
|
@@ -988,7 +993,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
988
993
|
const neverPeer = s.confirm?.type === "permission_default" ? s.confirm.peer : s.confirm?.type === "command" && s.confirm.args[0] === "permission" && s.confirm.args[2] === "never-ask" ? s.confirm.args[1] : undefined;
|
|
989
994
|
const defaultSource = s.confirm?.type === "permission_default" ? s.confirm.source : undefined;
|
|
990
995
|
if (s.confirm?.type === "permission_default") prompt = `enable never-ask default for ${s.confirm.peer}? y/n Esc defer`;
|
|
991
|
-
const footer = [fitLine(neverPeer ? [span(permissionBoundary(neverPeer), "attention")] : summary, columns),
|
|
996
|
+
const footer = [fitLine(neverPeer ? [span(permissionBoundary(neverPeer, undefined, createProjectAdapterCatalog()), "attention")] : summary, columns),
|
|
992
997
|
fitLine(defaultSource !== undefined ? [span(`source ${quoted(defaultSource.split(/[\\/]/).at(-1))}; stays ask until y`, "attention")] : approvals, columns),
|
|
993
998
|
fitLine([span(prompt, s.confirm || s.optionChoice ? "attention" : undefined)], columns)];
|
|
994
999
|
const rule = [span("-".repeat(Math.max(0, columns)), "muted")];
|
|
@@ -1006,7 +1011,7 @@ export function renderConsoleLines(s: ConsoleState, columns: number, rows = 24,
|
|
|
1006
1011
|
const noticeLines = noticeShown && s.petEnabled && petSize(s.petSize, columns, rows) !== "line" ? wrap(`Notice: ${s.notice}`, columns).slice(0, Math.max(0, bodyHeight - 6)).map(text => [span(text, s.noticeTone ?? "failure")]) : [];
|
|
1007
1012
|
const height = bodyHeight - noticeLines.length;
|
|
1008
1013
|
const lines: Span[][] = [];
|
|
1009
|
-
if (s.confirm?.type === "permission_default") lines.push(...fieldLines([["default", `${s.confirm.peer} never-ask`], ["source", s.confirm.source], ["bounds", permissionBoundary(s.confirm.peer)], ["decision", "y enables; n cancels for this hub; Esc defers; until then ask"]], columns, now).flat());
|
|
1014
|
+
if (s.confirm?.type === "permission_default") lines.push(...fieldLines([["default", `${s.confirm.peer} never-ask`], ["source", s.confirm.source], ["bounds", permissionBoundary(s.confirm.peer, undefined, createProjectAdapterCatalog())], ["decision", "y enables; n cancels for this hub; Esc defers; until then ask"]], columns, now).flat());
|
|
1010
1015
|
else if (s.settingsEdit) {
|
|
1011
1016
|
const edit = s.settingsEdit;
|
|
1012
1017
|
const row = s.settingsRows?.find(row => row.key === edit.key);
|
package/src/cli/facts-hook.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { claudePeer as defaultChannelPeer } from "../hub/claude-state.ts";
|
|
2
3
|
import { definitionOf } from "../adapters/registry.ts";
|
|
3
4
|
// Managed Claude hook transport for turn-free facts, native observations and runtime permissions.
|
|
4
5
|
// PreToolUse preserves facts additionalContext and adds permission decisions; other phases observe only.
|
|
@@ -101,7 +102,7 @@ export async function factsHook(stdin: string, stateDir: string, peer: string, t
|
|
|
101
102
|
if (import.meta.main) {
|
|
102
103
|
try {
|
|
103
104
|
const stateDir = process.env.AGENTHUB_STATE_DIR;
|
|
104
|
-
const out = stateDir ? await factsHook(await Bun.stdin.text(), stateDir, process.env.AGENTHUB_PEER_ID ??
|
|
105
|
+
const out = stateDir ? await factsHook(await Bun.stdin.text(), stateDir, process.env.AGENTHUB_PEER_ID ?? defaultChannelPeer(undefined), 2000, process.env.AGENTHUB_HOOK_EVENT) : undefined;
|
|
105
106
|
if (out) console.log(out);
|
|
106
107
|
} catch {
|
|
107
108
|
// a hook that fails must not get in the way of the tool call
|
package/src/cli/help.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ADAPTERS, deriveAdapterLists, type AdapterDefinition } from "../adapters/registry.ts";
|
|
1
2
|
import { VERSION } from "../version.ts";
|
|
2
3
|
import { paint, wrap, type Span } from "./console-state.ts";
|
|
3
4
|
|
|
@@ -26,9 +27,10 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
26
27
|
["ahub sdlc status [task] | explain <task> | export <task> | record <task> <receipt-json>", "read head-bound lifecycle gates and the next actor/action (person only)"],
|
|
27
28
|
["ahub sdlc queue <owner/repo> | release <owner/repo> [live-issue...]", "read-only merge and release evidence (person only)"],
|
|
28
29
|
["ahub sdlc accept <task> --issue <n> [--repo owner/repo]", "accept the task's spec issue at its current body; build entry needs it in enforce (person only)"],
|
|
29
|
-
["ahub sdlc
|
|
30
|
+
["ahub sdlc accept-departure <task> <head> <reason...>", "accept current plan departures on that exact PR head (person only)"],
|
|
31
|
+
["ahub sdlc plan-review <task> <revision> <reason...> [--review full|delta|none --reviewers N] [--ci full|focused|reuse]", "record planner review, scope and host-read issue metadata"],
|
|
30
32
|
["ahub sdlc train report|configure|cut <release-id> [revision] [json]", "plan a release and freeze its approved scope after resource/quota admission"],
|
|
31
|
-
["ahub sdlc release-assign <task> <revision> <release-id|none> <reason
|
|
33
|
+
["ahub sdlc release-assign <task> <revision> <release-id|none> <reason...>", "assign approved/in-flight work without changing execution"],
|
|
32
34
|
["ahub sdlc costs [owner/repo]", "read bounded planner cost metrics for linked merged PRs"],
|
|
33
35
|
["ahub backlog groom | return <task> <revision> <reason...>", "read grooming findings or return accepted untouched work to backlog (planner/person)"],
|
|
34
36
|
["ahub status [--json] [--full] [--color=auto|always|never]", "the hub, its peers, model backends and task counts"],
|
|
@@ -48,15 +50,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
48
50
|
["ahub recovery-run <operation-id>", "internal: the detached runner of an upgrade, restart or recovery resume"],
|
|
49
51
|
]],
|
|
50
52
|
["Agents", [
|
|
51
|
-
["ahub
|
|
52
|
-
["ahub codex [--print-command] [--unattended] [args...]", "start the Codex adapter and attach the TUI"],
|
|
53
|
-
["ahub kimi [--print-command] [--mode tui|headless] [--model <alias>]", "default: Kimi's own TUI, pull-only via hub_inbox; no push, steer, watchdog, console approvals or automatic quota handoff. Select headless for ACP"],
|
|
54
|
-
["ahub jcode [--mode tui|headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>] [--print-command]", "optional Jcode (roster.jcode, default off): its TUI on the hub-owned ACP session, opened once that session is saved; --mode headless for ACP only; native policy unmanaged, no hub sandbox"],
|
|
55
|
-
["ahub agy [--print-command | --dry-run] [args...]", "optional Antigravity CLI 1.4.x TUI (1.4.2 verified) in this terminal (roster.agy, default off): pull-only via hub_inbox, implementer/verifier only; native permissions unmanaged, no hub sandbox"],
|
|
56
|
-
["ahub qwen [--mode headless] [--model <alias>] [--session-id <id>] [--print-command]", "start optional Qwen Code 0.25.0 ACP (roster.qwen, default off)"],
|
|
57
|
-
["ahub pi [--print-command] [--mode headless|tui | --headless] [--backend auto|dgx|mlx] [--model <alias>] [--session-id <id>] [--session-file <path>] [--replace-session]", "start Pi: its TUI in this terminal unless --headless or the start mode setting (peers.pi.start_mode) says headless; without a terminal the hub opens one or says how"],
|
|
58
|
-
["ahub local [--route <id> | --model <id>]", "start the hub-native worker on the self-hosted models (routing.toml)"],
|
|
59
|
-
["ahub claude|codex|kimi|qwen|pi|local --as <instance> [options...]", "start an extra instance of that kind declared under instances in .agenthub/config.local.json"],
|
|
53
|
+
["ahub <configured-kind> [--mode headless] [--as <instance>] [--session-id <id>] [--print-command | --dry-run]", "start a trusted config.local.json ACP kind (default off roster); native argv is withheld in previews"],
|
|
60
54
|
["ahub role list [--group <group>] | show <id>", "read bounded catalog guidance with layer and content hash"],
|
|
61
55
|
["ahub role assign <peer> <id> | clear <peer> [<id>]", "person only: add or clear catalog guidance, applied at the next peer start"],
|
|
62
56
|
["ahub stop <peer>", "human only: stop a hub-owned headless peer; end TUI agents in their terminals"],
|
|
@@ -70,7 +64,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
70
64
|
["ahub permission [<peer> [ask|ask-when-needed|never-ask]] [--yes]", "human only: inspect or set a running peer mode; never-ask requires --yes; config never-ask waits for console y"],
|
|
71
65
|
["ahub permit <id> <option>", "answer a permission request shown by tail (\"deny\" cancels)"],
|
|
72
66
|
["ahub roster [--json] [--color=auto|always|never]", "human only: every known peer, participation, start mode and retained work"],
|
|
73
|
-
["ahub roster add <peer> | remove <peer> [--yes] [--now]", "human only: change live participation through settings; removal previews retained messages and task handoff"],
|
|
67
|
+
["ahub roster add <peer> | remove <peer> [--yes] [--now]", "human only: change live participation through settings; removal previews retained messages and task handoff; remove also deletes the roster key of an instance no longer declared"],
|
|
74
68
|
["ahub settings [list] [--json] | get <key> [--json]", "human only: the settings the dashboard can change, each with its value, the file it comes from and when it applies"],
|
|
75
69
|
["ahub settings set <key> <value|inherit> [--yes] [--preview] [--now] | undo | reload", "human only: change or reload validated settings, written to .agenthub/config.local.json or routing.local.toml (never a tracked file); --preview shows the effect and saves nothing; undo puts back the last write"],
|
|
76
70
|
["ahub pause|resume <peer>", "hold a peer's deliveries in its queue / release them"],
|
|
@@ -93,7 +87,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
93
87
|
["ahub task unassign <id> [--role owner|reviewer] [--revision N]", "person only: remove the owner/reservation or reviewer without routing a replacement or stopping native work"],
|
|
94
88
|
["ahub task cancel <id> --reason <text> [--superseded-by <ref>] [--revision N]", "person only: close obsolete work with a reason; retain its history without stopping native work or releasing dependents"],
|
|
95
89
|
["ahub task label <id> ok|regressed|reverted|incomplete|wrong|abandoned [--created <time>]", "your later verdict on an approved task, kept in the research records (research must be on)"],
|
|
96
|
-
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]... [--head <sha>]", "give a review verdict on a task"],
|
|
90
|
+
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]... [--head <sha>] [--findings <json>]", "give a review verdict on a task"],
|
|
97
91
|
["ahub review brief <id>", "print the review brief of a task's commit: reviewable and excluded files, rules, coverage to account for"],
|
|
98
92
|
["ahub route explain <id>", "why a task went where it went, its reserved owner included"],
|
|
99
93
|
["ahub route explain --class <c> <title...>", "what would happen to such a task now"],
|
|
@@ -144,7 +138,18 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
144
138
|
["ahub manager", "internal: the dashboard manager process that ahub ui --all starts"],
|
|
145
139
|
]],
|
|
146
140
|
];
|
|
147
|
-
export
|
|
141
|
+
export function helpEntries(definitions: readonly AdapterDefinition[] = ADAPTERS): readonly HelpEntry[] {
|
|
142
|
+
const byKind = new Map(definitions.map(definition => [definition.kind, definition]));
|
|
143
|
+
const order = deriveAdapterLists(definitions).HELP_ORDER;
|
|
144
|
+
const native = order.flatMap(kind => byKind.get(kind)?.cli.usage ?? []);
|
|
145
|
+
const instanceKinds = order.filter(kind => byKind.get(kind)?.instances !== undefined);
|
|
146
|
+
const instances: readonly [string, string] = [`ahub ${instanceKinds.join("|")} --as <instance> [options...]`, "start an extra instance of that kind declared under instances in .agenthub/config.local.json"];
|
|
147
|
+
return SECTIONS.flatMap(([section, rows]) => {
|
|
148
|
+
const entries: readonly (readonly [string, string])[] = section === "Agents" ? [rows[0]!, ...native, ...(instanceKinds.length ? [instances] : []), ...rows.slice(1)] : rows;
|
|
149
|
+
return entries.map(([usage, description]) => ({ section, usage, description }));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export const HELP = helpEntries();
|
|
148
153
|
|
|
149
154
|
const COMMAND = /^ahub (-{0,2}[a-z][a-z-]*(?: ?\| ?-{0,2}[a-z][a-z-]*)*)/;
|
|
150
155
|
/** The command words a usage documents: `ahub pause|resume <peer>` documents pause and resume. */
|
|
@@ -158,9 +163,10 @@ const DESCRIPTION = 34;
|
|
|
158
163
|
* (80 to 100 columns). A usage too long for that column puts its description on the next line.
|
|
159
164
|
* With a command, only its entries; an unknown command renders nothing.
|
|
160
165
|
*/
|
|
161
|
-
export function renderHelp(columns: number, color: boolean, command?: string): string {
|
|
166
|
+
export function renderHelp(columns: number, color: boolean, command?: string, definitions?: readonly AdapterDefinition[]): string {
|
|
162
167
|
const width = Math.min(100, Math.max(80, columns || 80));
|
|
163
|
-
const
|
|
168
|
+
const model = definitions === undefined ? HELP : helpEntries(definitions);
|
|
169
|
+
const entries = command === undefined ? model : model.filter(entry => helpCommands(entry).includes(command));
|
|
164
170
|
const lines: Span[][] = command === undefined ? wrap(`agent-hub ${VERSION}: Claude Code, Codex and Kimi as peers in one project directory`, width, 0).map(text => [{ text }]) : [];
|
|
165
171
|
let section = "";
|
|
166
172
|
for (const entry of entries) {
|
|
@@ -178,3 +184,8 @@ export function renderHelp(columns: number, color: boolean, command?: string): s
|
|
|
178
184
|
}
|
|
179
185
|
return lines.map(line => paint(line, color)).join("\n");
|
|
180
186
|
}
|
|
187
|
+
|
|
188
|
+
/** Body-free configured command guidance; no native argv or environment values enter help. */
|
|
189
|
+
export function configuredAcpHelp(kind: string): string {
|
|
190
|
+
return `ahub ${kind} [--mode headless] [--as <instance>] [--session-id <id>] [--print-command | --dry-run]\nConfigured ACP runs headless through the hub, with native permissions and NO hub sandbox.\nDeclare it in untracked .agenthub/config.local.json, restart the hub, then enable roster.${kind}.\nPreview withholds native argv and starts nothing. Native/model overrides and --unattended are refused.`;
|
|
191
|
+
}
|