@staix/agent-hub 0.19.0 → 0.21.0
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 +70 -3
- package/README.md +2 -2
- package/docs/agent-notes/README.md +11 -0
- package/docs/agent-notes/adapters.md +10 -5
- package/docs/agent-notes/benchmarks.md +27 -0
- package/docs/agent-notes/budget.md +2 -1
- package/docs/agent-notes/bus.md +3 -3
- package/docs/agent-notes/daemon.md +20 -7
- package/docs/agent-notes/local-worker.md +5 -0
- package/docs/agent-notes/models.md +2 -0
- package/docs/agent-notes/tasks.md +20 -10
- package/docs/agent-notes/tests.md +13 -5
- package/docs/bench.md +1 -1
- package/docs/data-policy.md +2 -0
- package/docs/dogfood.md +41 -5
- package/docs/events.md +2 -1
- package/docs/memory.md +9 -3
- package/docs/operations.md +598 -63
- package/docs/profiles.md +13 -1
- package/docs/quickstart.md +18 -9
- package/docs/research.md +7 -4
- package/docs/sdlc.md +66 -10
- package/docs/security.md +397 -35
- package/docs/smoke.md +127 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +1149 -19
- package/docs/verification/2026-10-27-0.19.0.md +5 -5
- package/docs/verification/2026-10-27-0.20.0.md +75 -0
- package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-bash-permission.json +635 -0
- package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-permission-transport.json +403 -0
- package/docs/verification/2026-10-28-claude-implementer-permission-triage.md +123 -0
- package/docs/verification/2026-10-28-jcode-0.99.1-usage.md +41 -0
- package/docs/verification/2026-10-28-realpath-name-audit.md +222 -0
- package/docs/verification/2026-10-28-routing-f0bb8ad0.md +1000 -0
- package/docs/verification/2026-10-29-0.21.0.md +61 -0
- package/docs/verification/2026-10-29-model-effort-capabilities.md +123 -0
- package/docs/verification/2026-10-29-modelrouting-b152ddb9.md +953 -0
- package/docs/verification/claude-implementer-permission-triage.ts +160 -0
- package/docs/verification/claude-permission-transport-native.py +71 -14
- package/docs/verification/profiles/YYYY-MM-DD-research.md +1 -1
- package/docs/verified.json +26 -18
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1116 -284
- package/src/adapters/acp.ts +87 -3
- package/src/adapters/channel-peer.ts +11 -5
- package/src/adapters/claude-channel.ts +13 -6
- package/src/adapters/claude-implementer.ts +61 -7
- package/src/adapters/codex-appserver.ts +88 -5
- package/src/adapters/conformance/enrollment.ts +4 -4
- package/src/adapters/conformance/factories.ts +2 -1
- package/src/adapters/conformance/fakes/jcode-agent.ts +6 -2
- package/src/adapters/conformance/kit.ts +10 -6
- package/src/adapters/definitions/builtin-metadata.ts +3 -114
- package/src/adapters/definitions/native-config-defaults.ts +1 -1
- package/src/adapters/definitions/native-config-policy.ts +3 -2
- package/src/adapters/definitions/native-context.ts +12 -0
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-modules.ts +63 -3
- package/src/adapters/jcode.ts +6 -2
- package/src/adapters/local-worker.ts +42 -25
- package/src/adapters/native-proof-ports.ts +10 -0
- package/src/adapters/pi.ts +7 -0
- package/src/adapters/registry-types.ts +4 -0
- package/src/adapters/reviewer-print.ts +1 -0
- package/src/broker/protocol.ts +203 -0
- package/src/broker/server.ts +314 -0
- package/src/broker/store.ts +548 -0
- package/src/cli/bench.ts +3 -2
- package/src/cli/broker.ts +58 -0
- package/src/cli/conductor-recovery.ts +142 -0
- package/src/cli/configured-acp-entry.ts +26 -6
- package/src/cli/console-state.ts +59 -17
- package/src/cli/console.ts +17 -2
- package/src/cli/facts-hook.ts +3 -1
- package/src/cli/help.ts +28 -16
- package/src/cli/identity.ts +23 -10
- package/src/cli/init.ts +34 -7
- package/src/cli/intake-output.ts +33 -0
- package/src/cli/main.ts +314 -71
- package/src/cli/output.ts +41 -1
- package/src/cli/recovery-package.ts +18 -1
- package/src/cli/role-holders.ts +35 -0
- package/src/cli/room.ts +304 -0
- package/src/cli/say-input.ts +16 -0
- package/src/cli/schedule-output.ts +49 -0
- package/src/cli/sdlc-output.ts +44 -6
- package/src/cli/smoke-pty-bridge.ts +33 -0
- package/src/cli/smoke-pty.ts +145 -0
- package/src/cli/smoke-run.ts +139 -0
- package/src/cli/smoke-target.ts +130 -0
- package/src/cli/source-output.ts +29 -0
- package/src/cli/task-output.ts +3 -2
- package/src/cli/upgrade-runtime.ts +98 -16
- package/src/cli/upgrade.ts +66 -7
- package/src/hub/ask.ts +76 -12
- package/src/hub/board.ts +132 -22
- package/src/hub/bus.ts +399 -56
- package/src/hub/child-process.ts +2 -0
- package/src/hub/ci-watch.ts +162 -0
- package/src/hub/claude-failure-stage.ts +85 -0
- package/src/hub/claude-launch-proof.ts +67 -2
- package/src/hub/claude-shell-wrapper.ts +11 -5
- package/src/hub/conductor.ts +17 -4
- package/src/hub/config-apply.ts +32 -5
- package/src/hub/config-reload.ts +7 -5
- package/src/hub/config-trust.ts +8 -1
- package/src/hub/context-window.ts +33 -3
- package/src/hub/control-client.ts +14 -2
- package/src/hub/corpus-config.ts +34 -0
- package/src/hub/corpus-evidence.ts +184 -0
- package/src/hub/corpus.ts +373 -0
- package/src/hub/cost-governance.ts +26 -0
- package/src/hub/daemon.ts +1228 -242
- package/src/hub/deliverable-files.ts +8 -2
- package/src/hub/delivery-journal.ts +123 -17
- package/src/hub/dogfood-profile.ts +32 -5
- package/src/hub/dogfood-read.ts +6 -2
- package/src/hub/envelope.ts +11 -0
- package/src/hub/eval-gate.ts +131 -0
- package/src/hub/events.ts +1 -1
- package/src/hub/external-instance.ts +69 -0
- package/src/hub/facts.ts +15 -5
- package/src/hub/folder-trigger.ts +223 -0
- package/src/hub/hub-tools.ts +2 -1
- package/src/hub/inference.ts +20 -5
- package/src/hub/instance-config.ts +69 -3
- package/src/hub/instance-execution.ts +6 -2
- package/src/hub/instances.ts +32 -4
- package/src/hub/intake.ts +125 -0
- package/src/hub/learning.ts +85 -0
- package/src/hub/lifecycle.ts +7 -3
- package/src/hub/limits.ts +12 -5
- package/src/hub/native-permission-request.ts +77 -0
- package/src/hub/operational-authority.ts +63 -5
- package/src/hub/operational-settings.ts +76 -0
- package/src/hub/operational-view.ts +1 -1
- package/src/hub/packs.ts +132 -0
- package/src/hub/path-policy.ts +7 -0
- package/src/hub/peer-doing.ts +130 -0
- package/src/hub/peers.ts +46 -0
- package/src/hub/queue-backpressure.ts +58 -0
- package/src/hub/read-tools.ts +2 -0
- package/src/hub/recovery-authority.ts +151 -0
- package/src/hub/recovery-policy.ts +220 -0
- package/src/hub/research.ts +24 -14
- package/src/hub/restart.ts +7 -0
- package/src/hub/routing.ts +54 -11
- package/src/hub/schedules.ts +266 -0
- package/src/hub/sdlc-merge.ts +38 -14
- package/src/hub/sdlc-metrics.ts +32 -10
- package/src/hub/sdlc-queue.ts +7 -1
- package/src/hub/sdlc-release.ts +1 -1
- package/src/hub/sdlc.ts +37 -7
- package/src/hub/security-baseline.ts +38 -0
- package/src/hub/settings.ts +7 -1
- package/src/hub/shipped-packs.ts +8 -0
- package/src/hub/snapshots.ts +72 -6
- package/src/hub/source-read.ts +160 -0
- package/src/hub/sources.ts +311 -0
- package/src/hub/task-shell-default.ts +3 -2
- package/src/hub/tasks.ts +1047 -115
- package/src/hub/workflow.ts +1 -1
- package/src/local/deny.ts +21 -0
- package/src/local/doc-read.ts +69 -0
- package/src/local/proxy.ts +4 -13
- package/src/local/sandbox.ts +3 -1
- package/src/local/tools.ts +37 -3
- package/src/local/web-fetch-config.ts +43 -0
- package/src/local/web-fetch.ts +183 -0
- package/src/memory/brief.ts +85 -13
- package/src/memory/capture.ts +1 -1
- package/src/rooms/application.ts +114 -0
- package/src/rooms/boundary.ts +103 -0
- package/src/rooms/commands.ts +294 -0
- package/src/rooms/crypto.ts +371 -0
- package/src/rooms/link.ts +584 -0
- package/src/rooms/policy.ts +82 -0
- package/src/rooms/remote-peer.ts +80 -0
- package/src/rooms/service.ts +426 -0
- package/src/rooms/status.ts +146 -0
- package/src/rooms/store.ts +1210 -0
- package/src/rooms/wire.ts +465 -0
- package/src/ui/index.html +91 -4
- package/templates/packs/example.pack.json +13 -0
package/src/cli/main.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { assertConductorOperationLocal, authorizeConductorOperation, authorizeConductorLaunch, issueConductorOperation, preflightConductorRecovery, retireConductorOperation } from "./conductor-recovery.ts";
|
|
3
|
+
import { conductorLocalOperation, conductorOperation, OPERATIONAL_PERSON_GUIDANCE } from "../hub/operational-authority.ts";
|
|
4
|
+
import { sayRecipients } from "./say-input.ts";
|
|
3
5
|
import type { DeliverableRow } from "../hub/tasks.ts";
|
|
4
6
|
import { kimiSourceRow } from "../hub/budget-notes.ts";
|
|
5
7
|
import { hubGitSync, hubGitEnv } from "../hub/git.ts";
|
|
@@ -20,12 +22,16 @@ import { supportsBoardReleaseFilter, supportsBoardColumnMetadata } from "./board
|
|
|
20
22
|
import { matchesPolicy } from "../hub/path-policy.ts";
|
|
21
23
|
import { projectedDiff } from "../hub/projected-diff.ts";
|
|
22
24
|
import { TextProjections } from "../hub/text-projection.ts";
|
|
25
|
+
import { fetchSourceUrl, MAX_SOURCE_BYTES, publicSourceView, screenRegistration, SourceRegistry, validSourceId, type SourceMeta } from "../hub/sources.ts";
|
|
26
|
+
import { renderSourceAdd, renderSourceList } from "./source-output.ts";
|
|
27
|
+
import { renderScheduleChange, renderScheduleList, renderScheduleRun } from "./schedule-output.ts";
|
|
28
|
+
import { renderIntake, renderIntakeChange } from "./intake-output.ts";
|
|
23
29
|
import { terminalText } from "./console-state.ts";
|
|
24
30
|
import { parseSettingsEditArgs, runSettingsEdit, SETTINGS_EDIT_USAGE } from "./settings-edit.ts";
|
|
25
31
|
import { petView, readPet, renderPet } from "./pet.ts";
|
|
26
32
|
import { startPetWindow, closePetWindow } from "../pet/window.ts";
|
|
27
33
|
import { loadPetConfig, savePetPalette } from "./pet-config.ts";
|
|
28
|
-
import {
|
|
34
|
+
import { projectProfile, resolveProfile } from "../hub/profiles.ts";
|
|
29
35
|
import { catalogAssignments, loadRoleCatalog, selectedRoles } from "../hub/roles.ts";
|
|
30
36
|
import { attachWatch } from "./watch.ts";
|
|
31
37
|
import { verifiedJcodeCommand } from "../adapters/jcode.ts";
|
|
@@ -34,9 +40,9 @@ import { qwenSessionId } from "../hub/crash.ts";
|
|
|
34
40
|
import { isPermissionMode } from "../hub/permission-mode.ts";
|
|
35
41
|
import { parseSettingText, settingDef, settingText, type SettingRow } from "../hub/settings.ts";
|
|
36
42
|
import { startModeOf } from "../hub/start-mode.ts";
|
|
37
|
-
import { OVERLAY_FILE, parseOverlay, parseRouting, routingText, type Routing, type RoutingOverlay } from "../hub/routing.ts";
|
|
43
|
+
import { currentRouting, OVERLAY_FILE, parseOverlay, parseRouting, routingText, type Routing, type RoutingOverlay } from "../hub/routing.ts";
|
|
38
44
|
import { spawn, spawnSync } from "node:child_process";
|
|
39
|
-
import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
45
|
+
import { chmodSync, closeSync, constants, existsSync, fstatSync, openSync, readFileSync, readSync, renameSync, writeFileSync } from "node:fs";
|
|
40
46
|
import { randomUUID } from "node:crypto";
|
|
41
47
|
import { homedir } from "node:os";
|
|
42
48
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
@@ -59,8 +65,8 @@ import { VERSION } from "../version.ts";
|
|
|
59
65
|
import { freeText } from "./free-text.ts";
|
|
60
66
|
import { createInterface } from "node:readline/promises";
|
|
61
67
|
import { createInterface as lineReader } from "node:readline";
|
|
62
|
-
import { activeOperation, assertLifecycleAvailable, operationIdPattern, readOperation, recoveryLock, recoveryRunner, signedRunner, stopSignedRunner, supportsAction } from "../hub/recovery-store.ts";
|
|
63
|
-
import { childEnv } from "../hub/child-process.ts";
|
|
68
|
+
import { activeOperation, assertLifecycleAvailable, operationIdPattern, readOperation, writeOperation, recoveryLock, recoveryRunner, signedRunner, stopSignedRunner, supportsAction } from "../hub/recovery-store.ts";
|
|
69
|
+
import { childEnv, personEnv } from "../hub/child-process.ts";
|
|
64
70
|
import { abortRecovery, createOperation, disposeRecovery, recordRecoveryDelegation, repairedFreshSession, reattachRecovery, liveProjects, nextChoices, nextActionsText, publicOperation, recoveryCommand, registeredProjects, runRecovery, type RecoveryOperation, type Inspection } from "./upgrade.ts";
|
|
65
71
|
import { endPlannedPeer, makeRecoveryDriver, makeUpgradePlan, preserveSource } from "./upgrade-runtime.ts";
|
|
66
72
|
import { exactVersion, latestRelease, newerVersion } from "./recovery-package.ts";
|
|
@@ -93,10 +99,10 @@ import { agentShellCause, classifyPeerCommand, cliCommandLabel, detectCliIdentit
|
|
|
93
99
|
import { recordCliAudit, recordDelegatedCliAudit } from "./identity-audit.ts";
|
|
94
100
|
import { runConsole } from "./console.ts";
|
|
95
101
|
import { paint, permissionBoundary, resolveColor, wrap, type Span } from "./console-state.ts";
|
|
96
|
-
import { outputWidth, renderRoster, renderStatus, renderBoard, renderBacklog, renderBoardColumns, boardColumnModel, renderBudget, renderDoctor, renderProjects, renderQueue, renderTurns, renderOrphans, renderQueueShow, renderModelsStatus, renderExecutionBudgetStatus, renderReport, renderUsage, renderLimits, renderTotal, type DoctorCheck } from "./output.ts";
|
|
102
|
+
import { outputWidth, renderRoster, renderStatus, renderBoard, renderBacklog, renderBoardColumns, boardColumnModel, renderBudget, renderDoctor, renderProjects, renderQueue, renderTurns, renderOrphans, renderOrphanCleanup, renderQueueShow, renderModelsStatus, renderExecutionBudgetStatus, renderReport, renderUsage, renderLimits, renderTotal, type DoctorCheck } from "./output.ts";
|
|
97
103
|
import { renderHelp, configuredAcpHelp } from "./help.ts";
|
|
98
104
|
import { renderBacklogResult, renderGroom, renderReleaseChange, renderReleaseList, renderReleaseNew, renderReleaseShow, renderTaskCancel, renderTaskShow } from "./task-output.ts";
|
|
99
|
-
import { renderAuthorization, renderSdlcCosts, renderSdlcExplain, renderSdlcMetrics, renderSdlcQueue, renderSdlcRelease, renderSdlcStatus } from "./sdlc-output.ts";
|
|
105
|
+
import { renderAuthorization, renderSdlcCosts, renderSdlcExplain, renderSdlcMerge, renderSdlcMetrics, renderSdlcQueue, renderSdlcRelease, renderSdlcStatus } from "./sdlc-output.ts";
|
|
100
106
|
import { renderTailEvent } from "./tail-render.ts";
|
|
101
107
|
import { archiveProblem, archiveState, damagedState, failureText, MANIFEST, planReset, resetLines, resetRuntime, startState, type ResetPlan } from "./reset.ts";
|
|
102
108
|
|
|
@@ -160,7 +166,7 @@ function localDeclarations(): { scope: Pick<InstanceRoleConfig, "instances" | "c
|
|
|
160
166
|
catch (error) { return { scope: { catalog: createProjectAdapterCatalog() }, error: startupDiagnostic(error) }; }
|
|
161
167
|
}
|
|
162
168
|
const projectCatalog = peerConfig.catalog;
|
|
163
|
-
const configuredCommand = !definitionOf(given) && projectCatalog.definitionOf(given)?.permission.genericAcp !== undefined;
|
|
169
|
+
const configuredCommand = !definitionOf(given) && (projectCatalog.definitionOf(given)?.permission.genericAcp !== undefined || peerConfig.declaredExternalInstances?.some(row => row.kind === given) === true);
|
|
164
170
|
const unboundCaller = detectCliIdentity(process.env);
|
|
165
171
|
const markerNeedsDeclaration = unboundCaller.role === "invalid" && unboundCaller.reason === "AGENTHUB_PEER_ID conflicts with native agent marker"
|
|
166
172
|
&& process.env.AGENTHUB_PEER_ID !== undefined && !definitionOf(process.env.AGENTHUB_PEER_ID);
|
|
@@ -170,7 +176,7 @@ let nativeSelection: ReturnType<typeof instanceLaunchArguments>;
|
|
|
170
176
|
try {
|
|
171
177
|
declaredInstances = needsInstances || configuredCommand ? peerConfig.instances : undefined;
|
|
172
178
|
if (configuredCommand && !(rest.length === 1 && ["--help", "-h"].includes(rest[0]!))) {
|
|
173
|
-
const selection = parseConfiguredAcpArguments(given, rest, projectCatalog, declaredInstances);
|
|
179
|
+
const selection = parseConfiguredAcpArguments(given, rest, projectCatalog, declaredInstances, peerConfig.declaredExternalInstances);
|
|
174
180
|
nativeSelection = { ...(selection.peer !== given ? { instance: selection.peer } : {}), args: [...rest] };
|
|
175
181
|
} else nativeSelection = instanceLaunchArguments(given, rest, declaredInstances);
|
|
176
182
|
} catch (error) { fail(startupDiagnostic(error)); }
|
|
@@ -207,11 +213,30 @@ const stateDir = selected.stateDir;
|
|
|
207
213
|
var operationalAuditReady=false, operationalRefusalRecorded=false;
|
|
208
214
|
const caller = declaredInstances ? detectCliIdentity(process.env, declaredInstances) : unboundCaller;
|
|
209
215
|
const commandLabel = cliCommandLabel(cmd, args, projectCatalog);
|
|
210
|
-
|
|
216
|
+
let commandAccess = classifyPeerCommand(cmd, args, projectCatalog);
|
|
217
|
+
const continuationPeer = process.env.AGENTHUB_RECOVERY_CONDUCTOR;
|
|
218
|
+
const continuationId = process.env.AGENTHUB_RECOVERY_OPERATION;
|
|
219
|
+
let recoveryLauncher = false, recoveryRunnerContinuation = false;
|
|
220
|
+
let recoveryLauncherCurrent: (() => void) | undefined;
|
|
221
|
+
if (continuationPeer !== undefined) {
|
|
222
|
+
if (caller.role !== "tools" || caller.peer !== continuationPeer || !continuationId || !operationIdPattern.test(continuationId) || liveTest) fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
223
|
+
let operation: RecoveryOperation;
|
|
224
|
+
try { operation = readOperation<RecoveryOperation>(continuationId); }
|
|
225
|
+
catch { fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal"); }
|
|
226
|
+
if (!operation || operation.id !== continuationId || typeof operation.sourceRoot !== "string") fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
227
|
+
if (cmd === "recovery-run" && args.length === 1 && args[0] === continuationId) {
|
|
228
|
+
await authorizeConductorOperation(operation, continuationPeer);
|
|
229
|
+
if (realPath(operation.sourceRoot) !== realPath(resolve(import.meta.dir, "../.."))) fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
230
|
+
recoveryRunnerContinuation = true; commandAccess = "conductor";
|
|
231
|
+
} else if (process.env.AGENTHUB_RECOVERY_CONTINUATION === continuationId) {
|
|
232
|
+
await authorizeConductorLaunch(operation, continuationPeer, cwd, resolve(import.meta.dir, "../.."), cmd, args); recoveryLauncher = true; recoveryLauncherCurrent = () => assertConductorOperationLocal(operation, continuationPeer);
|
|
233
|
+
} else fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
234
|
+
}
|
|
235
|
+
const conductorPeer = !liveTest && caller.role === "tools" && (recoveryRunnerContinuation || ["restart", "upgrade", "recovery"].includes(cmd) && commandAccess === "conductor") ? caller.peer : undefined;
|
|
211
236
|
// Agent-authored content keeps its peer. Operational starts/holds may borrow console authority for live verification.
|
|
212
237
|
const delegationRefused = liveTest && caller.role === "tools" && commandAccess !== "allowed" && liveTestRefusal(cmd, args);
|
|
213
238
|
const delegatedPeer = liveTest && caller.role === "tools" && commandAccess !== "allowed" && !delegationRefused ? caller.peer : undefined;
|
|
214
|
-
const identity = delegatedPeer ? { role: "console" as const } : caller;
|
|
239
|
+
const identity = delegatedPeer || recoveryLauncher ? { role: "console" as const } : caller;
|
|
215
240
|
operationalAuditReady=identity.role==="tools" && commandAccess==="conductor" && conductorOperation(cmd,args)!==undefined;
|
|
216
241
|
function audit(outcome: "run" | "refused" | "invalid"): void {
|
|
217
242
|
if (caller.role === "console") return;
|
|
@@ -576,13 +601,13 @@ function printRecoveryStatus(op: RecoveryOperation, message: string, presentatio
|
|
|
576
601
|
}
|
|
577
602
|
}
|
|
578
603
|
async function recoveryEndInspection(op: RecoveryOperation): Promise<Record<string, Inspection | undefined>> {
|
|
579
|
-
const driver = makeRecoveryDriver(undefined, delegatedPeer);
|
|
604
|
+
const driver = makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer);
|
|
580
605
|
return Object.fromEntries(await Promise.all(op.plan.projects.map(async planned => [planned.project.id, await driver.inspect(planned.project).catch(() => undefined)] as const)));
|
|
581
606
|
}
|
|
582
607
|
|
|
583
608
|
function spawnRecovery(operation: RecoveryOperation, quiet = false, presentation?: RecoveryPresentationOptions): void {
|
|
584
609
|
const child = spawn(process.execPath, [join(operation.sourceRoot, "src/cli/main.js"), ...liveTestArgs, "recovery-run", operation.id], {
|
|
585
|
-
cwd, detached: true, stdio: "ignore", env: { ...process.env, AGENTHUB_RECOVERY_OPERATION: operation.id },
|
|
610
|
+
cwd, detached: true, stdio: "ignore", env: { ...process.env, AGENTHUB_RECOVERY_OPERATION: operation.id, ...(conductorPeer ? { AGENTHUB_PEER_ID: conductorPeer, AGENTHUB_RECOVERY_CONDUCTOR: conductorPeer } : {}) },
|
|
586
611
|
});
|
|
587
612
|
child.on("error", () => console.error(`runner launch failed; use ${recoveryCommand(operation, "resume")}`));
|
|
588
613
|
child.unref();
|
|
@@ -657,7 +682,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
657
682
|
lock: recoveryLock,
|
|
658
683
|
read: (id) => readOperation<RecoveryOperation>(id),
|
|
659
684
|
runner: recoveryRunner,
|
|
660
|
-
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer).inspect),
|
|
685
|
+
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect),
|
|
661
686
|
endInspection: recoveryEndInspection,
|
|
662
687
|
endPeer: (planned, peer) => endPlannedPeer(planned, peer),
|
|
663
688
|
runnerStoppable: (id) => signedRunner(id) !== undefined,
|
|
@@ -684,6 +709,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
684
709
|
// Before the value reaches a package spec or the registry: a range such as 0.13 would resolve to whatever matches.
|
|
685
710
|
if (to !== undefined) { try { exactVersion(to); } catch (error) { fail((error as Error).message); } }
|
|
686
711
|
// An older CLI must not coordinate a newer target (docs/operations.md): the target's own coordinator takes over.
|
|
712
|
+
if (conductorPeer && kind === "upgrade" && newerVersion(to!, VERSION)) fail("a supporting target coordinator is required; the person runs it in ahub console or a terminal");
|
|
687
713
|
if (kind === "upgrade" && newerVersion(to!, VERSION) && process.env.AGENTHUB_UPGRADE_HANDOVER !== "1") {
|
|
688
714
|
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", ...liveTestArgs, "upgrade", "--to", to!, ...rest, ...display.flags, ...(dry && !rest.includes("--dry-run") ? ["--dry-run"] : [])];
|
|
689
715
|
console.error(`ahub: ${to} is newer than this CLI (${VERSION}), so its own coordinator runs the upgrade: bun ${argv.join(" ")}`);
|
|
@@ -703,7 +729,8 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
703
729
|
}
|
|
704
730
|
if (kind === "upgrade") console.error("ahub: Kimi now defaults to its own TUI (pull-only) in projects without peers.kimi.start_mode. To keep ACP: ahub settings set peers.kimi.start_mode headless");
|
|
705
731
|
if (interactive) return onTerminal((io) => planScreen(upgradeHost(kind, to ?? VERSION), io), display.options);
|
|
706
|
-
const plan = await makeUpgradePlan(kind, to ?? VERSION, kind === "restart" ? cwd : undefined);
|
|
732
|
+
const plan = await makeUpgradePlan(kind, to ?? VERSION, kind === "restart" ? cwd : undefined, undefined, conductorPeer);
|
|
733
|
+
if (conductorPeer) await preflightConductorRecovery(plan, conductorPeer);
|
|
707
734
|
const jsonApply = display.json && yes && !dry;
|
|
708
735
|
// #814: a person reading a pipe or a dry run gets the plan screen's rows, with every blocker, reconnect-only session and
|
|
709
736
|
// fresh start in them. `--json` keeps the document and the stderr lines (#206) exactly as they were.
|
|
@@ -715,7 +742,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
715
742
|
const blockers = [...plan.blockers, ...plan.projects.flatMap((p) => p.blockers.map((b) => `${p.project.id}: ${b}`))];
|
|
716
743
|
if (display.json) for (const blocker of blockers) console.error(`ahub: blocker: ${blocker}`);
|
|
717
744
|
if (dry) return;
|
|
718
|
-
if (blockers.length) { if (jsonApply) console.log(JSON.stringify(plan, null, 2)); fail("plan has blockers; no runtime was changed"); }
|
|
745
|
+
if (blockers.length) { if (jsonApply) console.log(JSON.stringify(plan, null, 2)); fail(conductorPeer ? "conductor recovery cannot prove this plan; no runtime was changed; the person runs it in ahub console or a terminal" : "plan has blockers; no runtime was changed"); }
|
|
719
746
|
assertLifecycleAvailable();
|
|
720
747
|
if (!yes) {
|
|
721
748
|
if (!process.stdin.isTTY) fail("review --dry-run and use --yes in non-interactive sessions");
|
|
@@ -724,10 +751,21 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
724
751
|
if (!/^(y|yes)$/i.test((await rl.question("Apply this project list and restore its sessions? [y/N] ")).trim())) return;
|
|
725
752
|
} finally { rl.close(); }
|
|
726
753
|
}
|
|
727
|
-
const current = await makeUpgradePlan(kind, plan.version, kind === "restart" ? cwd : undefined);
|
|
754
|
+
const current = await makeUpgradePlan(kind, plan.version, kind === "restart" ? cwd : undefined, undefined, conductorPeer);
|
|
728
755
|
if (current.fingerprint !== plan.fingerprint) fail("plan changed during review; run the command again");
|
|
756
|
+
const routingDefaults = conductorPeer ? await preflightConductorRecovery(plan, conductorPeer) : undefined;
|
|
729
757
|
const sourceRoot = preserveSource(plan);
|
|
730
|
-
|
|
758
|
+
let operation: RecoveryOperation;
|
|
759
|
+
if (conductorPeer) {
|
|
760
|
+
const id = randomUUID();
|
|
761
|
+
const pending: RecoveryOperation = { schema: 1, id, plan, sourceRoot, createdAt: Date.now(), updatedAt: Date.now(), phase: "pending", step: "stage", ...(routingDefaults ? { conductorRoutingDefaults: routingDefaults } : {}), projects: plan.projects.map(row => ({ id: row.project.id, phase: "pending", terminals: {} })) };
|
|
762
|
+
const target = await makeRecoveryDriver().stage(pending);
|
|
763
|
+
pending.targetRoot = target.root; pending.targetDigest = target.digest; pending.targetFeatures = target.targetFeatures;
|
|
764
|
+
await issueConductorOperation(pending, conductorPeer);
|
|
765
|
+
operation = createOperation(plan, sourceRoot, undefined, { peer: conductorPeer, command: kind }, id);
|
|
766
|
+
operation.targetRoot = target.root; operation.targetDigest = target.digest; operation.targetFeatures = target.targetFeatures; operation.conductorRoutingDefaults = routingDefaults;
|
|
767
|
+
writeOperation(id, operation);
|
|
768
|
+
} else operation = createOperation(plan, sourceRoot, undefined, delegatedPeer ? { peer: delegatedPeer, command: kind } : undefined);
|
|
731
769
|
if (display.json) { spawnRecovery(operation, true); console.log(JSON.stringify(publicOperation(operation), null, 2)); }
|
|
732
770
|
else spawnRecovery(operation, false, humanRecovery(false) ? display.options : undefined);
|
|
733
771
|
}
|
|
@@ -741,6 +779,40 @@ function parseRecoveryReattach(argv: readonly string[]): { id: string; peer: Ter
|
|
|
741
779
|
return { id: argv[1]!, peer: argv[2] as TerminalPeer };
|
|
742
780
|
}
|
|
743
781
|
|
|
782
|
+
/** #248: `ahub bench run` arguments, checked before anything runs (#933: also before the conductor's admission writes its audit row). */
|
|
783
|
+
function benchRunArgs(rest: string[]) {
|
|
784
|
+
const { one, many, rest: positional } = takeFlags(rest, ["--arm", "--repeat", "--tasks", "--memory"], ["--addon"]);
|
|
785
|
+
if (one["--memory"] !== undefined && !(MEMORY_MODES as readonly string[]).includes(one["--memory"])) fail(`--memory takes ${MEMORY_MODES.join(", ")}`);
|
|
786
|
+
// #248: `--addon ripwire=off` declares the arm's add-on state; the run refuses peers that do not match it.
|
|
787
|
+
const addons: Partial<Record<AddonName, "on" | "off">> = {};
|
|
788
|
+
for (const spec of many["--addon"] ?? []) {
|
|
789
|
+
const [name, state] = spec.split("=");
|
|
790
|
+
if (!name || !isAddon(name) || (state !== "on" && state !== "off") || addons[name]) fail(`--addon takes <name>=on|off once per add-on, name one of ${ADDONS.join(", ")}`);
|
|
791
|
+
addons[name] = state;
|
|
792
|
+
}
|
|
793
|
+
const suite = positional[0];
|
|
794
|
+
const repeat = one["--repeat"] === undefined ? 1 : Number(one["--repeat"]);
|
|
795
|
+
if (!suite || positional.length !== 1 || !one["--arm"]?.trim() || !Number.isSafeInteger(repeat) || repeat < 1 || repeat > 100) {
|
|
796
|
+
fail("usage: ahub bench run <suite.json> --arm <label> [--repeat 1..100] [--tasks a,b] [--addon <name>=on|off]... [--memory off|claude-mem|native|both]");
|
|
797
|
+
}
|
|
798
|
+
return { one, addons, suite, repeat };
|
|
799
|
+
}
|
|
800
|
+
/** Another project is `ahub --project <dir> research`; anything unknown is refused rather than ignored. */
|
|
801
|
+
function researchArgs(): void {
|
|
802
|
+
const sub = args[0], flags = new Set(["--all", "--json", "--since", "--format"]), valued = new Set(["--since", "--format"]);
|
|
803
|
+
for (let i = sub === "export" || sub === "backfill" || sub === "addons" ? 1 : 0; i < args.length; i++) {
|
|
804
|
+
if (!flags.has(args[i]!)) fail(`ahub research: unknown argument ${args[i]} (another project: ahub --project <dir> research)`);
|
|
805
|
+
if (valued.has(args[i]!)) i++;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const SOURCE_USAGE = "usage: ahub source add <path|url> [--json] [--color=auto|always|never] | list [--json] [--color=auto|always|never]";
|
|
809
|
+
/** #933: a local conductor form's argument check, run before the `admit` handshake so a usage error leaves no admitted row. */
|
|
810
|
+
const LOCAL_OPERATION_ARGS: Record<string, () => void> = {
|
|
811
|
+
"bench run": () => { benchRunArgs(args.filter(arg => arg !== "--json").slice(1)); },
|
|
812
|
+
"research backfill": researchArgs,
|
|
813
|
+
"source add": () => { const operands = args.filter(arg => arg !== "--json" && arg !== "--full" && !arg.startsWith("--color=")); if (operands[0] !== "add" || operands.length !== 2 || !operands[1]) fail(SOURCE_USAGE); },
|
|
814
|
+
};
|
|
815
|
+
|
|
744
816
|
const commands: Record<string, () => Promise<void> | void> = {
|
|
745
817
|
upgrade: () => upgrade("upgrade"),
|
|
746
818
|
restart: () => upgrade("restart"),
|
|
@@ -778,7 +850,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
778
850
|
const result = await reattachRecovery(selected.id, selected.peer, makeRecoveryDriver());
|
|
779
851
|
if (display.json) { console.log(JSON.stringify(publicOperation(result), null, 2)); return; }
|
|
780
852
|
printRecovery(renderOperation(result, undefined, display.options));
|
|
781
|
-
const live = await liveProjects(result, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
853
|
+
const live = await liveProjects(result, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect);
|
|
782
854
|
printRecovery(renderBlocked(result, nextChoices(result, undefined, live), display.options));
|
|
783
855
|
return;
|
|
784
856
|
}
|
|
@@ -805,15 +877,20 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
805
877
|
audit("refused"); fail(LIVE_TEST_PERSON_REFUSAL);
|
|
806
878
|
}
|
|
807
879
|
}
|
|
880
|
+
if (conductorPeer) {
|
|
881
|
+
if (operation.id !== id) fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
882
|
+
await authorizeConductorOperation(operation, conductorPeer);
|
|
883
|
+
}
|
|
808
884
|
const runner = recoveryRunner(id);
|
|
809
885
|
if (action === "status") {
|
|
810
886
|
// #215: abort and resume are offered by what the open sources show, read as the runner's errors read them.
|
|
811
|
-
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
887
|
+
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect);
|
|
812
888
|
if (display.json) console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
813
889
|
else printRecovery(renderRecoveryStatus(operation, runner, nextChoices(operation, runner, live), display.options));
|
|
814
890
|
}
|
|
815
891
|
else if (action === "abort") {
|
|
816
|
-
await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer));
|
|
892
|
+
await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer));
|
|
893
|
+
if (conductorPeer) retireConductorOperation(readOperation<RecoveryOperation>(id), conductorPeer);
|
|
817
894
|
if (display.json) console.log(JSON.stringify(publicOperation(readOperation<RecoveryOperation>(id)), null, 2));
|
|
818
895
|
else console.log("preflight cancelled; no committed transition was rolled back");
|
|
819
896
|
}
|
|
@@ -823,7 +900,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
823
900
|
const stop = flags.length === 1 && flags[0] === "--stop-and-archive";
|
|
824
901
|
const reason = one["--reason"]?.trim() ?? "";
|
|
825
902
|
if (!reason || reason.length > 500 || (flags.length && !stop) || stop === !!one["--fresh-session"]) fail(usage);
|
|
826
|
-
const result = await disposeRecovery(id, stop ? { stop: true } : { fresh: one["--fresh-session"]! }, reason, makeRecoveryDriver(undefined, delegatedPeer));
|
|
903
|
+
const result = await disposeRecovery(id, stop ? { stop: true } : { fresh: one["--fresh-session"]! }, reason, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer));
|
|
827
904
|
if (!stop) {
|
|
828
905
|
if (display.json) { console.log(JSON.stringify(publicOperation(result), null, 2)); return spawnRecovery(result, true); }
|
|
829
906
|
console.log(`${one["--fresh-session"]}: a fresh session is accepted and the lost one is recorded; resuming`);
|
|
@@ -834,7 +911,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
834
911
|
}
|
|
835
912
|
else if (["completed", "cancelled"].includes(operation.phase)) console.log(display.json ? JSON.stringify(publicOperation(operation, runner), null, 2) : `recovery is already ${operation.phase}`);
|
|
836
913
|
// #215: an operation being abandoned is never resumed, whichever coordinator started it (an older one would).
|
|
837
|
-
else if (operation.disposition) fail(`a stop-and-archive of this operation is partway; nothing was resumed; ${nextActionsText(operation, await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect))}`);
|
|
914
|
+
else if (operation.disposition) fail(`a stop-and-archive of this operation is partway; nothing was resumed; ${nextActionsText(operation, await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect))}`);
|
|
838
915
|
else if (runner === "unknown") {
|
|
839
916
|
if (display.json) console.log(JSON.stringify(publicOperation(operation, runner), null, 2));
|
|
840
917
|
else if (humanRecovery(false)) printRecoveryStatus(operation, "Runner ownership is unknown; nothing was started.", display.options);
|
|
@@ -853,8 +930,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
853
930
|
},
|
|
854
931
|
"recovery-run": async () => {
|
|
855
932
|
if (args.length !== 1 || process.env.AGENTHUB_RECOVERY_OPERATION !== args[0]) fail("recovery-run is an internal command");
|
|
856
|
-
const result = await runRecovery(args[0]!, makeRecoveryDriver(undefined, delegatedPeer));
|
|
933
|
+
const result = await runRecovery(args[0]!, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer));
|
|
857
934
|
if (result.phase !== "completed") process.exitCode = 1;
|
|
935
|
+
else if (conductorPeer) retireConductorOperation(result, conductorPeer);
|
|
858
936
|
},
|
|
859
937
|
help: () => {
|
|
860
938
|
const color = resolveColor(undefined, { isTTY: !!process.stdout.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR }) === true;
|
|
@@ -923,10 +1001,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
923
1001
|
},
|
|
924
1002
|
|
|
925
1003
|
setup: async () => {
|
|
1004
|
+
recoveryLauncherCurrent?.();
|
|
926
1005
|
assertLifecycleAvailable();
|
|
927
1006
|
const root = join(import.meta.dir, "..", "..");
|
|
928
1007
|
const state = () => {
|
|
929
|
-
const out = (argv: string[]) => spawnSync("claude", argv, { encoding: "utf8" }).stdout ?? "";
|
|
1008
|
+
const out = (argv: string[]) => { recoveryLauncherCurrent?.(); const result = spawnSync("claude", argv, { encoding: "utf8", ...(recoveryLauncherCurrent ? { timeout: 30_000 } : {}) }); recoveryLauncherCurrent?.(); return result.stdout ?? ""; };
|
|
930
1009
|
return nextStep(parseList<InstalledPlugin>(out(["plugin", "list", "--json"])), parseList<Marketplace>(out(["plugin", "marketplace", "list", "--json"])), root);
|
|
931
1010
|
};
|
|
932
1011
|
let step = state();
|
|
@@ -943,17 +1022,21 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
943
1022
|
}
|
|
944
1023
|
for (let i = 0; step && i < 6; i++, step = state()) {
|
|
945
1024
|
console.log(`> ${step.argv.join(" ")} (${step.why})`);
|
|
946
|
-
|
|
1025
|
+
recoveryLauncherCurrent?.();
|
|
1026
|
+
if (spawnSync(step.argv[0]!, step.argv.slice(1), { stdio: "inherit", ...(recoveryLauncherCurrent ? { timeout: 30_000 } : {}) }).status !== 0) fail(`"${step.argv.join(" ")}" failed; nothing after it was run`);
|
|
1027
|
+
recoveryLauncherCurrent?.();
|
|
947
1028
|
}
|
|
948
1029
|
if (step) fail(`still not done after 6 steps; next would be: ${step.argv.join(" ")}`);
|
|
949
1030
|
}
|
|
950
|
-
|
|
1031
|
+
if (recoveryLauncherCurrent) recoveryLauncherCurrent();
|
|
1032
|
+
else await commands.doctor!();
|
|
951
1033
|
},
|
|
952
1034
|
|
|
953
1035
|
init: () => {
|
|
954
1036
|
const flags = takeFlags(args.filter(arg => arg !== "--dry-run" && arg !== "--json" && arg !== "--shadow" && arg !== "--release-guard"), ["--profile"], []);
|
|
955
|
-
if (flags.rest.length) fail("usage: ahub init [--profile dev|research|writing|general] [--shadow] [--release-guard] [--dry-run --json]");
|
|
956
|
-
|
|
1037
|
+
if (flags.rest.length) fail("usage: ahub init [--profile dev|research|writing|general|<pack>] [--shadow] [--release-guard] [--dry-run --json]");
|
|
1038
|
+
// A pack is a project file in .agenthub/roles/<name>.pack.json (#513a); init resolves the name and refuses an unknown one with the profiles and packs it knows.
|
|
1039
|
+
const requestedProfile = flags.one["--profile"];
|
|
957
1040
|
if (args.includes("--dry-run")) {
|
|
958
1041
|
const plan = planInit(cwd, requestedProfile, args.includes("--shadow"), args.includes("--release-guard"));
|
|
959
1042
|
return console.log(args.includes("--json") ? JSON.stringify(plan, null, 2) : plan.map(change => `${change.action} ${change.path}: ${change.reason}${change.managedBlock ? ` (managed block: ${change.managedBlock})` : ""}`).join("\n") || "already up to date");
|
|
@@ -983,8 +1066,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
983
1066
|
up: async () => {
|
|
984
1067
|
const unattended = unattendedEnv || args.includes("--unattended");
|
|
985
1068
|
if (unattended) console.error(UNATTENDED_WARNING);
|
|
986
|
-
const status = await startProject(registeredProject(), { unattended, liveTest });
|
|
1069
|
+
const status = await startProject(registeredProject(), { unattended, liveTest, ...(recoveryLauncherCurrent ? { current: recoveryLauncherCurrent, env: { ...personEnv(process.env), AGENTHUB_RECOVERY_OPERATION: continuationId, AGENTHUB_PEER_ID: continuationPeer, AGENTHUB_RECOVERY_CONDUCTOR: continuationPeer, AGENTHUB_RECOVERY_CONTINUATION: continuationId } } : {}) });
|
|
987
1070
|
console.log(`ahub up (ws://127.0.0.1:${status.controlPort}), state in ${stateDir}`);
|
|
1071
|
+
if (recoveryLauncherCurrent) { recoveryLauncherCurrent(); return; }
|
|
988
1072
|
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
989
1073
|
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
990
1074
|
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir, liveTest });
|
|
@@ -1048,9 +1132,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1048
1132
|
},
|
|
1049
1133
|
|
|
1050
1134
|
say: async () => {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1135
|
+
let addressed: ReturnType<typeof sayRecipients>;
|
|
1136
|
+
try { addressed = sayRecipients(args); } catch (error) { return fail((error as Error).message); }
|
|
1137
|
+
const { to, text } = addressed;
|
|
1138
|
+
const body = text.length ? freeText(text, "ahub say [@peer ...] <text>") : "";
|
|
1054
1139
|
const hub = await connect();
|
|
1055
1140
|
const res = await hub.request({ t: "send", body, to });
|
|
1056
1141
|
hub.close();
|
|
@@ -1249,8 +1334,144 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1249
1334
|
return shown(renderReleaseChange)(taskOp("release_change", { name, revision: Number(revision), fields }));
|
|
1250
1335
|
},
|
|
1251
1336
|
|
|
1337
|
+
smoke: async () => {
|
|
1338
|
+
// Log every live-test invocation, including syntax/target refusals; native output never joins the audit.
|
|
1339
|
+
if (liveTest && caller.role === "console") {
|
|
1340
|
+
try { recordCliAudit(stateDir, "user", commandLabel, "run"); }
|
|
1341
|
+
catch { return fail("smoke audit unavailable; nothing was executed"); }
|
|
1342
|
+
}
|
|
1343
|
+
const options = outputOptions();
|
|
1344
|
+
const operands = args.filter(arg => arg !== "--json");
|
|
1345
|
+
const usage = "usage: ahub --live-test smoke run qwen-dual-output --project-id <id> --receipt <file.json> [--timeout-ms <100..120000>] [--json]";
|
|
1346
|
+
const values = new Map<string, string>();
|
|
1347
|
+
if (operands[0] !== "run" || operands[1] !== "qwen-dual-output") return fail(usage);
|
|
1348
|
+
for (let n = 2; n < operands.length; n += 2) {
|
|
1349
|
+
const key = operands[n], value = operands[n + 1];
|
|
1350
|
+
if (!key || !["--project-id", "--receipt", "--timeout-ms"].includes(key) || !value || value.startsWith("--") || values.has(key)) return fail(usage);
|
|
1351
|
+
values.set(key, value);
|
|
1352
|
+
}
|
|
1353
|
+
if (!values.has("--project-id") || !values.has("--receipt")) return fail(usage);
|
|
1354
|
+
const home = process.env.AGENTHUB_HOME;
|
|
1355
|
+
if (!liveTest || !home) return fail("smoke target refused");
|
|
1356
|
+
const { validateSmokeTarget } = await import("./smoke-target.ts");
|
|
1357
|
+
const { runSmoke } = await import("./smoke-run.ts");
|
|
1358
|
+
const project = registeredProjects(home).find(project => project.root === cwd && project.stateDir === stateDir);
|
|
1359
|
+
if (!project) return fail("smoke target refused");
|
|
1360
|
+
// Keep path spelling for the guard: project selection above canonicalizes symlinks for ordinary CLI commands.
|
|
1361
|
+
const smokeCwd = selector && selector !== project.id ? resolve(invokedFrom, selector) : cwd;
|
|
1362
|
+
const checked = validateSmokeTarget({ cwd: smokeCwd, hubHome: home, project, expectedProjectId: values.get("--project-id")!, liveTest });
|
|
1363
|
+
const target = { ...checked, check: () => { checked.check(); assertDelegatedAuditConsumer(); } };
|
|
1364
|
+
const abort = new AbortController();
|
|
1365
|
+
const interrupted = () => abort.abort();
|
|
1366
|
+
process.on("SIGINT", interrupted); process.on("SIGTERM", interrupted);
|
|
1367
|
+
try {
|
|
1368
|
+
const result = await runSmoke(target, { leg: "qwen-dual-output", receipt: values.get("--receipt")!, timeoutMs: Number(values.get("--timeout-ms") ?? "60000"), signal: abort.signal });
|
|
1369
|
+
process.exitCode = result.outcome === "passed" ? 0 : 1;
|
|
1370
|
+
if (options.json) return console.log(JSON.stringify(result));
|
|
1371
|
+
console.log(`smoke ${result.outcome} (${result.reason}): ${result.counts.submits} submits, ${result.counts.stops} stops; cleanup ${result.checks.cleaned ? "confirmed" : "unconfirmed"}`);
|
|
1372
|
+
} finally { process.removeListener("SIGINT", interrupted); process.removeListener("SIGTERM", interrupted); }
|
|
1373
|
+
},
|
|
1374
|
+
|
|
1375
|
+
room: async () => {
|
|
1376
|
+
if (liveTest && !["list", "status"].includes(args[0] ?? "")) fail(LIVE_TEST_PERSON_REFUSAL);
|
|
1377
|
+
const { runRoomCommand, roomCommandError } = await import("./room.ts");
|
|
1378
|
+
const hub = await connect();
|
|
1379
|
+
try { await runRoomCommand(args, message => hub.request(message)); }
|
|
1380
|
+
catch (error) { fail(roomCommandError(error)); }
|
|
1381
|
+
finally { hub.close(); }
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1384
|
+
broker: async () => {
|
|
1385
|
+
const { runBrokerCommand, brokerCommandError } = await import("./broker.ts");
|
|
1386
|
+
try { await runBrokerCommand(args, stateDir); }
|
|
1387
|
+
catch (error) { fail(brokerCommandError(error)); }
|
|
1388
|
+
},
|
|
1389
|
+
|
|
1390
|
+
source: async () => {
|
|
1391
|
+
// #508: the source registry. `add` is the person's or the configured conductor's (#933: admitted and audited by the daemon before this runs) and local:
|
|
1392
|
+
// it reads and writes the hub state dir directly and needs no daemon. A URL is fetched here, once, by this process; no hub tool or agent ever fetches one.
|
|
1393
|
+
const options = outputOptions();
|
|
1394
|
+
const operands = args.filter(arg => arg !== "--json" && arg !== "--full" && !arg.startsWith("--color="));
|
|
1395
|
+
const [sub, ...rest] = operands;
|
|
1396
|
+
const registry = new SourceRegistry(stateDir);
|
|
1397
|
+
if (sub === "list" && rest.length === 0) {
|
|
1398
|
+
const rows = registry.list();
|
|
1399
|
+
if (options.json) return console.log(JSON.stringify(rows, null, 2));
|
|
1400
|
+
return printOutput(renderSourceList(rows, options.columns, Date.now()), options.color);
|
|
1401
|
+
}
|
|
1402
|
+
if (sub !== "add" || rest.length !== 1 || !rest[0]) return fail(SOURCE_USAGE);
|
|
1403
|
+
const target = rest[0], config = projectConfig();
|
|
1404
|
+
const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(target);
|
|
1405
|
+
let bytes: Buffer, origin: string, kind: "file" | "url", absolute: string | undefined;
|
|
1406
|
+
try {
|
|
1407
|
+
if (isUrl) {
|
|
1408
|
+
const fetched = await fetchSourceUrl(target);
|
|
1409
|
+
bytes = fetched.bytes; origin = fetched.finalUrl; kind = "url";
|
|
1410
|
+
} else {
|
|
1411
|
+
absolute = realPath(resolve(cwd, target));
|
|
1412
|
+
const fd = openSync(absolute, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1413
|
+
try {
|
|
1414
|
+
const st = fstatSync(fd);
|
|
1415
|
+
if (!st.isFile()) throw new Error("not a regular file");
|
|
1416
|
+
if (st.size > MAX_SOURCE_BYTES) throw new Error(`file is ${st.size} bytes, over the ${MAX_SOURCE_BYTES} byte cap (MAX_PROJECTION_INPUT); nothing was stored`);
|
|
1417
|
+
bytes = Buffer.alloc(st.size); let got = 0;
|
|
1418
|
+
while (got < bytes.length) { const n = readSync(fd, bytes, got, bytes.length - got, got); if (!n) break; got += n; }
|
|
1419
|
+
if (got !== st.size) throw new Error("the file changed while it was read");
|
|
1420
|
+
} finally { closeSync(fd); }
|
|
1421
|
+
origin = absolute; kind = "file";
|
|
1422
|
+
}
|
|
1423
|
+
} catch (error) { return fail(`cannot register ${isUrl ? "that URL" : target}: ${(error as Error).message}`); }
|
|
1424
|
+
const projections = new TextProjections({ cwd, stateDir, config: config.text_projection ?? {}, deny: config.local.deny, readAllow: config.local.read_allow, log: line => console.error(line) });
|
|
1425
|
+
const extension = /\.[A-Za-z0-9]{1,16}$/.exec(isUrl ? new URL(origin).pathname : origin)?.[0] ?? "";
|
|
1426
|
+
const verdict = screenRegistration({ bytes, kind, ...(absolute ? { path: absolute, typedPath: resolve(cwd, target) } : {}) }, {
|
|
1427
|
+
piiPaths: config.pii?.paths ?? [], piiPatterns: currentRouting(cwd).signals.pii_patterns, cwd, matchesPolicy,
|
|
1428
|
+
project: data => extension ? projections.projectBytes(join(cwd, `source${extension.toLowerCase()}`), data)?.text : undefined,
|
|
1429
|
+
});
|
|
1430
|
+
let added;
|
|
1431
|
+
try { added = registry.add({ bytes, kind, origin, private: verdict.private, ...(verdict.text !== undefined ? { text: verdict.text } : {}) }); }
|
|
1432
|
+
catch (error) { return fail(`cannot register ${isUrl ? "that URL" : target}: ${(error as Error).message}`); }
|
|
1433
|
+
const meta = registry.get(added.id);
|
|
1434
|
+
if (!meta) return fail(`the record for ${added.id} cannot be read after it was registered; inspect ${join(stateDir, "sources")}`);
|
|
1435
|
+
const result = { id: added.id, created: added.created, upgraded: added.upgraded, kind: meta.kind, origin: meta.origin, sha256: meta.sha256, bytes: meta.bytes, private: meta.private, text: added.text, reasons: verdict.reasons };
|
|
1436
|
+
if (options.json) return console.log(JSON.stringify(result, null, 2));
|
|
1437
|
+
printOutput(renderSourceAdd(result, options.columns), options.color);
|
|
1438
|
+
},
|
|
1439
|
+
|
|
1440
|
+
schedule: async () => {
|
|
1441
|
+
// #335 (#512 AC3 and AC4): the person's schedule commands; #933: run, pause and resume are the configured conductor's too, admitted and audited by the daemon.
|
|
1442
|
+
// The hub keeps the schedules, their periods and the pause; this process only asks.
|
|
1443
|
+
const split = splitPresentation(args);
|
|
1444
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1445
|
+
const [sub, id, ...extra] = split.operands;
|
|
1446
|
+
const usage = "usage: ahub schedule list | run <id> | pause <id> | resume <id> [--json] [--color=auto|always|never]";
|
|
1447
|
+
if (sub === "list" && id === undefined) return showToolText(data => renderScheduleList(data, present.columns, Date.now()), await taskOp("schedule_list", {}), present);
|
|
1448
|
+
if (!["run", "pause", "resume"].includes(sub ?? "") || !id || extra.length) return fail(usage);
|
|
1449
|
+
if (sub === "run") return showToolText(data => renderScheduleRun(data, present.columns), await taskOp("schedule_run", { id }, false, undefined, 90_000), present);
|
|
1450
|
+
return showToolText(data => renderScheduleChange(sub as "pause" | "resume", data, present.columns, Date.now()), await taskOp(sub === "pause" ? "schedule_pause" : "schedule_resume", { id }), present);
|
|
1451
|
+
},
|
|
1452
|
+
|
|
1453
|
+
intake: async () => {
|
|
1454
|
+
// #334 (#511a): natural-language intake. Person only, except approve (#933: the configured conductor too, admitted and audited by the daemon).
|
|
1455
|
+
// The request is a draft backlog card that nothing offers until `approve`; the hub, not this process, splits it.
|
|
1456
|
+
const split = splitPresentation(args, { valueFlags: ["--source"] });
|
|
1457
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1458
|
+
const usage = "usage: ahub intake \"<text>\" [--source <id>...] | approve <id> | discard <id> [--json] [--color=auto|always|never]";
|
|
1459
|
+
const sources: string[] = [], words: string[] = [];
|
|
1460
|
+
for (let i = 0; i < split.operands.length; i++) {
|
|
1461
|
+
const arg = split.operands[i]!;
|
|
1462
|
+
if (arg === "--source") { const id = split.operands[++i]; if (!id) return fail("--source needs a source id"); sources.push(id); } else words.push(arg);
|
|
1463
|
+
}
|
|
1464
|
+
const [sub, id] = words;
|
|
1465
|
+
if ((sub === "approve" || sub === "discard") && words.length === 2 && !sources.length) {
|
|
1466
|
+
if (!/^[1-9]\d*$/.test(id!)) return fail(usage);
|
|
1467
|
+
return showToolText(data => renderIntakeChange(sub, data, present.columns), await taskOp(sub === "approve" ? "intake_approve" : "intake_discard", { id: Number(id) }), present);
|
|
1468
|
+
}
|
|
1469
|
+
if (words.length !== 1 || !words[0]!.trim()) return fail(usage);
|
|
1470
|
+
return showToolText(data => renderIntake(data, present.columns), await taskOp("intake", { text: words[0], ...(sources.length ? { sources } : {}) }, false, undefined, 90_000), present);
|
|
1471
|
+
},
|
|
1472
|
+
|
|
1252
1473
|
deliverables: async () => {
|
|
1253
|
-
// #317: what each task delivered and what a reviewer approved, by id, hash and size (a path only where the PII screen allows it).
|
|
1474
|
+
// #317: what each task delivered and what a reviewer approved, by id, hash and size (a path only where the PII screen allows it). #933: an agent gets only the rows hub_task_show would show it.
|
|
1254
1475
|
const options = outputOptions();
|
|
1255
1476
|
const { one, rest } = takeFlags(args, ["--task"], []);
|
|
1256
1477
|
const approved = rest.includes("--approved"), check = rest.includes("--check");
|
|
@@ -1415,14 +1636,19 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1415
1636
|
}
|
|
1416
1637
|
if (sub === "merge") {
|
|
1417
1638
|
// #786 T1: the guarded merge of one approved task; a person or the configured conductor asks, the daemon re-reads every gate.
|
|
1418
|
-
|
|
1639
|
+
// #899: the decision prints for a person; `--json` is the same view as a document. The presentation flags are read before the
|
|
1640
|
+
// request, so an invalid --color refuses before a mutation is sent, and they never reach the hub.
|
|
1641
|
+
const split = splitPresentation(args.slice(1)), display = outputOptions(split.flags);
|
|
1642
|
+
const task = Number(split.operands[0]), flags = takeFlags(split.operands.slice(1), [], []);
|
|
1419
1643
|
const dryRun = flags.rest.includes("--dry-run");
|
|
1420
|
-
if (flags.rest.some(arg => arg !== "--dry-run") || !Number.isSafeInteger(
|
|
1644
|
+
if (flags.rest.some(arg => arg !== "--dry-run") || !Number.isSafeInteger(task) || task <= 0) fail("usage: ahub sdlc merge <task> [--dry-run] [--json]");
|
|
1421
1645
|
const hub = await connect();
|
|
1422
1646
|
try {
|
|
1423
|
-
const reply = await hub.request({ t: "sdlc", op: "merge", id, ...(dryRun ? { dryRun: true } : {}) }, 60_000);
|
|
1647
|
+
const reply = await hub.request({ t: "sdlc", op: "merge", id: task, ...(dryRun ? { dryRun: true } : {}) }, 60_000);
|
|
1424
1648
|
if (!reply.ok) fail(reply.error);
|
|
1425
|
-
|
|
1649
|
+
const view = { task: reply.task, mode: reply.mode, ran: reply.ran, decision: reply.decision, ...(reply.argv ? { argv: reply.argv } : {}), ...(reply.refused ? { refused: reply.refused } : {}), ...(reply.result ? { result: reply.result } : {}) };
|
|
1650
|
+
if (display.json) console.log(JSON.stringify(view, null, 2));
|
|
1651
|
+
else printOutput(renderSdlcMerge(view, display.columns, Date.now()), display.color);
|
|
1426
1652
|
if (reply.decision === "refused" || reply.ran && reply.result?.state !== "merged") process.exitCode = 1;
|
|
1427
1653
|
} finally { hub.close(); }
|
|
1428
1654
|
return;
|
|
@@ -1481,6 +1707,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1481
1707
|
if (rest.length !== 1) fail("usage: ahub task show <id> [--json] [--full] [--color=auto|always|never]");
|
|
1482
1708
|
return shownTask(renderTaskShow)(taskOp("task_show", { id: rest[0] }));
|
|
1483
1709
|
}
|
|
1710
|
+
if (sub === "claim") {
|
|
1711
|
+
// #901: a person's verdict on one declared plan claim. The claim is named by its 1-based slot and its text is never typed, stored or printed here.
|
|
1712
|
+
const { one, rest: positional } = takeFlags(rest, ["--model", "--model-said"], []);
|
|
1713
|
+
if (positional.length !== 3 || !/^[1-9]\d*$/.test(positional[0]!) || !/^[1-9]\d*$/.test(positional[1]!) || !["supported", "unsupported", "unclear"].includes(positional[2]!)) fail("usage: ahub task claim <id> <slot> <supported|unsupported|unclear> [--model <label>] [--model-said <supported|unsupported|unclear>]");
|
|
1714
|
+
const reply = JSON.parse(await taskOp("claim_verdict", { id: Number(positional[0]), slot: Number(positional[1]), verdict: positional[2], ...(one["--model"] === undefined ? {} : { model: one["--model"] }), ...(one["--model-said"] === undefined ? {} : { modelVerdict: one["--model-said"] }) })) as { task: number; recorded?: { slot: number; verdict: string; model?: string; modelVerdict?: string } };
|
|
1715
|
+
return console.log(`task #${reply.task}: claim ${reply.recorded?.slot} recorded as ${reply.recorded?.verdict}${reply.recorded?.model ? ` (model ${reply.recorded.model}` : ""}${reply.recorded?.modelVerdict ? `${reply.recorded?.model ? ", " : " ("}model said ${reply.recorded.modelVerdict}` : ""}${reply.recorded?.model || reply.recorded?.modelVerdict ? ")" : ""}`);
|
|
1716
|
+
}
|
|
1484
1717
|
if (sub === "escalate") return console.log(await taskOp("task_escalate", { id: rest[0] }));
|
|
1485
1718
|
if (sub === "assign") {
|
|
1486
1719
|
const { one, rest: positional } = takeFlags(rest, ["--role", "--revision"], []);
|
|
@@ -1495,14 +1728,18 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1495
1728
|
return console.log(await taskOp("task_unassign", { id: positional[0], role, ...(revision === undefined ? {} : { revision }) }));
|
|
1496
1729
|
}
|
|
1497
1730
|
if (sub === "label") {
|
|
1498
|
-
// #247: a
|
|
1731
|
+
// #247: a later verdict on an approved task, for research records (#933: an agent's own task only, attributed to it).
|
|
1499
1732
|
const { one, rest: positional } = takeFlags(rest, ["--created"], []);
|
|
1500
1733
|
const id = Number(positional[0]), label = positional[1] as Label;
|
|
1501
1734
|
if (positional.length !== 2 || !/^[1-9]\d*$/.test(positional[0]!) || !Number.isSafeInteger(id) || !(LABELS as readonly string[]).includes(label)) fail(`usage: ahub task label <id> ${LABELS.join("|")} [--created <time from ahub research export>]`);
|
|
1735
|
+
// #933: the daemon checks the current board row only, so an agent never names an earlier task with this id from before a reset.
|
|
1736
|
+
if (identity.role === "tools" && one["--created"] !== undefined) fail("--created names an earlier task with this id; only the person labels it (ahub console or a terminal)");
|
|
1502
1737
|
const project = researchProject();
|
|
1738
|
+
// #933: an agent labels only its own task, which the daemon checks by role; its record names it, so it never reads as the person's verdict.
|
|
1739
|
+
if (identity.role === "tools") await taskOp("task_label_check", { id });
|
|
1503
1740
|
let record: TaskRecord;
|
|
1504
1741
|
try { record = labelTarget(readStores(project.id), readEvents(join(stateDir, "events.jsonl")), id, one["--created"]); } catch (error) { fail((error as Error).message); }
|
|
1505
|
-
appendRecords(project.id, [{ schema: RESEARCH_SCHEMA, kind: "label", project: projectKey(project.id), task: id, createdAt: record!.createdAt!, label, at: new Date().toISOString() }]);
|
|
1742
|
+
appendRecords(project.id, [{ schema: RESEARCH_SCHEMA, kind: "label", project: projectKey(project.id), task: id, createdAt: record!.createdAt!, label, at: new Date().toISOString(), ...(identity.role === "tools" ? { by: identity.peer } : {}) }]);
|
|
1506
1743
|
return console.log(`task #${id} (approved ${record!.approvedAt}) labelled ${label} in the research records`);
|
|
1507
1744
|
}
|
|
1508
1745
|
if (sub !== "propose" || rest.length < 1) fail("usage: ahub task propose [<class>] <title...> | show <id> | assign <id> <peer> | unassign <id> | cancel <id> --reason <text> | escalate <id>");
|
|
@@ -1831,26 +2068,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1831
2068
|
const json = args.includes("--json");
|
|
1832
2069
|
const [sub, ...rest] = args.filter((a) => a !== "--json");
|
|
1833
2070
|
if (sub === "run") {
|
|
1834
|
-
const { one,
|
|
1835
|
-
if (one["--memory"] !== undefined && !(MEMORY_MODES as readonly string[]).includes(one["--memory"])) fail(`--memory takes ${MEMORY_MODES.join(", ")}`);
|
|
1836
|
-
// #248: `--addon ripwire=off` declares the arm's add-on state; the run refuses peers that do not match it.
|
|
1837
|
-
const addons: Partial<Record<AddonName, "on" | "off">> = {};
|
|
1838
|
-
for (const spec of many["--addon"] ?? []) {
|
|
1839
|
-
const [name, state] = spec.split("=");
|
|
1840
|
-
if (!name || !isAddon(name) || (state !== "on" && state !== "off") || addons[name]) fail(`--addon takes <name>=on|off once per add-on, name one of ${ADDONS.join(", ")}`);
|
|
1841
|
-
addons[name] = state;
|
|
1842
|
-
}
|
|
1843
|
-
const suite = positional[0];
|
|
1844
|
-
const repeat = one["--repeat"] === undefined ? 1 : Number(one["--repeat"]);
|
|
1845
|
-
if (!suite || positional.length !== 1 || !one["--arm"]?.trim() || !Number.isSafeInteger(repeat) || repeat < 1 || repeat > 100) {
|
|
1846
|
-
fail("usage: ahub bench run <suite.json> --arm <label> [--repeat 1..100] [--tasks a,b] [--addon <name>=on|off]... [--memory off|claude-mem|native|both]");
|
|
1847
|
-
}
|
|
2071
|
+
const { one, addons, suite, repeat } = benchRunArgs(rest);
|
|
1848
2072
|
await benchPreflight(cwd, loadConfig(cwd).bench.enabled); // the suite's refs are checked once it is read
|
|
1849
2073
|
let interrupted = false;
|
|
1850
2074
|
const stop = () => { interrupted = true; console.log("stopping at the next check: the attempt in progress is recorded as interrupted (a task still open is named)"); };
|
|
1851
2075
|
process.on("SIGINT", stop);
|
|
1852
2076
|
try {
|
|
1853
|
-
const run = await runBench({ cwd, stateDir, suitePath: resolve(invokedFrom, suite!), arm: one["--arm"]!.trim(), repeat, addons, ...(one["--memory"] ? { memory: one["--memory"] as MemoryMode } : {}), only: one["--tasks"]?.split(",").map((t) => t.trim()).filter(Boolean), stopped: () => interrupted }, defaultIo(stateDir, cwd));
|
|
2077
|
+
const run = await runBench({ cwd, stateDir, suitePath: resolve(invokedFrom, suite!), arm: one["--arm"]!.trim(), repeat, addons, ...(one["--memory"] ? { memory: one["--memory"] as MemoryMode } : {}), only: one["--tasks"]?.split(",").map((t) => t.trim()).filter(Boolean), stopped: () => interrupted }, defaultIo(stateDir, cwd, identity.role === "tools" ? identity.peer : undefined));
|
|
1854
2078
|
return console.log(formatBenchReport(benchReport(readRuns().filter((r) => r.header.run === run))).join("\n"));
|
|
1855
2079
|
} finally { process.off("SIGINT", stop); }
|
|
1856
2080
|
}
|
|
@@ -1915,12 +2139,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1915
2139
|
research: () => {
|
|
1916
2140
|
// #247: measures from the opt-in research records; `export` writes them out, `backfill` builds them from events.jsonl.
|
|
1917
2141
|
const [sub] = args;
|
|
1918
|
-
|
|
1919
|
-
const flags = new Set(["--all", "--json", "--since", "--format"]), valued = new Set(["--since", "--format"]);
|
|
1920
|
-
for (let i = sub === "export" || sub === "backfill" || sub === "addons" ? 1 : 0; i < args.length; i++) {
|
|
1921
|
-
if (!flags.has(args[i]!)) fail(`ahub research: unknown argument ${args[i]} (another project: ahub --project <dir> research)`);
|
|
1922
|
-
if (valued.has(args[i]!)) i++;
|
|
1923
|
-
}
|
|
2142
|
+
researchArgs();
|
|
1924
2143
|
if (sub === "backfill") {
|
|
1925
2144
|
const project = researchProject();
|
|
1926
2145
|
const written = appendRecords(project.id, taskRecords(readEvents(join(stateDir, "events.jsonl")), project.id, { version: VERSION, source: "backfill" }));
|
|
@@ -2084,9 +2303,14 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2084
2303
|
if (why.length) fail(`refusing to undo ${id}: ${why.join("\n")}${reverted ? "\nCodex's conversation was already reverted; restore the files later with ahub undo without --context" : ""}`);
|
|
2085
2304
|
return p;
|
|
2086
2305
|
};
|
|
2306
|
+
// #798 D3: the hub saw these submodules dirty when the turn ended and cannot see inside them: they are never restored, and never counted as "no change".
|
|
2307
|
+
const unobserved = turn.unobserved ?? [];
|
|
2308
|
+
const unobservedNote = unobserved.length ? `\nthese submodules were changed during the turn and their contents cannot be restored (the hub does not observe inside a submodule); undo them in the submodule itself:\n ${unobserved.join("\n ")}` : "";
|
|
2087
2309
|
const first = plan();
|
|
2088
|
-
if (first.undone) return console.log(`turn ${id} is already undone: its files are as they were before it started`);
|
|
2089
|
-
|
|
2310
|
+
if (first.undone && !unobserved.length) return console.log(`turn ${id} is already undone: its files are as they were before it started`);
|
|
2311
|
+
if (first.undone) return console.log(`turn ${id}: its files are as they were before it started${unobservedNote}`);
|
|
2312
|
+
// "changed no files" is true of the files the hub observes and false of the turn: with a submodule dirty it says what it can and cannot see instead.
|
|
2313
|
+
console.log((first.restore.length ? `turn ${id} changed:\n ${first.restore.join("\n ")}` : unobserved.length ? `turn ${id} changed no files the hub can restore` : `turn ${id} changed no files`) + unobservedNote);
|
|
2090
2314
|
console.log("(every change made in the project during the turn counts as its own, including any by Claude or by you)");
|
|
2091
2315
|
const projections = new TextProjections({ cwd: undoCwd, stateDir, config: undoConfig.text_projection ?? {}, deny: undoConfig.local.deny, readAllow: undoConfig.local.read_allow, log: line => console.error(line) });
|
|
2092
2316
|
const preview = projectedDiff({ repo, from: endTree, to: startTree, paths: first.restore, projections, deny: undoConfig.local.deny, privatePath: path => matchesPolicy(undoCwd, join(repo.top, path), undoConfig.pii?.paths ?? []) });
|
|
@@ -2115,6 +2339,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2115
2339
|
await stopProject(project);
|
|
2116
2340
|
console.log("hub stopped");
|
|
2117
2341
|
},
|
|
2342
|
+
// The counterpart of `ahub up`: the same verified graceful stop as `ahub kill`, never a second shutdown path.
|
|
2343
|
+
down: async () => { await commands.kill!(); },
|
|
2118
2344
|
|
|
2119
2345
|
reset: async () => {
|
|
2120
2346
|
if (args.some((a) => !["--all", "--yes"].includes(a))) fail("usage: ahub reset [--all] [--yes]");
|
|
@@ -2235,7 +2461,15 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2235
2461
|
}
|
|
2236
2462
|
if (args.includes("--orphans")) {
|
|
2237
2463
|
if(identity.role==="tools" && args.includes("--kill")) {
|
|
2238
|
-
|
|
2464
|
+
// #812: the conductor's shell asks the hub to stop its own orphan; #899: the answer prints for a person unless --json.
|
|
2465
|
+
const hub=await connect();
|
|
2466
|
+
try {
|
|
2467
|
+
const result=await hub.request({t:"conductor_operational",op:"orphans"},10_000);
|
|
2468
|
+
if(result.ok!==true)fail(String(result.error??"orphan inspection unavailable"),result.refusalAudited===true,result.authorityRefused);
|
|
2469
|
+
if (options.json) console.log(JSON.stringify(result,null,2));
|
|
2470
|
+
else printOutput(renderOrphanCleanup(result, options.columns), options.color);
|
|
2471
|
+
} finally{hub.close();}
|
|
2472
|
+
return;
|
|
2239
2473
|
}
|
|
2240
2474
|
return orphanDoctor(args.includes("--kill"), options);
|
|
2241
2475
|
}
|
|
@@ -2395,20 +2629,26 @@ function inspectDoctorRouting(cwd: string): { routing?: Routing; errors: string[
|
|
|
2395
2629
|
}
|
|
2396
2630
|
|
|
2397
2631
|
async function runConductorCommand(): Promise<void> {
|
|
2632
|
+
if (recoveryRunnerContinuation) return commands[cmd]!();
|
|
2398
2633
|
const operation=conductorOperation(cmd,args);
|
|
2399
2634
|
if(operation) {
|
|
2400
2635
|
const unavailable=`conductor authorization unavailable; nothing was executed; ${OPERATIONAL_PERSON_GUIDANCE}`;
|
|
2401
2636
|
const descriptor=readControl(stateDir);
|
|
2637
|
+
// Offline resume proves the source-issued local continuation inside the command; no console connection is borrowed.
|
|
2638
|
+
if (conductorPeer && cmd === "recovery" && (!descriptor?.pid || !descriptor.pidSignature || processLiveness(descriptor.pid, descriptor.pidSignature) !== "live")) return commands.recovery!();
|
|
2402
2639
|
if(!descriptor?.instanceId || !descriptor.projectId || !descriptor.pid || !descriptor.pidSignature || processLiveness(descriptor.pid,descriptor.pidSignature)!=="live")fail(unavailable);
|
|
2403
2640
|
let hub:ControlClient;
|
|
2404
2641
|
try {hub=await connect(3000);} catch {fail(unavailable);}
|
|
2405
2642
|
let release:(()=>void)|undefined;
|
|
2406
2643
|
try {
|
|
2407
2644
|
let result:any;
|
|
2408
|
-
|
|
2645
|
+
// #933: a form that runs in this process is admitted and audited once here (`admit`), after its arguments pass; any other form's own request admits it.
|
|
2646
|
+
if(conductorLocalOperation(operation))LOCAL_OPERATION_ARGS[operation]?.();
|
|
2647
|
+
try {result=await hub.request({t:"conductor_operational",op:conductorLocalOperation(operation)?"admit":"authorize",command:operation},3000);} catch {fail(unavailable);}
|
|
2409
2648
|
if(result.ok!==true)fail(result.authorityRefused===true || result.authorityRefused===false?String(result.error??unavailable):unavailable,result.refusalAudited===true,result.authorityRefused);
|
|
2410
2649
|
if(!hub.instanceId)fail(unavailable);
|
|
2411
|
-
|
|
2650
|
+
// Recovery replaces its source instance. Its own per-project proofs fence every later connection.
|
|
2651
|
+
if (!conductorPeer || !["restart", "upgrade", "recovery"].includes(cmd)) release=ControlClient.pinInstance(stateDir,hub.instanceId);
|
|
2412
2652
|
await commands[cmd]!();
|
|
2413
2653
|
} finally {release?.();hub.close();}
|
|
2414
2654
|
return;
|
|
@@ -2430,11 +2670,14 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2430
2670
|
} else {
|
|
2431
2671
|
// Native TUIs return a human launch command through the daemon; an agent shell never executes it.
|
|
2432
2672
|
if (configuredCommand) {
|
|
2433
|
-
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances);
|
|
2434
|
-
if (selection.preview) return runConfiguredAcpEntry({ kind: cmd, raw: args, catalog: projectCatalog, instances: declaredInstances, connect, print: text => console.log(text) });
|
|
2673
|
+
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances, peerConfig.declaredExternalInstances);
|
|
2674
|
+
if (selection.preview) return runConfiguredAcpEntry({ kind: cmd, raw: args, catalog: projectCatalog, instances: declaredInstances, declaredExternalInstances: peerConfig.declaredExternalInstances, projectRoot: cwd, connect, print: text => console.log(text) });
|
|
2435
2675
|
if (selection.sessionId) fail("conductor starts accept no session override; a person starts a configured ACP session");
|
|
2436
2676
|
const hub = await connect();
|
|
2437
2677
|
try {
|
|
2678
|
+
const external = selection.peer !== cmd && (peerConfig.declaredExternalInstances?.some(row => row.id === selection.peer && row.kind === cmd)
|
|
2679
|
+
|| projectCatalog.externalDefinitions.some(definition => definition.kind === cmd));
|
|
2680
|
+
if (external) fail("external module instances require an explicit person start");
|
|
2438
2681
|
await requireActiveConfiguredAcp(hub, cmd, selection.peer, text => console.error(text));
|
|
2439
2682
|
console.log(await taskOp("hub_peer_start", { peer: selection.peer, mode: "headless" }, false, hub, 70_000));
|
|
2440
2683
|
} finally { hub.close(); }
|
|
@@ -2451,9 +2694,9 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2451
2694
|
console.log(await taskOp(op, input));
|
|
2452
2695
|
}
|
|
2453
2696
|
const runConfigured = configuredCommand ? async () => {
|
|
2454
|
-
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances);
|
|
2697
|
+
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances, peerConfig.declaredExternalInstances);
|
|
2455
2698
|
if (!selection.preview) assertLifecycleAvailable();
|
|
2456
|
-
await runConfiguredAcpEntry({ kind: cmd, raw: args, catalog: projectCatalog, instances: declaredInstances, connect: connectLaunch, print: text => console.log(text), operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
2699
|
+
await runConfiguredAcpEntry({ kind: cmd, raw: args, catalog: projectCatalog, instances: declaredInstances, declaredExternalInstances: peerConfig.declaredExternalInstances, projectRoot: cwd, connect: connectLaunch, print: text => console.log(text), operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
2457
2700
|
} : undefined;
|
|
2458
2701
|
const run = identity.role === "tools" && commandAccess === "conductor" ? runConductorCommand : (Object.hasOwn(commands, cmd) ? commands[cmd] : undefined) ?? runConfigured ?? (() => fail(`unknown command "${cmd}"; run ahub help`));
|
|
2459
2702
|
try {
|