@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
package/src/cli/identity.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { nativeCliMarkerPeer } from "../adapters/definitions/native-identity-defaults.ts";
|
|
1
2
|
import { recoveryPresentationFlag } from "./recovery-presentation-options.ts";
|
|
2
|
-
import { definitionOf, kindOf, type KindMap } from "../adapters/registry.ts";
|
|
3
|
+
import { definitionOf, kindOf, type KindMap, type ProjectAdapterCatalog } from "../adapters/registry.ts";
|
|
3
4
|
|
|
4
5
|
/** Recovery reads this declaration before delegating to another package's CLI. */
|
|
5
6
|
export const LIVE_TEST_SUPPORTED = true;
|
|
@@ -20,7 +21,7 @@ export function liveTestRefusal(command: string, args: readonly string[], contex
|
|
|
20
21
|
|| (command === "backlog" && sub !== "list") || (command === "release" && !["list", "show"].includes(sub ?? ""))
|
|
21
22
|
|| (command === "memory" && sub !== "graph")
|
|
22
23
|
|| (command === "recovery" && (sub === "reattach" || sub === "dispose" && context.repairedFresh === true))
|
|
23
|
-
|| (command === "board" && args[0] === "import") || (command === "sdlc" && (args[0] === "record" || args[0] === "accept"))
|
|
24
|
+
|| (command === "board" && args[0] === "import") || (command === "sdlc" && (args[0] === "record" || args[0] === "accept" || args[0] === "accept-departure"))
|
|
24
25
|
|| (command === "bench" && args.find(arg => arg !== "--json") === "run") || command === "pet";
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -42,7 +43,7 @@ export function detectCliIdentity(env: Readonly<Record<string, string | undefine
|
|
|
42
43
|
if (codex !== undefined && (!codex || /\s/.test(codex))) return invalid("invalid CODEX_THREAD_ID marker");
|
|
43
44
|
if (session !== undefined && claude === undefined) return invalid("CLAUDE_CODE_SESSION_ID without CLAUDECODE marker");
|
|
44
45
|
if (claude !== undefined && codex !== undefined) return invalid("conflicting native agent markers");
|
|
45
|
-
const native = claude !== undefined
|
|
46
|
+
const native = nativeCliMarkerPeer(claude !== undefined, codex !== undefined);
|
|
46
47
|
if (hub !== undefined && native !== undefined && (kindOf(hub, instances) ?? hub) !== native) return invalid("AGENTHUB_PEER_ID conflicts with native agent marker");
|
|
47
48
|
const peer = hub ?? native;
|
|
48
49
|
return peer === undefined ? { role: "console" } : { role: "tools", peer };
|
|
@@ -51,9 +52,9 @@ export function detectCliIdentity(env: Readonly<Record<string, string | undefine
|
|
|
51
52
|
export type PeerCommandAccess = "allowed" | "conductor" | "console";
|
|
52
53
|
|
|
53
54
|
/** A closed list: unknown commands cannot acquire console authority through an agent shell. */
|
|
54
|
-
export function classifyPeerCommand(command: string, args: readonly string[]): PeerCommandAccess {
|
|
55
|
+
export function classifyPeerCommand(command: string, args: readonly string[], catalog?: ProjectAdapterCatalog): PeerCommandAccess {
|
|
55
56
|
if (command === "recovery" && recoveryAction(args) === "reattach") return "console";
|
|
56
|
-
const native = definitionOf(command);
|
|
57
|
+
const native = catalog?.definitionOf(command) ?? definitionOf(command);
|
|
57
58
|
if (native?.cli.personOnlyFlags?.some(flag => args.includes(flag))) return "console";
|
|
58
59
|
if (command === "pet" && args.includes("--window")) return "console";
|
|
59
60
|
if (command === "permission" || command === "stop" || command === "watch") return "console";
|
|
@@ -82,11 +83,11 @@ export function classifyPeerCommand(command: string, args: readonly string[]): P
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/** No argument text is included: notices and refusal logs must not copy task text or credentials. */
|
|
85
|
-
export function cliCommandLabel(command: string, args: readonly string[]): string {
|
|
86
|
+
export function cliCommandLabel(command: string, args: readonly string[], catalog?: ProjectAdapterCatalog): string {
|
|
86
87
|
if (command === "backlog") return ["list", "add", "pull", "pull-ready", "split", "return", "plan", "pin", "undo"].includes(args[0] ?? "") ? `backlog ${args[0]}` : "backlog";
|
|
87
88
|
if (command === "board" && args[0] === "import") return "board import";
|
|
88
89
|
if (command === "release") return ["new", "list", "show", "plan", "activate", "freeze", "ship", "drop"].includes(args[0] ?? "") ? `release ${args[0]}` : "release";
|
|
89
|
-
if (!definitionOf(command) && !["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "completion", "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";
|
|
90
|
+
if (!(catalog?.definitionOf(command) ?? definitionOf(command)) && !["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "completion", "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";
|
|
90
91
|
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
91
92
|
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
92
93
|
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/launch.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { claudePeer as defaultChannelPeer } from "../hub/claude-state.ts";
|
|
1
2
|
// Launchers inject only the flags the hub owns and refuse user-supplied duplicates.
|
|
2
3
|
import { claudeStateFile } from "../hub/claude-state.ts";
|
|
3
4
|
import { mkdirSync, writeFileSync, renameSync, unlinkSync, lstatSync, readFileSync, openSync, closeSync, readSync, accessSync, constants, linkSync } from "node:fs";
|
|
@@ -159,7 +160,7 @@ async function withClaudeLaunchRecord(stateDir: string, action: () => void): Pro
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
function writeClaudeLaunchRecord(stateDir: string, record: Record<string, unknown>): void {
|
|
162
|
-
const file = join(stateDir, claudeStateFile("claude-launch", typeof record.peer === "string" ? record.peer :
|
|
163
|
+
const file = join(stateDir, claudeStateFile("claude-launch", typeof record.peer === "string" ? record.peer : defaultChannelPeer(undefined))), temp = `${file}.${randomUUID()}.tmp`;
|
|
163
164
|
try { writeFileSync(temp, JSON.stringify(record), { mode: 0o600 }); renameSync(temp, file); }
|
|
164
165
|
finally { try { unlinkSync(temp); } catch { /* the rename normally consumed it */ } }
|
|
165
166
|
}
|
|
@@ -202,7 +203,7 @@ export async function recordClaudeNative(stateDir: string, launch: ClaudeLaunchR
|
|
|
202
203
|
const publication = await withClaudeLaunchRecord(stateDir, () => {
|
|
203
204
|
let current: ClaudeLaunchRecord;
|
|
204
205
|
try {
|
|
205
|
-
const file = join(stateDir, claudeStateFile("claude-launch", launch.peer ??
|
|
206
|
+
const file = join(stateDir, claudeStateFile("claude-launch", launch.peer ?? defaultChannelPeer(undefined))), stat = lstatSync(file);
|
|
206
207
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || (stat.mode & 0o777) !== 0o600 || !process.getuid || stat.uid !== process.getuid()) return;
|
|
207
208
|
current = JSON.parse(readFileSync(file, "utf8"));
|
|
208
209
|
} catch { return; }
|
|
@@ -219,7 +220,7 @@ export async function recordClaudeNative(stateDir: string, launch: ClaudeLaunchR
|
|
|
219
220
|
writeFileSync(temp, JSON.stringify({ ...launch, nativePid: pid, nativeSignature: signature, nativeIdentity: "direct-child" }), { mode: 0o600, flag: "wx" });
|
|
220
221
|
renameSync(temp, file);
|
|
221
222
|
} finally { try { unlinkSync(temp); } catch { /* atomic rename consumed our private temporary file */ } }
|
|
222
|
-
try { recorded = sameClaudeLaunch(JSON.parse(readFileSync(join(stateDir, claudeStateFile("claude-launch", launch.peer ??
|
|
223
|
+
try { recorded = sameClaudeLaunch(JSON.parse(readFileSync(join(stateDir, claudeStateFile("claude-launch", launch.peer ?? defaultChannelPeer(undefined))), "utf8")), launch); } catch { /* replaced or unreadable managed record */ }
|
|
223
224
|
});
|
|
224
225
|
if (publication.warning) report?.(publication.warning);
|
|
225
226
|
return publication.recorded && recorded;
|
|
@@ -333,7 +334,7 @@ export function buildLaunch(
|
|
|
333
334
|
): Launch {
|
|
334
335
|
const adapter = definitionOf(tool);
|
|
335
336
|
if (!adapter) throw new Error("native launch definition unavailable");
|
|
336
|
-
const { metadata: _metadata, ...launch } = adapter.start.launch({
|
|
337
|
+
const { metadata: _metadata, ...launch } = adapter.start.launch!({
|
|
337
338
|
instance: ctx.instance ?? tool, form: "tui", projectRoot: process.cwd(), stateDir: ctx.facts?.stateDir ?? ctx.statusLine?.stateDir ?? "",
|
|
338
339
|
entrypoint: resolve(import.meta.dir, "main.js"), userArgs, env: {}, ...ctx,
|
|
339
340
|
});
|
package/src/cli/main.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { claudePeer as defaultChannelPeer } from "../hub/claude-state.ts";
|
|
2
3
|
import { effectiveInstanceRoles } from "../hub/instances.ts";
|
|
3
4
|
import { resolveInstanceExecutionCwd } from "../hub/instance-execution.ts";
|
|
4
|
-
import { readInstanceConfig } from "../hub/instance-config.ts";
|
|
5
|
+
import { readInstanceRoleConfig, readInstanceConfig, type InstanceRoleConfig } from "../hub/instance-config.ts";
|
|
6
|
+
import { staleInstanceKey } from "../hub/roster.ts";
|
|
7
|
+
import { parseConfiguredAcpArguments, runConfiguredAcpEntry, requireActiveConfiguredAcp } from "./configured-acp-entry.ts";
|
|
5
8
|
import type { NativeCliEntryContext } from "../adapters/definitions/native-cli-entry-types.ts";
|
|
6
|
-
import { BUILTIN_CLI_ENTRIES, definitionOf, DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "../adapters/registry.ts";
|
|
9
|
+
import { BUILTIN_CLI_ENTRIES, createProjectAdapterCatalog, definitionOf, DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "../adapters/registry.ts";
|
|
7
10
|
import { parsePiFlags } from "../adapters/definitions/native-cli-pi.ts";
|
|
8
11
|
import { supportsBoardReleaseFilter, supportsBoardColumnMetadata } from "./board-read.ts";
|
|
9
12
|
import { matchesPolicy } from "../hub/path-policy.ts";
|
|
@@ -79,7 +82,7 @@ import { recordCliAudit, recordDelegatedCliAudit } from "./identity-audit.ts";
|
|
|
79
82
|
import { runConsole } from "./console.ts";
|
|
80
83
|
import { paint, permissionBoundary, resolveColor, wrap, type Span } from "./console-state.ts";
|
|
81
84
|
import { outputWidth, renderRoster, renderStatus, renderBoard, renderBoardColumns, boardColumnModel, renderBudget, renderDoctor, renderProjects, renderQueue, renderTurns, renderOrphans, renderQueueShow, renderModelsStatus, renderExecutionBudgetStatus, renderReport, type DoctorCheck } from "./output.ts";
|
|
82
|
-
import { renderHelp } from "./help.ts";
|
|
85
|
+
import { renderHelp, configuredAcpHelp } from "./help.ts";
|
|
83
86
|
import { renderTailEvent } from "./tail-render.ts";
|
|
84
87
|
import { archiveProblem, archiveState, damagedState, failureText, MANIFEST, planReset, resetLines, resetRuntime, startState, type ResetPlan } from "./reset.ts";
|
|
85
88
|
|
|
@@ -118,16 +121,32 @@ try {
|
|
|
118
121
|
}
|
|
119
122
|
} else {
|
|
120
123
|
const markedPeer = process.env.AGENTHUB_PEER_ID, markedRoot = process.env.AGENTHUB_PROJECT_DIR;
|
|
121
|
-
const
|
|
122
|
-
|
|
124
|
+
const markedConfig = markedPeer && markedRoot && !definitionOf(markedPeer) ? readInstanceConfig(realPath(markedRoot)) : undefined;
|
|
125
|
+
const declarations = markedConfig?.instances;
|
|
126
|
+
if (markedPeer && markedRoot && markedConfig && declarations && Object.hasOwn(declarations, markedPeer)) {
|
|
123
127
|
const canonicalRoot = realPath(markedRoot), config = loadConfig(canonicalRoot);
|
|
124
|
-
const executionCwd = resolveInstanceExecutionCwd(canonicalRoot, markedPeer, declarations, effectiveInstanceRoles(markedPeer, declarations, config.roles));
|
|
128
|
+
const executionCwd = resolveInstanceExecutionCwd(canonicalRoot, markedPeer, declarations, effectiveInstanceRoles(markedPeer, declarations, config.roles, markedConfig.catalog), markedConfig.catalog);
|
|
125
129
|
if (realPath(projectRoot(process.cwd())) !== realPath(executionCwd)) fail("native instance workspace does not match its declared checkout");
|
|
126
130
|
selected = projectContext(canonicalRoot);
|
|
127
131
|
} else selected = projectContext(process.cwd());
|
|
128
132
|
}
|
|
129
133
|
} catch (error) { fail(startupDiagnostic(error)); }
|
|
130
134
|
const cwd = selected.root;
|
|
135
|
+
let peerConfig: ReturnType<typeof readInstanceConfig>;
|
|
136
|
+
const needsProjectDeclarations = cliCommandLabel(given, rest) === "unknown"
|
|
137
|
+
|| rest.some(arg => arg === "--as" || arg.startsWith("--as="))
|
|
138
|
+
|| !!process.env.AGENTHUB_PEER_ID && !definitionOf(process.env.AGENTHUB_PEER_ID)
|
|
139
|
+
|| given === "help" && !!rest[0] && cliCommandLabel(rest[0], []) === "unknown";
|
|
140
|
+
try { peerConfig = needsProjectDeclarations ? readInstanceConfig(cwd) : { catalog: createProjectAdapterCatalog(), ignored: [] }; } catch (error) { fail(startupDiagnostic(error)); }
|
|
141
|
+
/** Local declarations for a command the running daemon answers. A config file that is mid-edit or invalid must not
|
|
142
|
+
* block it (lowering a permission, listing or reloading settings): such a read falls back to builtins and keeps its
|
|
143
|
+
* diagnostic for a key that only a declaration could define. */
|
|
144
|
+
function localDeclarations(): { scope: Pick<InstanceRoleConfig, "instances" | "catalog">; error?: string } {
|
|
145
|
+
try { return { scope: readInstanceRoleConfig(cwd) }; }
|
|
146
|
+
catch (error) { return { scope: { catalog: createProjectAdapterCatalog() }, error: startupDiagnostic(error) }; }
|
|
147
|
+
}
|
|
148
|
+
const projectCatalog = peerConfig.catalog;
|
|
149
|
+
const configuredCommand = !definitionOf(given) && projectCatalog.definitionOf(given)?.permission.genericAcp !== undefined;
|
|
131
150
|
const unboundCaller = detectCliIdentity(process.env);
|
|
132
151
|
const markerNeedsDeclaration = unboundCaller.role === "invalid" && unboundCaller.reason === "AGENTHUB_PEER_ID conflicts with native agent marker"
|
|
133
152
|
&& process.env.AGENTHUB_PEER_ID !== undefined && !definitionOf(process.env.AGENTHUB_PEER_ID);
|
|
@@ -135,8 +154,11 @@ const needsInstances = definitionOf(given) && rest.some(arg => arg === "--as" ||
|
|
|
135
154
|
let declaredInstances: ReturnType<typeof readInstanceConfig>["instances"];
|
|
136
155
|
let nativeSelection: ReturnType<typeof instanceLaunchArguments>;
|
|
137
156
|
try {
|
|
138
|
-
declaredInstances = needsInstances ?
|
|
139
|
-
|
|
157
|
+
declaredInstances = needsInstances || configuredCommand ? peerConfig.instances : undefined;
|
|
158
|
+
if (configuredCommand && !(rest.length === 1 && ["--help", "-h"].includes(rest[0]!))) {
|
|
159
|
+
const selection = parseConfiguredAcpArguments(given, rest, projectCatalog, declaredInstances);
|
|
160
|
+
nativeSelection = { ...(selection.peer !== given ? { instance: selection.peer } : {}), args: [...rest] };
|
|
161
|
+
} else nativeSelection = instanceLaunchArguments(given, rest, declaredInstances);
|
|
140
162
|
} catch (error) { fail(startupDiagnostic(error)); }
|
|
141
163
|
const selectedInstance = nativeSelection.instance;
|
|
142
164
|
// `ahub <command> -h` alone asks for that command's help instead of running it. Elsewhere `-h` may be message text
|
|
@@ -146,10 +168,19 @@ const cmd = asksHelp(given) || (definitionOf(given)?.start.passthrough !== true
|
|
|
146
168
|
const args = cmd === "help" && given !== "help" && !asksHelp(given) ? [given] : nativeSelection.args;
|
|
147
169
|
|
|
148
170
|
/** Resolve a named peer only against this project's current trusted declarations. */
|
|
149
|
-
const peerDefinition = (peer: unknown) =>
|
|
150
|
-
|
|
171
|
+
const peerDefinition = (peer: unknown) => {
|
|
172
|
+
if (typeof peer !== "string") return undefined;
|
|
173
|
+
const bound = projectCatalog.definitionOf(peer, peerConfig.instances);
|
|
174
|
+
if (bound) return bound;
|
|
175
|
+
const current = readInstanceConfig(cwd);
|
|
176
|
+
return current.catalog.definitionOf(peer, current.instances);
|
|
177
|
+
};
|
|
151
178
|
const requireNamedPeerFeature = (hub: ControlClient, peer: string | undefined) => {
|
|
152
|
-
if (peer !== undefined && !definitionOf(peer))
|
|
179
|
+
if (peer !== undefined && !definitionOf(peer)) {
|
|
180
|
+
const adapter = peerDefinition(peer);
|
|
181
|
+
if (adapter?.permission.genericAcp !== undefined && !hub.features.includes("configured_acp")) throw new Error("running hub lacks configured_acp; upgrade and restart it first");
|
|
182
|
+
if (adapter?.kind !== peer) requirePeerInstances(hub.features);
|
|
183
|
+
}
|
|
153
184
|
};
|
|
154
185
|
|
|
155
186
|
/** The project config, saying on stderr which machine-local fields it had to ignore (issue #17). */
|
|
@@ -160,8 +191,8 @@ const projectConfig = () => {
|
|
|
160
191
|
};
|
|
161
192
|
const stateDir = selected.stateDir;
|
|
162
193
|
const caller = declaredInstances ? detectCliIdentity(process.env, declaredInstances) : unboundCaller;
|
|
163
|
-
const commandLabel = cliCommandLabel(cmd, args);
|
|
164
|
-
const commandAccess = classifyPeerCommand(cmd, args);
|
|
194
|
+
const commandLabel = cliCommandLabel(cmd, args, projectCatalog);
|
|
195
|
+
const commandAccess = classifyPeerCommand(cmd, args, projectCatalog);
|
|
165
196
|
// Agent-authored content keeps its peer. Operational starts/holds may borrow console authority for live verification.
|
|
166
197
|
const delegationRefused = liveTest && caller.role === "tools" && commandAccess !== "allowed" && liveTestRefusal(cmd, args);
|
|
167
198
|
const delegatedPeer = liveTest && caller.role === "tools" && commandAccess !== "allowed" && !delegationRefused ? caller.peer : undefined;
|
|
@@ -408,11 +439,11 @@ function fail(message: string): never {
|
|
|
408
439
|
process.exit(1);
|
|
409
440
|
}
|
|
410
441
|
|
|
411
|
-
async function taskOp(op: string, a: Record<string, unknown>, throwOnError = false, connection?: ControlClient): Promise<string> {
|
|
442
|
+
async function taskOp(op: string, a: Record<string, unknown>, throwOnError = false, connection?: ControlClient, timeoutMs?: number): Promise<string> {
|
|
412
443
|
const hub = connection ?? await connect();
|
|
413
444
|
try {
|
|
414
445
|
if (op === "hub_task_cancel" && !hub.features.includes("task_cancel")) fail("this daemon does not support task cancellation; upgrade it first");
|
|
415
|
-
const res = await hub.request({ t: "task", op, args: a }, op === "github_import" ? 60_000 : 30_000);
|
|
446
|
+
const res = await hub.request({ t: "task", op, args: a }, timeoutMs ?? (op === "github_import" ? 60_000 : 30_000));
|
|
416
447
|
if (!res.ok) { if (throwOnError) throw new Error(res.error); audit("refused"); fail(res.error); }
|
|
417
448
|
return res.text;
|
|
418
449
|
} finally { if (!connection) hub.close(); }
|
|
@@ -762,7 +793,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
762
793
|
help: () => {
|
|
763
794
|
const color = resolveColor(undefined, { isTTY: !!process.stdout.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR }) === true;
|
|
764
795
|
const topic = asksHelp(args[0]) ? undefined : args[0];
|
|
765
|
-
const text =
|
|
796
|
+
const text = topic && !definitionOf(topic) && projectCatalog.definitionOf(topic)?.permission.genericAcp !== undefined
|
|
797
|
+
? configuredAcpHelp(topic) : renderHelp(process.stdout.columns ?? 80, color, topic);
|
|
766
798
|
if (!text) fail(`unknown command "${topic}"; run ahub help`);
|
|
767
799
|
console.log(text);
|
|
768
800
|
},
|
|
@@ -1199,6 +1231,17 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1199
1231
|
if (!flags.rest.length || (scope === undefined) !== (reviewers === undefined)) fail("plan-review needs a reason and both --review and --reviewers when supplying review scope");
|
|
1200
1232
|
return console.log(await taskOp("hub_plan_review", { id, revision, reason: flags.rest.join(" "), ...(scope !== undefined ? { review: { scope, reviewers: Number(reviewers) } } : {}), ...(flags.one["--ci"] !== undefined ? { ci: flags.one["--ci"] } : {}) }));
|
|
1201
1233
|
}
|
|
1234
|
+
if (sub === "accept-departure") {
|
|
1235
|
+
const head = args[2], rawReason = args.slice(3).join(" "), reason = rawReason.trim();
|
|
1236
|
+
if (!Number.isSafeInteger(id) || id! <= 0 || typeof head !== "string" || !/^[a-f0-9]{40}$/.test(head) || !reason || rawReason.length > 2048) fail("usage: ahub sdlc accept-departure <task> <head> <reason...>");
|
|
1237
|
+
const hub = await connect();
|
|
1238
|
+
try {
|
|
1239
|
+
const reply = await hub.request({ t: "sdlc", op: "accept-departure", id, head, reason }, 30_000);
|
|
1240
|
+
if (!reply.ok) fail(reply.error);
|
|
1241
|
+
console.log(`task #${id}: plan departure accepted for the reviewed head`);
|
|
1242
|
+
} finally { hub.close(); }
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1202
1245
|
if (sub === "accept") {
|
|
1203
1246
|
// #481 S1: the person accepts the task's spec issue at its current body; agent shells are refused before this runs.
|
|
1204
1247
|
const flags = takeFlags(args.slice(2), ["--issue", "--repo"], []);
|
|
@@ -1284,10 +1327,15 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1284
1327
|
if (args.length !== 2 || !/^\d+$/.test(args[1]!)) fail("usage: ahub review brief <task id>");
|
|
1285
1328
|
return console.log(await taskOp("review_brief", { id: Number(args[1]) }));
|
|
1286
1329
|
}
|
|
1287
|
-
const { one, many, rest } = takeFlags(args, ["--head"], ["--unmet"]);
|
|
1330
|
+
const { one, many, rest } = takeFlags(args, ["--head", "--findings"], ["--unmet"]);
|
|
1288
1331
|
const [id, verdict, ...note] = rest;
|
|
1289
|
-
if (!id || !verdict) fail("usage: ahub review <id> approved|changes_requested [note...] [--unmet <item>]... | ahub review brief <id>");
|
|
1290
|
-
|
|
1332
|
+
if (!id || !verdict) fail("usage: ahub review <id> approved|changes_requested [note...] [--unmet <item>]... [--head <sha>] [--findings <json>] | ahub review brief <id>");
|
|
1333
|
+
let findings: unknown;
|
|
1334
|
+
if (one["--findings"] !== undefined) {
|
|
1335
|
+
try { findings = JSON.parse(one["--findings"]); } catch { fail("--findings must be a JSON array"); }
|
|
1336
|
+
if (!Array.isArray(findings)) fail("--findings must be a JSON array");
|
|
1337
|
+
}
|
|
1338
|
+
console.log(await taskOp("hub_review", { id: Number(id), verdict, ...(one["--head"] ? { head: one["--head"] } : {}), note: note.join(" "), ...(findings === undefined ? {} : { findings }), ...(many["--unmet"]?.length ? { unmet: many["--unmet"] } : {}) }));
|
|
1291
1339
|
},
|
|
1292
1340
|
|
|
1293
1341
|
remember: async () => console.log(await taskOp("hub_remember", { text: freeText(args, "ahub remember <text>") })),
|
|
@@ -1338,7 +1386,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1338
1386
|
const [peer, mode] = positional;
|
|
1339
1387
|
if (positional.length > 2 || positional.some(arg => arg.startsWith("--")) || (mode !== undefined && !isPermissionMode(mode))) fail("usage: ahub permission [<peer> [ask|ask-when-needed|never-ask]] [--yes]");
|
|
1340
1388
|
if (mode === "never-ask" && !args.includes("--yes")) fail("never-ask requires --yes; nothing was changed");
|
|
1341
|
-
|
|
1389
|
+
const { scope: permissionScope, error: permissionScopeError } = localDeclarations();
|
|
1390
|
+
if (mode === "never-ask" && peer) console.error(`never-ask: ${permissionBoundary(peer, permissionScope.instances, permissionScope.catalog)}; approval prompts are disabled${permissionScopeError ? ` (project config unreadable: ${permissionScopeError})` : ""}`);
|
|
1342
1391
|
const hub = await connect();
|
|
1343
1392
|
try {
|
|
1344
1393
|
const reply = await hub.request({ t: "permission", ...(peer ? { peer } : {}), ...(mode ? { mode, confirmed: args.includes("--yes") } : {}) }, 35_000);
|
|
@@ -1358,7 +1407,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1358
1407
|
roster: async () => {
|
|
1359
1408
|
const options = outputOptions(), flags = args.filter(arg => arg.startsWith("--")), positional = outputArguments().filter(arg => arg !== "--yes" && arg !== "--now");
|
|
1360
1409
|
const [sub = "list", peer, ...extra] = positional;
|
|
1361
|
-
if (!["list", "add", "remove"].includes(sub) || extra.length || (sub === "list" ? peer !== undefined : !peerDefinition(peer)) || flags.some(flag => !["--json", "--full", "--yes", "--now"].includes(flag) && !flag.startsWith("--color=")) || (sub !== "list" && options.json) || (sub !== "remove" && flags.some(flag => ["--yes", "--now"].includes(flag)))) fail("usage: ahub roster [--json] | add <peer> | remove <peer> [--yes] [--now]");
|
|
1410
|
+
if (!["list", "add", "remove"].includes(sub) || extra.length || (sub === "list" ? peer !== undefined : !peerDefinition(peer) && !(sub === "remove" && typeof peer === "string" && staleInstanceKey(peer, projectCatalog))) || flags.some(flag => !["--json", "--full", "--yes", "--now"].includes(flag) && !flag.startsWith("--color=")) || (sub !== "list" && options.json) || (sub !== "remove" && flags.some(flag => ["--yes", "--now"].includes(flag)))) fail("usage: ahub roster [--json] | add <peer> | remove <peer> [--yes] [--now]");
|
|
1362
1411
|
const hub = await connect();
|
|
1363
1412
|
try {
|
|
1364
1413
|
requireNamedPeerFeature(hub, peer);
|
|
@@ -1368,7 +1417,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1368
1417
|
if (!Array.isArray(view.roster)) fail("upgrade the running hub to use ahub roster");
|
|
1369
1418
|
return options.json ? console.log(JSON.stringify(view.roster, null, 2)) : printOutput(renderRoster(view.roster, options.columns), options.color);
|
|
1370
1419
|
}
|
|
1371
|
-
|
|
1420
|
+
// A key whose instance is no longer declared is deleted (null), never set to false: there is no peer left for it to switch off (#225).
|
|
1421
|
+
const key = `roster.${peer}`, stale = sub === "remove" && !peerDefinition(peer) && staleInstanceKey(peer!, projectCatalog), value = stale ? null : sub === "add";
|
|
1372
1422
|
const preview = await request({ t: "settings_preview", key, value });
|
|
1373
1423
|
printOutput((preview.lines ?? []).map((text: string) => [{ text }]), options.color);
|
|
1374
1424
|
if (sub === "remove" && !flags.includes("--yes")) {
|
|
@@ -1387,14 +1437,15 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1387
1437
|
const allowed: Record<string, string[]> = { list: ["--json"], get: ["--json"], set: ["--yes", "--preview", "--now"], undo: [], reload: [] };
|
|
1388
1438
|
if (!Object.hasOwn(allowed, sub) || flags.some((flag) => !allowed[sub]!.includes(flag)) || extra.length) fail(usage);
|
|
1389
1439
|
if ((sub === "get" && (!key || text !== undefined)) || (sub === "set" && (!key || text === undefined)) || ((sub === "list" || sub === "undo" || sub === "reload") && key !== undefined)) fail(usage);
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1440
|
+
const { scope: settingScope, error: settingScopeError } = localDeclarations();
|
|
1441
|
+
const def = key === undefined ? undefined : settingDef(key, settingScope.instances, settingScope.catalog);
|
|
1442
|
+
if (typeof def === "string") fail(settingScopeError && def.startsWith("unknown setting") ? `${key}: ${def}; the project config could not be read: ${settingScopeError}` : `${key}: ${def}`);
|
|
1392
1443
|
if (flags.includes("--now") && (typeof def !== "object" || def.group !== "Roster")) fail(usage);
|
|
1393
1444
|
const value = sub === "set" ? parseSettingText(def!, text!) : undefined;
|
|
1394
1445
|
const peer = def?.group === "Permissions" || def?.group === "Roster" ? def.path.at(-1)! : undefined;
|
|
1395
1446
|
if (sub === "set" && value === "never-ask" && !flags.includes("--preview")) {
|
|
1396
1447
|
if (!flags.includes("--yes")) fail("never-ask requires --yes; nothing was changed");
|
|
1397
|
-
console.error(`never-ask: ${permissionBoundary(peer
|
|
1448
|
+
console.error(`never-ask: ${permissionBoundary(peer!, settingScope.instances, settingScope.catalog)}; approval prompts are disabled`);
|
|
1398
1449
|
}
|
|
1399
1450
|
const hub = await connect();
|
|
1400
1451
|
try {
|
|
@@ -1444,7 +1495,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1444
1495
|
return console.log(paint([{ text: result.text }], false));
|
|
1445
1496
|
}
|
|
1446
1497
|
if (sub === "undo") {
|
|
1447
|
-
const view = await request({ t: "settings_get" }), undoDef = settingDef(view.undo);
|
|
1498
|
+
const view = await request({ t: "settings_get" }), undoDef = settingDef(view.undo, settingScope.instances, settingScope.catalog);
|
|
1499
|
+
// A roster undo of a declared peer needs its confirmation prompt; without readable declarations, refuse rather than skip it.
|
|
1500
|
+
if (typeof undoDef === "string" && settingScopeError) fail(`pending undo ${String(view.undo)}: ${undoDef}; the project config could not be read: ${settingScopeError}`);
|
|
1448
1501
|
const rosterPeer = typeof undoDef !== "string" && undoDef.group === "Roster" ? undoDef.path[1] : undefined;
|
|
1449
1502
|
let confirm: string | undefined;
|
|
1450
1503
|
if (rosterPeer) {
|
|
@@ -1662,7 +1715,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1662
1715
|
facts: async () => {
|
|
1663
1716
|
if (!args.includes("--hook")) return fail("usage: ahub facts --hook (a Claude Code PreToolUse, PostToolUse and Stop hook)");
|
|
1664
1717
|
try {
|
|
1665
|
-
const out = await factsHook(await Bun.stdin.text(), stateDir, process.env.AGENTHUB_PEER_ID ??
|
|
1718
|
+
const out = await factsHook(await Bun.stdin.text(), stateDir, process.env.AGENTHUB_PEER_ID ?? defaultChannelPeer(undefined));
|
|
1666
1719
|
if (out) console.log(out);
|
|
1667
1720
|
} catch {
|
|
1668
1721
|
// a hook that fails must not get in the way of the tool call
|
|
@@ -1671,7 +1724,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1671
1724
|
"check-path": async () => {
|
|
1672
1725
|
const hook = args.includes("--hook");
|
|
1673
1726
|
const { one, rest } = takeFlags(args.filter((a) => a !== "--hook"), ["--peer"], []);
|
|
1674
|
-
const peer = one["--peer"] ??
|
|
1727
|
+
const peer = one["--peer"] ?? defaultChannelPeer(undefined);
|
|
1675
1728
|
let target = rest[0];
|
|
1676
1729
|
try {
|
|
1677
1730
|
if (hook) {
|
|
@@ -1728,10 +1781,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1728
1781
|
const undoWorkspace = (): string => {
|
|
1729
1782
|
// Old turns, including external peers and ids declared only later, belong to the canonical checkout.
|
|
1730
1783
|
if (recordedWorkspace === undefined) return cwd;
|
|
1731
|
-
const declarations = readInstanceConfig(cwd)
|
|
1784
|
+
const { instances: declarations, catalog } = readInstanceConfig(cwd);
|
|
1732
1785
|
if (!declarations || !Object.hasOwn(declarations, turn.peer)) fail(`turn ${id}'s recorded instance is no longer declared; nothing was restored`);
|
|
1733
1786
|
const config = projectConfig();
|
|
1734
|
-
const current = resolveInstanceExecutionCwd(cwd, turn.peer, declarations, effectiveInstanceRoles(turn.peer, declarations, config.roles));
|
|
1787
|
+
const current = resolveInstanceExecutionCwd(cwd, turn.peer, declarations, effectiveInstanceRoles(turn.peer, declarations, config.roles, catalog), catalog);
|
|
1735
1788
|
if (realPath(current) !== realPath(recordedWorkspace)) fail(`turn ${id}'s recorded execution workspace changed; nothing was restored`);
|
|
1736
1789
|
return current;
|
|
1737
1790
|
};
|
|
@@ -2061,7 +2114,18 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2061
2114
|
input = { peer: args[0] };
|
|
2062
2115
|
} else {
|
|
2063
2116
|
// Native TUIs return a human launch command through the daemon; an agent shell never executes it.
|
|
2064
|
-
|
|
2117
|
+
if (configuredCommand) {
|
|
2118
|
+
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances);
|
|
2119
|
+
if (selection.preview) return runConfiguredAcpEntry({ kind: cmd, raw: args, catalog: projectCatalog, instances: declaredInstances, connect, print: text => console.log(text) });
|
|
2120
|
+
if (selection.sessionId) fail("conductor starts accept no session override; a person starts a configured ACP session");
|
|
2121
|
+
const hub = await connect();
|
|
2122
|
+
try {
|
|
2123
|
+
await requireActiveConfiguredAcp(hub, cmd, selection.peer, text => console.error(text));
|
|
2124
|
+
console.log(await taskOp("hub_peer_start", { peer: selection.peer, mode: "headless" }, false, hub, 70_000));
|
|
2125
|
+
} finally { hub.close(); }
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
const parseStart = projectCatalog.definitionOf(cmd)?.start.conductorStartArgs;
|
|
2065
2129
|
const parsed = parseStart ? parseStart(args) : args.length ? new Error(`conductor starts accept no launch overrides; use ahub ${cmd}`) : {};
|
|
2066
2130
|
if (parsed instanceof Error) fail(parsed.message);
|
|
2067
2131
|
op = "hub_peer_start";
|
|
@@ -2071,7 +2135,12 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2071
2135
|
if (selectedInstance) { const hub = await connect(); try { requirePeerInstances(hub.features); } finally { hub.close(); } }
|
|
2072
2136
|
console.log(await taskOp(op, input));
|
|
2073
2137
|
}
|
|
2074
|
-
const
|
|
2138
|
+
const runConfigured = configuredCommand ? async () => {
|
|
2139
|
+
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances);
|
|
2140
|
+
if (!selection.preview) assertLifecycleAvailable();
|
|
2141
|
+
await runConfiguredAcpEntry({ kind: cmd, raw: args, catalog: projectCatalog, instances: declaredInstances, connect, print: text => console.log(text), operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
2142
|
+
} : undefined;
|
|
2143
|
+
const run = identity.role === "tools" && commandAccess === "conductor" ? runConductorCommand : (Object.hasOwn(commands, cmd) ? commands[cmd] : undefined) ?? runConfigured ?? (() => fail(`unknown command "${cmd}"; run ahub help`));
|
|
2075
2144
|
try {
|
|
2076
2145
|
assertDelegatedAuditConsumer(); // Local command paths must not begin after the authenticated consumer was replaced either.
|
|
2077
2146
|
await run();
|
package/src/cli/memory.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { detectCliIdentity } from "./identity.ts";
|
|
2
2
|
import { createStalenessProbe } from "../hub/memory-staleness.ts";
|
|
3
|
-
import { looksSecret, type MemoryStore } from "@staix/agent-mem";
|
|
3
|
+
import { looksSecret, memoryProjectKey, type MemoryStore } from "@staix/agent-mem";
|
|
4
4
|
import type { SyncOptions } from "@staix/agent-mem";
|
|
5
5
|
import { MEMORY_USAGE as CORE_MEMORY_USAGE, rawMemoryCommand, rawGoalCommand } from "@staix/agent-mem";
|
|
6
6
|
import { mentionsPrivatePath, promotionScreen, withoutPii } from "../hub/memory-policy.ts";
|
|
7
7
|
import { currentRouting, detectSignals } from "../hub/routing.ts";
|
|
8
8
|
import { loadConfig } from "../hub/daemon.ts";
|
|
9
|
-
import { bindProfileMemory, memoryProjectFor, openProfileMemory, MEMORY_PROFILE_UNAVAILABLE, MEMORY_SPLIT_INCOMPLETE, type MemoryProfileReceipt, type ProfileMemoryOptions } from "../hub/memory-profile.ts";
|
|
9
|
+
import { bindProfileMemory, memoryProjectFor, memoryProjectForStore, openProfileMemory, MEMORY_PROFILE_UNAVAILABLE, MEMORY_SPLIT_INCOMPLETE, type MemoryProfileReceipt, type ProfileMemoryOptions } from "../hub/memory-profile.ts";
|
|
10
10
|
|
|
11
11
|
export { GOAL_USAGE } from "@staix/agent-mem";
|
|
12
12
|
export const MEMORY_USAGE = `${CORE_MEMORY_USAGE} | decay [--days N] [--json] | profile-check [--json]`;
|
|
@@ -16,9 +16,12 @@ const fail = (message: string): never => { throw new Error(message); };
|
|
|
16
16
|
const loadConfigSafe = (cwd: string) => { try { return loadConfig(cwd); } catch { return undefined; } };
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
/** The project key: an injected store resolves the alias in its own home, the default open in the ambient one (#240). */
|
|
20
|
+
const projectOf = (cwd: string, provided: MemoryStore | undefined): string => provided ? memoryProjectForStore(provided, cwd) : memoryProjectFor(cwd);
|
|
21
|
+
|
|
19
22
|
/** Bind each command at entry; the CLI closes default opens and injected stores remain caller-owned. */
|
|
20
|
-
function commandStores(provided: MemoryStore | undefined, options: ProfileMemoryOptions) {
|
|
21
|
-
try { return { ...(provided ? bindProfileMemory(provided) : openProfileMemory(options)), owned: !provided }; }
|
|
23
|
+
function commandStores(provided: MemoryStore | undefined, options: ProfileMemoryOptions, cwd: string) {
|
|
24
|
+
try { return { ...(provided ? bindProfileMemory(provided, undefined, memoryProjectKey(cwd)) : openProfileMemory(options)), owned: !provided }; }
|
|
22
25
|
catch (error) { throw new Error(error instanceof Error && error.message === MEMORY_SPLIT_INCOMPLETE ? error.message : MEMORY_PROFILE_UNAVAILABLE); }
|
|
23
26
|
}
|
|
24
27
|
function profileOutput(text: string, json: boolean, profile: MemoryProfileReceipt | undefined, person: boolean): string {
|
|
@@ -43,9 +46,9 @@ export async function memoryCommand(args: string[], cwd: string, opts: { store?:
|
|
|
43
46
|
const cutoffMs = (opts.now ?? Date.now)() - days * 24 * 3600_000;
|
|
44
47
|
if (!Number.isSafeInteger(days) || !Number.isFinite(cutoffMs) || Math.abs(cutoffMs) > 8_640_000_000_000_000) fail("--days is outside the supported date range");
|
|
45
48
|
const cutoff = new Date(cutoffMs).toISOString();
|
|
46
|
-
const project =
|
|
49
|
+
const project = projectOf(cwd, opts.store), routing = currentRouting(cwd), paths = privatePaths();
|
|
47
50
|
// Review must not promote a held source while reading it: only explicit person actions may append rows.
|
|
48
|
-
const context = commandStores(opts.store, { project, deferPromotions: true, now: opts.now });
|
|
51
|
+
const context = commandStores(opts.store, { project, deferPromotions: true, now: opts.now }, cwd);
|
|
49
52
|
const store = context.store;
|
|
50
53
|
try {
|
|
51
54
|
context.binding.assertCurrent();
|
|
@@ -67,9 +70,9 @@ export async function memoryCommand(args: string[], cwd: string, opts: { store?:
|
|
|
67
70
|
} finally { if (context.owned) store.close(); }
|
|
68
71
|
}
|
|
69
72
|
let showStale = false;
|
|
70
|
-
const project =
|
|
73
|
+
const project = projectOf(cwd, opts.store);
|
|
71
74
|
const context = commandStores(opts.store, { project, ttlMs: (loadConfigSafe(cwd)?.memory.short_ttl_days ?? 14) * 24 * 3600_000,
|
|
72
|
-
screen: promotionScreen(project, cwd, () => currentRouting(cwd), privatePaths) });
|
|
75
|
+
screen: promotionScreen(project, cwd, () => currentRouting(cwd), privatePaths) }, cwd);
|
|
73
76
|
try {
|
|
74
77
|
context.binding.assertCurrent();
|
|
75
78
|
const text = await rawMemoryCommand(args, cwd, { ...opts, project, store: context.store, open: () => context.store,
|
|
@@ -92,8 +95,8 @@ export async function memoryCommand(args: string[], cwd: string, opts: { store?:
|
|
|
92
95
|
export async function goalCommand(args: string[], cwd: string, opts: { store?: MemoryStore; link?: (goal: string, task: string) => Promise<string>; person?: boolean } = {}): Promise<string> {
|
|
93
96
|
const person = opts.person ?? detectCliIdentity(process.env).role === "console";
|
|
94
97
|
const policy = (): string[] => { try { return loadConfig(cwd).pii?.paths ?? []; } catch (e) { return fail(`the project's config does not load (${(e as Error).message}); fix it before using long-term tasks`); } };
|
|
95
|
-
const project =
|
|
96
|
-
const context = commandStores(opts.store, { project });
|
|
98
|
+
const project = projectOf(cwd, opts.store);
|
|
99
|
+
const context = commandStores(opts.store, { project }, cwd);
|
|
97
100
|
try {
|
|
98
101
|
context.binding.assertCurrent();
|
|
99
102
|
const text = await rawGoalCommand(args, cwd, { ...opts, project, store: context.store, open: () => context.store,
|
package/src/cli/output.ts
CHANGED
|
@@ -246,6 +246,7 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
246
246
|
if (p.toolsOnly) out.push(...detail(definitionOf(id)?.attach?.pullOnlyForms?.includes("tui") && p.pullOnly ? "pull-only" : "tools-only", flat(p.toolsOnly).replace(/^(?:tools-only|pull-only):\s*/, ""), columns, "attention"));
|
|
247
247
|
if (p.pullOnly && definitionOf(id)?.attach?.pullOnlyForms?.includes("tui")) out.push(...detail(`${id} last read`, typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never", columns, "muted"));
|
|
248
248
|
if (p.permissionNote) out.push(...detail("permission", p.permissionNote, columns, "muted"));
|
|
249
|
+
if (typeof p.statusNotice?.message === "string" && p.statusNotice.message) out.push(...detail("status notice", p.statusNotice.message, columns, "attention"));
|
|
249
250
|
if (p.requestedModel && p.servedBy && p.requestedModel !== p.servedBy) out.push(...detail("requested model", p.requestedModel, columns));
|
|
250
251
|
if (typeof p.importantWaitingSince === "number" && p.importantWaitingNote) out.push(...detail("important waiting", `${p.importantWaitingNote.replace(/^\d+ important messages? waiting/, "important messages waiting")}`, columns, "attention"));
|
|
251
252
|
if (typeof p.oldestQueuedAt === "number") out.push(...detail(`${id} oldest queued`, relative(p.oldestQueuedAt, now), columns, "muted"));
|
package/src/cli/preview.ts
CHANGED
|
@@ -32,3 +32,12 @@ export function launcherPreview(tool: "claude" | "codex" | "kimi" | "pi" | "qwen
|
|
|
32
32
|
unresolved: [...result.unresolved, { field: "executionCwd", reason: "verified by the daemon at instance start; preview does not start or authorize a worktree" }],
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/** Configured commands are arbitrary machine-local argv; preview never displays their values or starts a child. */
|
|
37
|
+
export function configuredAcpPreview(kind: string, peer: string, resumed: boolean) {
|
|
38
|
+
return { tool: kind, peer, mode: "headless", cmd: "[redacted]", args: ["[redacted]"],
|
|
39
|
+
...(resumed ? { sessionId: "[redacted]" } : {}),
|
|
40
|
+
envNames: [], redaction: "Configured executable, argv and session values are withheld.",
|
|
41
|
+
unresolved: [{ field: "runtime", reason: "Native identity, owned process group, accounts and daemon catalog are checked at start; preview runs nothing." }],
|
|
42
|
+
needs: ["A trusted config.local.json declaration and a running hub with configured_acp support."] };
|
|
43
|
+
}
|
|
@@ -27,7 +27,7 @@ export interface RecoveryGuidanceOptions {
|
|
|
27
27
|
|
|
28
28
|
const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
29
29
|
const argv = (...words: string[]): string => words.map(shellQuote).join(" ");
|
|
30
|
-
const projectDefinition = (peer: string, root: string) => definitionOf(peer)
|
|
30
|
+
const projectDefinition = (peer: string, root: string) => { const builtin = definitionOf(peer); if (builtin) return builtin; const bound = readInstanceConfig(root); return bound.catalog.definitionOf(peer, bound.instances); };
|
|
31
31
|
const peers = (instances: KindMap | undefined): RecoveryNativePeer[] => [...ADAPTERS.map(adapter => adapter.kind), ...Object.keys(instances ?? {})]
|
|
32
32
|
.filter(peer => definitionOf(peer, instances)?.session.launchRecord)
|
|
33
33
|
.sort((a, b) => Number(definitionOf(a, instances)?.transport === "claude-channel") - Number(definitionOf(b, instances)?.transport === "claude-channel"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { shellQuote } from "./shell-quote.ts";
|
|
2
2
|
export { shellQuote } from "./shell-quote.ts";
|
|
3
|
-
import { definitionOf, type KindMap } from "../adapters/registry.ts";
|
|
3
|
+
import { createProjectAdapterCatalog, definitionOf, type ProjectAdapterCatalog, type KindMap } from "../adapters/registry.ts";
|
|
4
4
|
import { readInstanceConfig, readInstanceRoleConfig } from "../hub/instance-config.ts";
|
|
5
5
|
import { effectiveInstanceRoles } from "../hub/instances.ts";
|
|
6
6
|
import { resolveInstanceExecutionCwd } from "../hub/instance-execution.ts";
|
|
@@ -332,8 +332,8 @@ export function recordPath(stateDir: string): string {
|
|
|
332
332
|
function executionRoot(peer: string, projectRoot: string, captured?: string): string {
|
|
333
333
|
const builtin = definitionOf(peer);
|
|
334
334
|
if (builtin?.session.launchRecord) return projectRoot;
|
|
335
|
-
const { instances: declarations, roles } = readInstanceRoleConfig(projectRoot);
|
|
336
|
-
const definition = definitionOf(peer, declarations);
|
|
335
|
+
const { instances: declarations, roles, catalog } = readInstanceRoleConfig(projectRoot);
|
|
336
|
+
const definition = catalog.definitionOf(peer, declarations);
|
|
337
337
|
if (!definition?.session.launchRecord) throw new Error(`${peer}: no managed terminal recovery adapter`);
|
|
338
338
|
if (!declarations?.[peer] || definition.transport !== "codex-appserver" && definition.transport !== "claude-channel") throw new Error(`${peer}: extra instance has no managed terminal support`);
|
|
339
339
|
// #450 I3: a Claude instance reviews or plans in the canonical checkout; it never owns an execution worktree.
|
|
@@ -341,7 +341,7 @@ function executionRoot(peer: string, projectRoot: string, captured?: string): st
|
|
|
341
341
|
if (captured !== undefined && captured !== projectRoot) throw new Error(`${peer}: recorded execution root changed; manual recovery required`);
|
|
342
342
|
return projectRoot;
|
|
343
343
|
}
|
|
344
|
-
const expected = resolveInstanceExecutionCwd(projectRoot, peer, declarations, effectiveInstanceRoles(peer, declarations, roles));
|
|
344
|
+
const expected = resolveInstanceExecutionCwd(projectRoot, peer, declarations, effectiveInstanceRoles(peer, declarations, roles, catalog), catalog);
|
|
345
345
|
if (captured !== undefined && captured !== expected) throw new Error(`${peer}: declared execution worktree changed; manual recovery required`);
|
|
346
346
|
return expected;
|
|
347
347
|
}
|
|
@@ -556,12 +556,12 @@ function sessionFrom(terminal: Record<string, unknown>): string | undefined {
|
|
|
556
556
|
return nestedString(terminal, ["sessionId", "agentSessionId", "resumeSessionId"]) ?? nestedString(wait, ["sessionId", "agentSessionId", "resumeSessionId", "session", "agentSession"]) ?? nestedString(session, ["id", "sessionId"]);
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
-
function identityFrom(terminal: Record<string, unknown>, instances
|
|
559
|
+
function identityFrom(terminal: Record<string, unknown>, instances: KindMap | undefined, catalog: ProjectAdapterCatalog): TerminalPeer | undefined {
|
|
560
560
|
const wait = object(terminal.agentWait);
|
|
561
561
|
const session = object(terminal.session);
|
|
562
562
|
const candidates = [terminal.agentIdentity, terminal.agent, wait.agentIdentity, wait.agent, session.agentIdentity, session.agent];
|
|
563
563
|
for (const candidate of candidates) {
|
|
564
|
-
if (typeof candidate === "string" && definitionOf(candidate, instances)?.session.launchRecord) return candidate as TerminalPeer;
|
|
564
|
+
if (typeof candidate === "string" && catalog.definitionOf(candidate, instances)?.session.launchRecord) return candidate as TerminalPeer;
|
|
565
565
|
}
|
|
566
566
|
return undefined;
|
|
567
567
|
}
|
|
@@ -599,7 +599,8 @@ function launchFor(peer: TerminalPeer, projectRoot: string, sessionId: string, o
|
|
|
599
599
|
const binding: TerminalBinding = { peer, projectRoot, sessionId, handle: nestedString(source, ["handle"])!,
|
|
600
600
|
incarnationId: nestedString(source, ["incarnationId"])!, worktreeId: nestedString(source, ["worktreeId"])!,
|
|
601
601
|
...(ref?.sessionFile ? { sessionFile: ref.sessionFile } : {}), ...(ref?.backend ? { backend: ref.backend } : {}), ...(ref?.model ? { model: ref.model } : {}), launch, launchMetadata: launch };
|
|
602
|
-
const
|
|
602
|
+
const scope = definitionOf(peer) ? undefined : readInstanceConfig(projectRoot);
|
|
603
|
+
const adapter = definitionOf(peer) ?? scope!.catalog.definitionOf(peer, scope!.instances);
|
|
603
604
|
const nativeTail = adapter?.session.resumeArgv?.(binding, false);
|
|
604
605
|
const tail = nativeTail && adapter ? [nativeTail[0]!, ...(peer !== adapter.kind ? ["--as", peer] : []), ...nativeTail.slice(1)] : undefined;
|
|
605
606
|
if (!tail) throw new Error(`${peer}: no recorded terminal resume adapter`);
|
|
@@ -697,7 +698,8 @@ export async function inspectTerminals(projectRoot: string, sessions: Partial<Re
|
|
|
697
698
|
const byPeer: Partial<Record<TerminalPeer, TerminalBinding>> = {};
|
|
698
699
|
const requested = (Object.entries(sessions) as [TerminalPeer, string | SessionRef | undefined][]).map(([peer, value]) => [peer, typeof value === "string" ? { sessionId: value } : value] as const).filter((entry): entry is [TerminalPeer, SessionRef] => !!entry[1]?.sessionId);
|
|
699
700
|
if (!requested.length) return { bindings, byPeer, blockers, manualRequired: false };
|
|
700
|
-
const
|
|
701
|
+
const scope = requested.some(([peer]) => definitionOf(peer) === undefined) ? readInstanceConfig(projectRoot) : { instances: undefined, catalog: createProjectAdapterCatalog() };
|
|
702
|
+
const { instances, catalog } = scope;
|
|
701
703
|
// #215: without the records a terminal could be bound from Orca metadata alone, losing the launch's captured
|
|
702
704
|
// CODEX_HOME or CLAUDE_CONFIG_DIR: an unreadable file blocks the binding, never reads as "no records".
|
|
703
705
|
const rows = launchRecords(config.stateDir);
|
|
@@ -714,7 +716,7 @@ export async function inspectTerminals(projectRoot: string, sessions: Partial<Re
|
|
|
714
716
|
|
|
715
717
|
for (const [peer, ref] of requested) {
|
|
716
718
|
const sessionId = ref.sessionId;
|
|
717
|
-
const adapter = definitionOf(peer, instances), extra = definitionOf(peer) === undefined;
|
|
719
|
+
const adapter = catalog.definitionOf(peer, instances), extra = definitionOf(peer) === undefined;
|
|
718
720
|
if (!adapter?.session.launchRecord || extra && adapter.transport !== "codex-appserver" && adapter.transport !== "claude-channel") { blockers.push(blocker("ownership-unknown", `${peer}: no declared managed-terminal recovery adapter`, peer)); continue; }
|
|
719
721
|
const ownRecord = records.find(item => item.peer === peer);
|
|
720
722
|
let expectedRoot: string, expectedTerminalRoot: string;
|
|
@@ -747,7 +749,7 @@ export async function inspectTerminals(projectRoot: string, sessions: Partial<Re
|
|
|
747
749
|
}
|
|
748
750
|
// A show readback is required; list can be stale between recovery steps.
|
|
749
751
|
if (!rootMatches(terminal.worktreePath ?? terminal.projectRoot, expectedTerminalRoot)) continue;
|
|
750
|
-
const identity = identityFrom(terminal, instances);
|
|
752
|
+
const identity = identityFrom(terminal, instances, catalog);
|
|
751
753
|
const terminalSession = sessionFrom(terminal);
|
|
752
754
|
const recorded = records.find((item) => item.peer === peer && item.handle === handle && item.worktreeId === terminal.worktreeId && item.incarnationId === terminal.incarnationId);
|
|
753
755
|
// A recorded launcher proves ownership; the daemon's supplied session id is the
|
|
@@ -755,7 +757,7 @@ export async function inspectTerminals(projectRoot: string, sessions: Partial<Re
|
|
|
755
757
|
if ((!extra && identity === peer && terminalSession === sessionId) || (recorded !== undefined && (identity === undefined || identity === peer || extra && identity === adapter.kind) && (terminalSession === undefined || terminalSession === sessionId))) peerCandidates.push(terminal);
|
|
756
758
|
}
|
|
757
759
|
if (!peerCandidates.length) {
|
|
758
|
-
const ownershipUnknown = candidates.some((terminal) => identityFrom(terminal, instances) === undefined && !records.some((item) => item.handle === terminal.handle));
|
|
760
|
+
const ownershipUnknown = candidates.some((terminal) => identityFrom(terminal, instances, catalog) === undefined && !records.some((item) => item.handle === terminal.handle));
|
|
759
761
|
blockers.push(ownershipUnknown
|
|
760
762
|
? blocker("ownership-unknown", `cannot prove ${peer} terminal ownership from Orca metadata`, peer)
|
|
761
763
|
: blocker("missing-terminal", `no ${peer} terminal with session ${sessionId} in ${projectRoot}`, peer));
|
|
@@ -784,7 +786,7 @@ export async function inspectTerminals(projectRoot: string, sessions: Partial<Re
|
|
|
784
786
|
const recorded = records.find((item) => item.peer === peer && item.handle === handle && item.worktreeId === terminal.worktreeId && item.incarnationId === terminal.incarnationId);
|
|
785
787
|
// Orca observation supplies file identity; launch arguments are not observed native session-file evidence.
|
|
786
788
|
const { args: _launchArgs, ...observedTerminal } = terminal;
|
|
787
|
-
const sessionFile = definitionOf(peer, instances)?.session.crashIdentity?.(observedTerminal).sessionFile;
|
|
789
|
+
const sessionFile = catalog.definitionOf(peer, instances)?.session.crashIdentity?.(observedTerminal).sessionFile;
|
|
788
790
|
const launch = launchFor(peer, projectRoot, sessionId, config, terminal, recorded, { ...ref, ...(sessionFile ? { sessionFile } : {}) });
|
|
789
791
|
const binding: TerminalBinding = { peer, handle, incarnationId, worktreeId, projectRoot, ...(extra ? { executionCwd: expectedRoot, terminalRoot: expectedTerminalRoot } : {}), sessionId, ...(sessionFile ? { sessionFile } : {}), ...(ref.backend ? { backend: ref.backend } : {}), ...(ref.model ? { model: ref.model } : {}), launch, launchMetadata: launch };
|
|
790
792
|
bindings.push(binding);
|
|
@@ -919,7 +921,8 @@ export async function createTerminal(binding: TerminalBinding, options?: Command
|
|
|
919
921
|
const launch = await launches();
|
|
920
922
|
if (launch.gone) return exited("exited");
|
|
921
923
|
const shown = await showBinding(replacement, config);
|
|
922
|
-
const
|
|
924
|
+
const scope = definitionOf(binding.peer) ? { instances: undefined, catalog: createProjectAdapterCatalog() } : readInstanceConfig(binding.projectRoot);
|
|
925
|
+
const identity = identityFrom(shown, scope.instances, scope.catalog);
|
|
923
926
|
session = sessionFrom(shown);
|
|
924
927
|
const recorded = launch.live;
|
|
925
928
|
if ((identity === binding.peer && same(session)) || (recorded && (identity === undefined || identity === binding.peer) && (session === undefined || same(session)))) break;
|