@staix/agent-hub 0.20.0 → 0.22.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 +66 -3
- package/README.md +2 -2
- package/docs/agent-notes/README.md +11 -0
- package/docs/agent-notes/adapters.md +7 -3
- 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 +17 -8
- package/docs/agent-notes/local-worker.md +4 -0
- package/docs/agent-notes/models.md +2 -0
- package/docs/agent-notes/tasks.md +17 -9
- package/docs/agent-notes/tests.md +9 -2
- package/docs/bench.md +1 -1
- package/docs/data-policy.md +2 -0
- package/docs/dogfood.md +39 -4
- package/docs/events.md +2 -1
- package/docs/memory.md +9 -3
- package/docs/operations.md +610 -62
- package/docs/profiles.md +13 -1
- package/docs/quickstart.md +18 -9
- package/docs/research.md +7 -4
- package/docs/sdlc.md +54 -10
- package/docs/security.md +396 -34
- package/docs/smoke.md +133 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +1101 -22
- package/docs/verification/2026-10-27-0.19.0.md +5 -5
- 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/2026-10-30-0.22.0.md +49 -0
- package/docs/verification/2026-10-30-445-capability-probes.md +172 -0
- package/docs/verification/2026-10-30-bus-delivery-operational-settings.md +77 -0
- package/docs/verification/2026-10-30-jcode-ac4.md +24 -0
- package/docs/verification/2026-10-30-jcode-ac6.md +52 -0
- package/docs/verification/2026-10-30-recovery-wait-settings.md +141 -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 +15 -15
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1016 -280
- package/src/adapters/acp.ts +79 -3
- package/src/adapters/channel-peer.ts +4 -0
- package/src/adapters/claude-channel.ts +13 -6
- package/src/adapters/claude-implementer.ts +60 -7
- package/src/adapters/codex-appserver.ts +77 -4
- package/src/adapters/conformance/fakes/jcode-agent.ts +6 -2
- package/src/adapters/conformance/kit.ts +3 -9
- 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 +40 -25
- package/src/adapters/native-proof-ports.ts +10 -0
- package/src/adapters/registry-types.ts +4 -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 +29 -16
- package/src/cli/identity.ts +25 -10
- package/src/cli/init.ts +34 -7
- package/src/cli/intake-output.ts +33 -0
- package/src/cli/learn-output.ts +38 -0
- package/src/cli/main.ts +334 -73
- package/src/cli/output.ts +41 -1
- package/src/cli/recovery-package.ts +18 -1
- package/src/cli/recovery-waits.ts +26 -0
- 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 +82 -0
- package/src/cli/smoke-pty.ts +153 -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 +112 -28
- package/src/cli/upgrade.ts +68 -8
- package/src/hub/ask.ts +76 -12
- package/src/hub/board.ts +132 -22
- package/src/hub/bus.ts +416 -65
- package/src/hub/child-process.ts +2 -0
- package/src/hub/ci-watch.ts +42 -20
- package/src/hub/claude-failure-stage.ts +85 -0
- package/src/hub/claude-shell-wrapper.ts +11 -5
- package/src/hub/conductor.ts +17 -4
- package/src/hub/config-apply.ts +34 -6
- package/src/hub/config-reload.ts +7 -5
- package/src/hub/config-trust.ts +7 -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 +1209 -183
- package/src/hub/deliverable-files.ts +8 -2
- package/src/hub/delivery-journal.ts +200 -73
- 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 +117 -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 +154 -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 +12 -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 +27 -2
- 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/tasks.ts +987 -110
- package/src/hub/workflow.ts +1 -1
- package/src/local/doc-read.ts +69 -0
- package/src/local/proxy.ts +4 -13
- 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,23 +22,28 @@ 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";
|
|
29
|
+
import { renderLearnChange, renderLearnList } from "./learn-output.ts";
|
|
23
30
|
import { terminalText } from "./console-state.ts";
|
|
24
31
|
import { parseSettingsEditArgs, runSettingsEdit, SETTINGS_EDIT_USAGE } from "./settings-edit.ts";
|
|
25
32
|
import { petView, readPet, renderPet } from "./pet.ts";
|
|
26
33
|
import { startPetWindow, closePetWindow } from "../pet/window.ts";
|
|
27
34
|
import { loadPetConfig, savePetPalette } from "./pet-config.ts";
|
|
28
|
-
import {
|
|
35
|
+
import { projectProfile, resolveProfile } from "../hub/profiles.ts";
|
|
29
36
|
import { catalogAssignments, loadRoleCatalog, selectedRoles } from "../hub/roles.ts";
|
|
30
37
|
import { attachWatch } from "./watch.ts";
|
|
31
38
|
import { verifiedJcodeCommand } from "../adapters/jcode.ts";
|
|
32
39
|
import { configureJcodeMcp, jcodeExtraMcpServers } from "./jcode-mcp.ts";
|
|
33
40
|
import { qwenSessionId } from "../hub/crash.ts";
|
|
34
41
|
import { isPermissionMode } from "../hub/permission-mode.ts";
|
|
35
|
-
import { parseSettingText, settingDef, settingText, type SettingRow } from "../hub/settings.ts";
|
|
42
|
+
import { parseSettingText, settingDef, settingText, undoableDef, type SettingRow } from "../hub/settings.ts";
|
|
36
43
|
import { startModeOf } from "../hub/start-mode.ts";
|
|
37
|
-
import { OVERLAY_FILE, parseOverlay, parseRouting, routingText, type Routing, type RoutingOverlay } from "../hub/routing.ts";
|
|
44
|
+
import { currentRouting, OVERLAY_FILE, parseOverlay, parseRouting, routingText, type Routing, type RoutingOverlay } from "../hub/routing.ts";
|
|
38
45
|
import { spawn, spawnSync } from "node:child_process";
|
|
39
|
-
import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
46
|
+
import { chmodSync, closeSync, constants, existsSync, fstatSync, openSync, readFileSync, readSync, renameSync, writeFileSync } from "node:fs";
|
|
40
47
|
import { randomUUID } from "node:crypto";
|
|
41
48
|
import { homedir } from "node:os";
|
|
42
49
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
@@ -59,8 +66,8 @@ import { VERSION } from "../version.ts";
|
|
|
59
66
|
import { freeText } from "./free-text.ts";
|
|
60
67
|
import { createInterface } from "node:readline/promises";
|
|
61
68
|
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";
|
|
69
|
+
import { activeOperation, assertLifecycleAvailable, operationIdPattern, readOperation, writeOperation, recoveryLock, recoveryRunner, signedRunner, stopSignedRunner, supportsAction } from "../hub/recovery-store.ts";
|
|
70
|
+
import { childEnv, personEnv } from "../hub/child-process.ts";
|
|
64
71
|
import { abortRecovery, createOperation, disposeRecovery, recordRecoveryDelegation, repairedFreshSession, reattachRecovery, liveProjects, nextChoices, nextActionsText, publicOperation, recoveryCommand, registeredProjects, runRecovery, type RecoveryOperation, type Inspection } from "./upgrade.ts";
|
|
65
72
|
import { endPlannedPeer, makeRecoveryDriver, makeUpgradePlan, preserveSource } from "./upgrade-runtime.ts";
|
|
66
73
|
import { exactVersion, latestRelease, newerVersion } from "./recovery-package.ts";
|
|
@@ -93,10 +100,10 @@ import { agentShellCause, classifyPeerCommand, cliCommandLabel, detectCliIdentit
|
|
|
93
100
|
import { recordCliAudit, recordDelegatedCliAudit } from "./identity-audit.ts";
|
|
94
101
|
import { runConsole } from "./console.ts";
|
|
95
102
|
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";
|
|
103
|
+
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
104
|
import { renderHelp, configuredAcpHelp } from "./help.ts";
|
|
98
105
|
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";
|
|
106
|
+
import { renderAuthorization, renderSdlcCosts, renderSdlcExplain, renderSdlcMerge, renderSdlcMetrics, renderSdlcQueue, renderSdlcRelease, renderSdlcStatus } from "./sdlc-output.ts";
|
|
100
107
|
import { renderTailEvent } from "./tail-render.ts";
|
|
101
108
|
import { archiveProblem, archiveState, damagedState, failureText, MANIFEST, planReset, resetLines, resetRuntime, startState, type ResetPlan } from "./reset.ts";
|
|
102
109
|
|
|
@@ -160,7 +167,7 @@ function localDeclarations(): { scope: Pick<InstanceRoleConfig, "instances" | "c
|
|
|
160
167
|
catch (error) { return { scope: { catalog: createProjectAdapterCatalog() }, error: startupDiagnostic(error) }; }
|
|
161
168
|
}
|
|
162
169
|
const projectCatalog = peerConfig.catalog;
|
|
163
|
-
const configuredCommand = !definitionOf(given) && projectCatalog.definitionOf(given)?.permission.genericAcp !== undefined;
|
|
170
|
+
const configuredCommand = !definitionOf(given) && (projectCatalog.definitionOf(given)?.permission.genericAcp !== undefined || peerConfig.declaredExternalInstances?.some(row => row.kind === given) === true);
|
|
164
171
|
const unboundCaller = detectCliIdentity(process.env);
|
|
165
172
|
const markerNeedsDeclaration = unboundCaller.role === "invalid" && unboundCaller.reason === "AGENTHUB_PEER_ID conflicts with native agent marker"
|
|
166
173
|
&& process.env.AGENTHUB_PEER_ID !== undefined && !definitionOf(process.env.AGENTHUB_PEER_ID);
|
|
@@ -170,7 +177,7 @@ let nativeSelection: ReturnType<typeof instanceLaunchArguments>;
|
|
|
170
177
|
try {
|
|
171
178
|
declaredInstances = needsInstances || configuredCommand ? peerConfig.instances : undefined;
|
|
172
179
|
if (configuredCommand && !(rest.length === 1 && ["--help", "-h"].includes(rest[0]!))) {
|
|
173
|
-
const selection = parseConfiguredAcpArguments(given, rest, projectCatalog, declaredInstances);
|
|
180
|
+
const selection = parseConfiguredAcpArguments(given, rest, projectCatalog, declaredInstances, peerConfig.declaredExternalInstances);
|
|
174
181
|
nativeSelection = { ...(selection.peer !== given ? { instance: selection.peer } : {}), args: [...rest] };
|
|
175
182
|
} else nativeSelection = instanceLaunchArguments(given, rest, declaredInstances);
|
|
176
183
|
} catch (error) { fail(startupDiagnostic(error)); }
|
|
@@ -207,11 +214,30 @@ const stateDir = selected.stateDir;
|
|
|
207
214
|
var operationalAuditReady=false, operationalRefusalRecorded=false;
|
|
208
215
|
const caller = declaredInstances ? detectCliIdentity(process.env, declaredInstances) : unboundCaller;
|
|
209
216
|
const commandLabel = cliCommandLabel(cmd, args, projectCatalog);
|
|
210
|
-
|
|
217
|
+
let commandAccess = classifyPeerCommand(cmd, args, projectCatalog);
|
|
218
|
+
const continuationPeer = process.env.AGENTHUB_RECOVERY_CONDUCTOR;
|
|
219
|
+
const continuationId = process.env.AGENTHUB_RECOVERY_OPERATION;
|
|
220
|
+
let recoveryLauncher = false, recoveryRunnerContinuation = false;
|
|
221
|
+
let recoveryLauncherCurrent: (() => void) | undefined;
|
|
222
|
+
if (continuationPeer !== undefined) {
|
|
223
|
+
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");
|
|
224
|
+
let operation: RecoveryOperation;
|
|
225
|
+
try { operation = readOperation<RecoveryOperation>(continuationId); }
|
|
226
|
+
catch { fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal"); }
|
|
227
|
+
if (!operation || operation.id !== continuationId || typeof operation.sourceRoot !== "string") fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
228
|
+
if (cmd === "recovery-run" && args.length === 1 && args[0] === continuationId) {
|
|
229
|
+
await authorizeConductorOperation(operation, continuationPeer);
|
|
230
|
+
if (realPath(operation.sourceRoot) !== realPath(resolve(import.meta.dir, "../.."))) fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
231
|
+
recoveryRunnerContinuation = true; commandAccess = "conductor";
|
|
232
|
+
} else if (process.env.AGENTHUB_RECOVERY_CONTINUATION === continuationId) {
|
|
233
|
+
await authorizeConductorLaunch(operation, continuationPeer, cwd, resolve(import.meta.dir, "../.."), cmd, args); recoveryLauncher = true; recoveryLauncherCurrent = () => assertConductorOperationLocal(operation, continuationPeer);
|
|
234
|
+
} else fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
235
|
+
}
|
|
236
|
+
const conductorPeer = !liveTest && caller.role === "tools" && (recoveryRunnerContinuation || ["restart", "upgrade", "recovery"].includes(cmd) && commandAccess === "conductor") ? caller.peer : undefined;
|
|
211
237
|
// Agent-authored content keeps its peer. Operational starts/holds may borrow console authority for live verification.
|
|
212
238
|
const delegationRefused = liveTest && caller.role === "tools" && commandAccess !== "allowed" && liveTestRefusal(cmd, args);
|
|
213
239
|
const delegatedPeer = liveTest && caller.role === "tools" && commandAccess !== "allowed" && !delegationRefused ? caller.peer : undefined;
|
|
214
|
-
const identity = delegatedPeer ? { role: "console" as const } : caller;
|
|
240
|
+
const identity = delegatedPeer || recoveryLauncher ? { role: "console" as const } : caller;
|
|
215
241
|
operationalAuditReady=identity.role==="tools" && commandAccess==="conductor" && conductorOperation(cmd,args)!==undefined;
|
|
216
242
|
function audit(outcome: "run" | "refused" | "invalid"): void {
|
|
217
243
|
if (caller.role === "console") return;
|
|
@@ -576,13 +602,13 @@ function printRecoveryStatus(op: RecoveryOperation, message: string, presentatio
|
|
|
576
602
|
}
|
|
577
603
|
}
|
|
578
604
|
async function recoveryEndInspection(op: RecoveryOperation): Promise<Record<string, Inspection | undefined>> {
|
|
579
|
-
const driver = makeRecoveryDriver(undefined, delegatedPeer);
|
|
605
|
+
const driver = makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer);
|
|
580
606
|
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
607
|
}
|
|
582
608
|
|
|
583
609
|
function spawnRecovery(operation: RecoveryOperation, quiet = false, presentation?: RecoveryPresentationOptions): void {
|
|
584
610
|
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 },
|
|
611
|
+
cwd, detached: true, stdio: "ignore", env: { ...process.env, AGENTHUB_RECOVERY_OPERATION: operation.id, ...(conductorPeer ? { AGENTHUB_PEER_ID: conductorPeer, AGENTHUB_RECOVERY_CONDUCTOR: conductorPeer } : {}) },
|
|
586
612
|
});
|
|
587
613
|
child.on("error", () => console.error(`runner launch failed; use ${recoveryCommand(operation, "resume")}`));
|
|
588
614
|
child.unref();
|
|
@@ -657,7 +683,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
657
683
|
lock: recoveryLock,
|
|
658
684
|
read: (id) => readOperation<RecoveryOperation>(id),
|
|
659
685
|
runner: recoveryRunner,
|
|
660
|
-
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer).inspect),
|
|
686
|
+
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect),
|
|
661
687
|
endInspection: recoveryEndInspection,
|
|
662
688
|
endPeer: (planned, peer) => endPlannedPeer(planned, peer),
|
|
663
689
|
runnerStoppable: (id) => signedRunner(id) !== undefined,
|
|
@@ -684,6 +710,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
684
710
|
// Before the value reaches a package spec or the registry: a range such as 0.13 would resolve to whatever matches.
|
|
685
711
|
if (to !== undefined) { try { exactVersion(to); } catch (error) { fail((error as Error).message); } }
|
|
686
712
|
// An older CLI must not coordinate a newer target (docs/operations.md): the target's own coordinator takes over.
|
|
713
|
+
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
714
|
if (kind === "upgrade" && newerVersion(to!, VERSION) && process.env.AGENTHUB_UPGRADE_HANDOVER !== "1") {
|
|
688
715
|
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", ...liveTestArgs, "upgrade", "--to", to!, ...rest, ...display.flags, ...(dry && !rest.includes("--dry-run") ? ["--dry-run"] : [])];
|
|
689
716
|
console.error(`ahub: ${to} is newer than this CLI (${VERSION}), so its own coordinator runs the upgrade: bun ${argv.join(" ")}`);
|
|
@@ -703,7 +730,8 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
703
730
|
}
|
|
704
731
|
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
732
|
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);
|
|
733
|
+
const plan = await makeUpgradePlan(kind, to ?? VERSION, kind === "restart" ? cwd : undefined, undefined, conductorPeer);
|
|
734
|
+
if (conductorPeer) await preflightConductorRecovery(plan, conductorPeer);
|
|
707
735
|
const jsonApply = display.json && yes && !dry;
|
|
708
736
|
// #814: a person reading a pipe or a dry run gets the plan screen's rows, with every blocker, reconnect-only session and
|
|
709
737
|
// fresh start in them. `--json` keeps the document and the stderr lines (#206) exactly as they were.
|
|
@@ -715,7 +743,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
715
743
|
const blockers = [...plan.blockers, ...plan.projects.flatMap((p) => p.blockers.map((b) => `${p.project.id}: ${b}`))];
|
|
716
744
|
if (display.json) for (const blocker of blockers) console.error(`ahub: blocker: ${blocker}`);
|
|
717
745
|
if (dry) return;
|
|
718
|
-
if (blockers.length) { if (jsonApply) console.log(JSON.stringify(plan, null, 2)); fail("plan has blockers; no runtime was changed"); }
|
|
746
|
+
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
747
|
assertLifecycleAvailable();
|
|
720
748
|
if (!yes) {
|
|
721
749
|
if (!process.stdin.isTTY) fail("review --dry-run and use --yes in non-interactive sessions");
|
|
@@ -724,10 +752,21 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
724
752
|
if (!/^(y|yes)$/i.test((await rl.question("Apply this project list and restore its sessions? [y/N] ")).trim())) return;
|
|
725
753
|
} finally { rl.close(); }
|
|
726
754
|
}
|
|
727
|
-
const current = await makeUpgradePlan(kind, plan.version, kind === "restart" ? cwd : undefined);
|
|
755
|
+
const current = await makeUpgradePlan(kind, plan.version, kind === "restart" ? cwd : undefined, undefined, conductorPeer);
|
|
728
756
|
if (current.fingerprint !== plan.fingerprint) fail("plan changed during review; run the command again");
|
|
757
|
+
const routingDefaults = conductorPeer ? await preflightConductorRecovery(plan, conductorPeer) : undefined;
|
|
729
758
|
const sourceRoot = preserveSource(plan);
|
|
730
|
-
|
|
759
|
+
let operation: RecoveryOperation;
|
|
760
|
+
if (conductorPeer) {
|
|
761
|
+
const id = randomUUID();
|
|
762
|
+
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: {} })) };
|
|
763
|
+
const target = await makeRecoveryDriver().stage(pending);
|
|
764
|
+
pending.targetRoot = target.root; pending.targetDigest = target.digest; pending.targetFeatures = target.targetFeatures;
|
|
765
|
+
await issueConductorOperation(pending, conductorPeer);
|
|
766
|
+
operation = createOperation(plan, sourceRoot, undefined, { peer: conductorPeer, command: kind }, id);
|
|
767
|
+
operation.targetRoot = target.root; operation.targetDigest = target.digest; operation.targetFeatures = target.targetFeatures; operation.conductorRoutingDefaults = routingDefaults;
|
|
768
|
+
writeOperation(id, operation);
|
|
769
|
+
} else operation = createOperation(plan, sourceRoot, undefined, delegatedPeer ? { peer: delegatedPeer, command: kind } : undefined);
|
|
731
770
|
if (display.json) { spawnRecovery(operation, true); console.log(JSON.stringify(publicOperation(operation), null, 2)); }
|
|
732
771
|
else spawnRecovery(operation, false, humanRecovery(false) ? display.options : undefined);
|
|
733
772
|
}
|
|
@@ -741,6 +780,40 @@ function parseRecoveryReattach(argv: readonly string[]): { id: string; peer: Ter
|
|
|
741
780
|
return { id: argv[1]!, peer: argv[2] as TerminalPeer };
|
|
742
781
|
}
|
|
743
782
|
|
|
783
|
+
/** #248: `ahub bench run` arguments, checked before anything runs (#933: also before the conductor's admission writes its audit row). */
|
|
784
|
+
function benchRunArgs(rest: string[]) {
|
|
785
|
+
const { one, many, rest: positional } = takeFlags(rest, ["--arm", "--repeat", "--tasks", "--memory"], ["--addon"]);
|
|
786
|
+
if (one["--memory"] !== undefined && !(MEMORY_MODES as readonly string[]).includes(one["--memory"])) fail(`--memory takes ${MEMORY_MODES.join(", ")}`);
|
|
787
|
+
// #248: `--addon ripwire=off` declares the arm's add-on state; the run refuses peers that do not match it.
|
|
788
|
+
const addons: Partial<Record<AddonName, "on" | "off">> = {};
|
|
789
|
+
for (const spec of many["--addon"] ?? []) {
|
|
790
|
+
const [name, state] = spec.split("=");
|
|
791
|
+
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(", ")}`);
|
|
792
|
+
addons[name] = state;
|
|
793
|
+
}
|
|
794
|
+
const suite = positional[0];
|
|
795
|
+
const repeat = one["--repeat"] === undefined ? 1 : Number(one["--repeat"]);
|
|
796
|
+
if (!suite || positional.length !== 1 || !one["--arm"]?.trim() || !Number.isSafeInteger(repeat) || repeat < 1 || repeat > 100) {
|
|
797
|
+
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]");
|
|
798
|
+
}
|
|
799
|
+
return { one, addons, suite, repeat };
|
|
800
|
+
}
|
|
801
|
+
/** Another project is `ahub --project <dir> research`; anything unknown is refused rather than ignored. */
|
|
802
|
+
function researchArgs(): void {
|
|
803
|
+
const sub = args[0], flags = new Set(["--all", "--json", "--since", "--format"]), valued = new Set(["--since", "--format"]);
|
|
804
|
+
for (let i = sub === "export" || sub === "backfill" || sub === "addons" ? 1 : 0; i < args.length; i++) {
|
|
805
|
+
if (!flags.has(args[i]!)) fail(`ahub research: unknown argument ${args[i]} (another project: ahub --project <dir> research)`);
|
|
806
|
+
if (valued.has(args[i]!)) i++;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const SOURCE_USAGE = "usage: ahub source add <path|url> [--json] [--color=auto|always|never] | list [--json] [--color=auto|always|never]";
|
|
810
|
+
/** #933: a local conductor form's argument check, run before the `admit` handshake so a usage error leaves no admitted row. */
|
|
811
|
+
const LOCAL_OPERATION_ARGS: Record<string, () => void> = {
|
|
812
|
+
"bench run": () => { benchRunArgs(args.filter(arg => arg !== "--json").slice(1)); },
|
|
813
|
+
"research backfill": researchArgs,
|
|
814
|
+
"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); },
|
|
815
|
+
};
|
|
816
|
+
|
|
744
817
|
const commands: Record<string, () => Promise<void> | void> = {
|
|
745
818
|
upgrade: () => upgrade("upgrade"),
|
|
746
819
|
restart: () => upgrade("restart"),
|
|
@@ -778,7 +851,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
778
851
|
const result = await reattachRecovery(selected.id, selected.peer, makeRecoveryDriver());
|
|
779
852
|
if (display.json) { console.log(JSON.stringify(publicOperation(result), null, 2)); return; }
|
|
780
853
|
printRecovery(renderOperation(result, undefined, display.options));
|
|
781
|
-
const live = await liveProjects(result, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
854
|
+
const live = await liveProjects(result, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect);
|
|
782
855
|
printRecovery(renderBlocked(result, nextChoices(result, undefined, live), display.options));
|
|
783
856
|
return;
|
|
784
857
|
}
|
|
@@ -805,15 +878,20 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
805
878
|
audit("refused"); fail(LIVE_TEST_PERSON_REFUSAL);
|
|
806
879
|
}
|
|
807
880
|
}
|
|
881
|
+
if (conductorPeer) {
|
|
882
|
+
if (operation.id !== id) fail("conductor recovery authority unavailable; the person runs it in ahub console or a terminal");
|
|
883
|
+
await authorizeConductorOperation(operation, conductorPeer);
|
|
884
|
+
}
|
|
808
885
|
const runner = recoveryRunner(id);
|
|
809
886
|
if (action === "status") {
|
|
810
887
|
// #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);
|
|
888
|
+
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect);
|
|
812
889
|
if (display.json) console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
813
890
|
else printRecovery(renderRecoveryStatus(operation, runner, nextChoices(operation, runner, live), display.options));
|
|
814
891
|
}
|
|
815
892
|
else if (action === "abort") {
|
|
816
|
-
await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer));
|
|
893
|
+
await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer));
|
|
894
|
+
if (conductorPeer) retireConductorOperation(readOperation<RecoveryOperation>(id), conductorPeer);
|
|
817
895
|
if (display.json) console.log(JSON.stringify(publicOperation(readOperation<RecoveryOperation>(id)), null, 2));
|
|
818
896
|
else console.log("preflight cancelled; no committed transition was rolled back");
|
|
819
897
|
}
|
|
@@ -823,7 +901,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
823
901
|
const stop = flags.length === 1 && flags[0] === "--stop-and-archive";
|
|
824
902
|
const reason = one["--reason"]?.trim() ?? "";
|
|
825
903
|
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));
|
|
904
|
+
const result = await disposeRecovery(id, stop ? { stop: true } : { fresh: one["--fresh-session"]! }, reason, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer));
|
|
827
905
|
if (!stop) {
|
|
828
906
|
if (display.json) { console.log(JSON.stringify(publicOperation(result), null, 2)); return spawnRecovery(result, true); }
|
|
829
907
|
console.log(`${one["--fresh-session"]}: a fresh session is accepted and the lost one is recorded; resuming`);
|
|
@@ -834,7 +912,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
834
912
|
}
|
|
835
913
|
else if (["completed", "cancelled"].includes(operation.phase)) console.log(display.json ? JSON.stringify(publicOperation(operation, runner), null, 2) : `recovery is already ${operation.phase}`);
|
|
836
914
|
// #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))}`);
|
|
915
|
+
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
916
|
else if (runner === "unknown") {
|
|
839
917
|
if (display.json) console.log(JSON.stringify(publicOperation(operation, runner), null, 2));
|
|
840
918
|
else if (humanRecovery(false)) printRecoveryStatus(operation, "Runner ownership is unknown; nothing was started.", display.options);
|
|
@@ -853,8 +931,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
853
931
|
},
|
|
854
932
|
"recovery-run": async () => {
|
|
855
933
|
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));
|
|
934
|
+
const result = await runRecovery(args[0]!, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer));
|
|
857
935
|
if (result.phase !== "completed") process.exitCode = 1;
|
|
936
|
+
else if (conductorPeer) retireConductorOperation(result, conductorPeer);
|
|
858
937
|
},
|
|
859
938
|
help: () => {
|
|
860
939
|
const color = resolveColor(undefined, { isTTY: !!process.stdout.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR }) === true;
|
|
@@ -923,10 +1002,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
923
1002
|
},
|
|
924
1003
|
|
|
925
1004
|
setup: async () => {
|
|
1005
|
+
recoveryLauncherCurrent?.();
|
|
926
1006
|
assertLifecycleAvailable();
|
|
927
1007
|
const root = join(import.meta.dir, "..", "..");
|
|
928
1008
|
const state = () => {
|
|
929
|
-
const out = (argv: string[]) => spawnSync("claude", argv, { encoding: "utf8" }).stdout ?? "";
|
|
1009
|
+
const out = (argv: string[]) => { recoveryLauncherCurrent?.(); const result = spawnSync("claude", argv, { encoding: "utf8", ...(recoveryLauncherCurrent ? { timeout: 30_000 } : {}) }); recoveryLauncherCurrent?.(); return result.stdout ?? ""; };
|
|
930
1010
|
return nextStep(parseList<InstalledPlugin>(out(["plugin", "list", "--json"])), parseList<Marketplace>(out(["plugin", "marketplace", "list", "--json"])), root);
|
|
931
1011
|
};
|
|
932
1012
|
let step = state();
|
|
@@ -943,17 +1023,21 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
943
1023
|
}
|
|
944
1024
|
for (let i = 0; step && i < 6; i++, step = state()) {
|
|
945
1025
|
console.log(`> ${step.argv.join(" ")} (${step.why})`);
|
|
946
|
-
|
|
1026
|
+
recoveryLauncherCurrent?.();
|
|
1027
|
+
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`);
|
|
1028
|
+
recoveryLauncherCurrent?.();
|
|
947
1029
|
}
|
|
948
1030
|
if (step) fail(`still not done after 6 steps; next would be: ${step.argv.join(" ")}`);
|
|
949
1031
|
}
|
|
950
|
-
|
|
1032
|
+
if (recoveryLauncherCurrent) recoveryLauncherCurrent();
|
|
1033
|
+
else await commands.doctor!();
|
|
951
1034
|
},
|
|
952
1035
|
|
|
953
1036
|
init: () => {
|
|
954
1037
|
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
|
-
|
|
1038
|
+
if (flags.rest.length) fail("usage: ahub init [--profile dev|research|writing|general|<pack>] [--shadow] [--release-guard] [--dry-run --json]");
|
|
1039
|
+
// 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.
|
|
1040
|
+
const requestedProfile = flags.one["--profile"];
|
|
957
1041
|
if (args.includes("--dry-run")) {
|
|
958
1042
|
const plan = planInit(cwd, requestedProfile, args.includes("--shadow"), args.includes("--release-guard"));
|
|
959
1043
|
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 +1067,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
983
1067
|
up: async () => {
|
|
984
1068
|
const unattended = unattendedEnv || args.includes("--unattended");
|
|
985
1069
|
if (unattended) console.error(UNATTENDED_WARNING);
|
|
986
|
-
const status = await startProject(registeredProject(), { unattended, liveTest });
|
|
1070
|
+
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
1071
|
console.log(`ahub up (ws://127.0.0.1:${status.controlPort}), state in ${stateDir}`);
|
|
1072
|
+
if (recoveryLauncherCurrent) { recoveryLauncherCurrent(); return; }
|
|
988
1073
|
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
989
1074
|
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
990
1075
|
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir, liveTest });
|
|
@@ -1048,9 +1133,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1048
1133
|
},
|
|
1049
1134
|
|
|
1050
1135
|
say: async () => {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1136
|
+
let addressed: ReturnType<typeof sayRecipients>;
|
|
1137
|
+
try { addressed = sayRecipients(args); } catch (error) { return fail((error as Error).message); }
|
|
1138
|
+
const { to, text } = addressed;
|
|
1139
|
+
const body = text.length ? freeText(text, "ahub say [@peer ...] <text>") : "";
|
|
1054
1140
|
const hub = await connect();
|
|
1055
1141
|
const res = await hub.request({ t: "send", body, to });
|
|
1056
1142
|
hub.close();
|
|
@@ -1249,8 +1335,161 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1249
1335
|
return shown(renderReleaseChange)(taskOp("release_change", { name, revision: Number(revision), fields }));
|
|
1250
1336
|
},
|
|
1251
1337
|
|
|
1338
|
+
smoke: async () => {
|
|
1339
|
+
// Log every live-test invocation, including syntax/target refusals; native output never joins the audit.
|
|
1340
|
+
if (liveTest && caller.role === "console") {
|
|
1341
|
+
try { recordCliAudit(stateDir, "user", commandLabel, "run"); }
|
|
1342
|
+
catch { return fail("smoke audit unavailable; nothing was executed"); }
|
|
1343
|
+
}
|
|
1344
|
+
const options = outputOptions();
|
|
1345
|
+
const operands = args.filter(arg => arg !== "--json");
|
|
1346
|
+
const usage = "usage: ahub --live-test smoke run qwen-dual-output --project-id <id> --receipt <file.json> [--timeout-ms <100..120000>] [--json]";
|
|
1347
|
+
const values = new Map<string, string>();
|
|
1348
|
+
if (operands[0] !== "run" || operands[1] !== "qwen-dual-output") return fail(usage);
|
|
1349
|
+
for (let n = 2; n < operands.length; n += 2) {
|
|
1350
|
+
const key = operands[n], value = operands[n + 1];
|
|
1351
|
+
if (!key || !["--project-id", "--receipt", "--timeout-ms"].includes(key) || !value || value.startsWith("--") || values.has(key)) return fail(usage);
|
|
1352
|
+
values.set(key, value);
|
|
1353
|
+
}
|
|
1354
|
+
if (!values.has("--project-id") || !values.has("--receipt")) return fail(usage);
|
|
1355
|
+
const home = process.env.AGENTHUB_HOME;
|
|
1356
|
+
if (!liveTest || !home) return fail("smoke target refused");
|
|
1357
|
+
const { validateSmokeTarget } = await import("./smoke-target.ts");
|
|
1358
|
+
const { runSmoke } = await import("./smoke-run.ts");
|
|
1359
|
+
const project = registeredProjects(home).find(project => project.root === cwd && project.stateDir === stateDir);
|
|
1360
|
+
if (!project) return fail("smoke target refused");
|
|
1361
|
+
// Keep path spelling for the guard: project selection above canonicalizes symlinks for ordinary CLI commands.
|
|
1362
|
+
const smokeCwd = selector && selector !== project.id ? resolve(invokedFrom, selector) : cwd;
|
|
1363
|
+
const checked = validateSmokeTarget({ cwd: smokeCwd, hubHome: home, project, expectedProjectId: values.get("--project-id")!, liveTest });
|
|
1364
|
+
const target = { ...checked, check: () => { checked.check(); assertDelegatedAuditConsumer(); } };
|
|
1365
|
+
const abort = new AbortController();
|
|
1366
|
+
const interrupted = () => abort.abort();
|
|
1367
|
+
process.on("SIGINT", interrupted); process.on("SIGTERM", interrupted);
|
|
1368
|
+
try {
|
|
1369
|
+
const result = await runSmoke(target, { leg: "qwen-dual-output", receipt: values.get("--receipt")!, timeoutMs: Number(values.get("--timeout-ms") ?? "60000"), signal: abort.signal });
|
|
1370
|
+
process.exitCode = result.outcome === "passed" ? 0 : 1;
|
|
1371
|
+
if (options.json) return console.log(JSON.stringify(result));
|
|
1372
|
+
console.log(`smoke ${result.outcome} (${result.reason}): ${result.counts.submits} submits, ${result.counts.stops} stops; cleanup ${result.checks.cleaned ? "confirmed" : "unconfirmed"}`);
|
|
1373
|
+
} finally { process.removeListener("SIGINT", interrupted); process.removeListener("SIGTERM", interrupted); }
|
|
1374
|
+
},
|
|
1375
|
+
|
|
1376
|
+
room: async () => {
|
|
1377
|
+
if (liveTest && !["list", "status"].includes(args[0] ?? "")) fail(LIVE_TEST_PERSON_REFUSAL);
|
|
1378
|
+
const { runRoomCommand, roomCommandError } = await import("./room.ts");
|
|
1379
|
+
const hub = await connect();
|
|
1380
|
+
try { await runRoomCommand(args, message => hub.request(message)); }
|
|
1381
|
+
catch (error) { fail(roomCommandError(error)); }
|
|
1382
|
+
finally { hub.close(); }
|
|
1383
|
+
},
|
|
1384
|
+
|
|
1385
|
+
broker: async () => {
|
|
1386
|
+
const { runBrokerCommand, brokerCommandError } = await import("./broker.ts");
|
|
1387
|
+
try { await runBrokerCommand(args, stateDir); }
|
|
1388
|
+
catch (error) { fail(brokerCommandError(error)); }
|
|
1389
|
+
},
|
|
1390
|
+
|
|
1391
|
+
source: async () => {
|
|
1392
|
+
// #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:
|
|
1393
|
+
// 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.
|
|
1394
|
+
const options = outputOptions();
|
|
1395
|
+
const operands = args.filter(arg => arg !== "--json" && arg !== "--full" && !arg.startsWith("--color="));
|
|
1396
|
+
const [sub, ...rest] = operands;
|
|
1397
|
+
const registry = new SourceRegistry(stateDir);
|
|
1398
|
+
if (sub === "list" && rest.length === 0) {
|
|
1399
|
+
const rows = registry.list();
|
|
1400
|
+
if (options.json) return console.log(JSON.stringify(rows, null, 2));
|
|
1401
|
+
return printOutput(renderSourceList(rows, options.columns, Date.now()), options.color);
|
|
1402
|
+
}
|
|
1403
|
+
if (sub !== "add" || rest.length !== 1 || !rest[0]) return fail(SOURCE_USAGE);
|
|
1404
|
+
const target = rest[0], config = projectConfig();
|
|
1405
|
+
const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(target);
|
|
1406
|
+
let bytes: Buffer, origin: string, kind: "file" | "url", absolute: string | undefined;
|
|
1407
|
+
try {
|
|
1408
|
+
if (isUrl) {
|
|
1409
|
+
const fetched = await fetchSourceUrl(target);
|
|
1410
|
+
bytes = fetched.bytes; origin = fetched.finalUrl; kind = "url";
|
|
1411
|
+
} else {
|
|
1412
|
+
absolute = realPath(resolve(cwd, target));
|
|
1413
|
+
const fd = openSync(absolute, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1414
|
+
try {
|
|
1415
|
+
const st = fstatSync(fd);
|
|
1416
|
+
if (!st.isFile()) throw new Error("not a regular file");
|
|
1417
|
+
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`);
|
|
1418
|
+
bytes = Buffer.alloc(st.size); let got = 0;
|
|
1419
|
+
while (got < bytes.length) { const n = readSync(fd, bytes, got, bytes.length - got, got); if (!n) break; got += n; }
|
|
1420
|
+
if (got !== st.size) throw new Error("the file changed while it was read");
|
|
1421
|
+
} finally { closeSync(fd); }
|
|
1422
|
+
origin = absolute; kind = "file";
|
|
1423
|
+
}
|
|
1424
|
+
} catch (error) { return fail(`cannot register ${isUrl ? "that URL" : target}: ${(error as Error).message}`); }
|
|
1425
|
+
const projections = new TextProjections({ cwd, stateDir, config: config.text_projection ?? {}, deny: config.local.deny, readAllow: config.local.read_allow, log: line => console.error(line) });
|
|
1426
|
+
const extension = /\.[A-Za-z0-9]{1,16}$/.exec(isUrl ? new URL(origin).pathname : origin)?.[0] ?? "";
|
|
1427
|
+
const verdict = screenRegistration({ bytes, kind, ...(absolute ? { path: absolute, typedPath: resolve(cwd, target) } : {}) }, {
|
|
1428
|
+
piiPaths: config.pii?.paths ?? [], piiPatterns: currentRouting(cwd).signals.pii_patterns, cwd, matchesPolicy,
|
|
1429
|
+
project: data => extension ? projections.projectBytes(join(cwd, `source${extension.toLowerCase()}`), data)?.text : undefined,
|
|
1430
|
+
});
|
|
1431
|
+
let added;
|
|
1432
|
+
try { added = registry.add({ bytes, kind, origin, private: verdict.private, ...(verdict.text !== undefined ? { text: verdict.text } : {}) }); }
|
|
1433
|
+
catch (error) { return fail(`cannot register ${isUrl ? "that URL" : target}: ${(error as Error).message}`); }
|
|
1434
|
+
const meta = registry.get(added.id);
|
|
1435
|
+
if (!meta) return fail(`the record for ${added.id} cannot be read after it was registered; inspect ${join(stateDir, "sources")}`);
|
|
1436
|
+
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 };
|
|
1437
|
+
if (options.json) return console.log(JSON.stringify(result, null, 2));
|
|
1438
|
+
printOutput(renderSourceAdd(result, options.columns), options.color);
|
|
1439
|
+
},
|
|
1440
|
+
|
|
1441
|
+
schedule: async () => {
|
|
1442
|
+
// #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.
|
|
1443
|
+
// The hub keeps the schedules, their periods and the pause; this process only asks.
|
|
1444
|
+
const split = splitPresentation(args);
|
|
1445
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1446
|
+
const [sub, id, ...extra] = split.operands;
|
|
1447
|
+
const usage = "usage: ahub schedule list | run <id> | pause <id> | resume <id> [--json] [--color=auto|always|never]";
|
|
1448
|
+
if (sub === "list" && id === undefined) return showToolText(data => renderScheduleList(data, present.columns, Date.now()), await taskOp("schedule_list", {}), present);
|
|
1449
|
+
if (!["run", "pause", "resume"].includes(sub ?? "") || !id || extra.length) return fail(usage);
|
|
1450
|
+
if (sub === "run") return showToolText(data => renderScheduleRun(data, present.columns), await taskOp("schedule_run", { id }, false, undefined, 90_000), present);
|
|
1451
|
+
return showToolText(data => renderScheduleChange(sub as "pause" | "resume", data, present.columns, Date.now()), await taskOp(sub === "pause" ? "schedule_pause" : "schedule_resume", { id }), present);
|
|
1452
|
+
},
|
|
1453
|
+
|
|
1454
|
+
learn: async () => {
|
|
1455
|
+
// #514b: the person's verdict on a recurring-criterion candidate (#514a). Person only, end to end: `learn` is a `console` command for an agent shell (classifyPeerCommand has no entry for it, so the
|
|
1456
|
+
// identity layer refuses it before this runs), and the daemon refuses any caller but the person again. `accept` writes ONE key (profile_overrides.review_checklist) to the machine-local
|
|
1457
|
+
// config through the settings writer; nothing in a card's text is written.
|
|
1458
|
+
const split = splitPresentation(args);
|
|
1459
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1460
|
+
const usage = "usage: ahub learn list [--all] | accept <id> | reject <id> [--json] [--color=auto|always|never]";
|
|
1461
|
+
const [sub, ...rest] = split.operands;
|
|
1462
|
+
if (sub === "list" && rest.every(arg => arg === "--all")) {
|
|
1463
|
+
const all = rest.includes("--all");
|
|
1464
|
+
return showToolText(data => renderLearnList(data, present.columns, all), await taskOp("learn_list", { ...(all ? { all: true } : {}) }), present);
|
|
1465
|
+
}
|
|
1466
|
+
const [id, ...extra] = rest;
|
|
1467
|
+
if ((sub !== "accept" && sub !== "reject") || !id || extra.length || !/^[1-9]\d*$/.test(id)) return fail(usage);
|
|
1468
|
+
return showToolText(data => renderLearnChange(sub, data, present.columns), await taskOp(sub === "accept" ? "learn_accept" : "learn_reject", { id: Number(id) }), present);
|
|
1469
|
+
},
|
|
1470
|
+
|
|
1471
|
+
intake: async () => {
|
|
1472
|
+
// #334 (#511a): natural-language intake. Person only, except approve (#933: the configured conductor too, admitted and audited by the daemon).
|
|
1473
|
+
// The request is a draft backlog card that nothing offers until `approve`; the hub, not this process, splits it.
|
|
1474
|
+
const split = splitPresentation(args, { valueFlags: ["--source"] });
|
|
1475
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1476
|
+
const usage = "usage: ahub intake \"<text>\" [--source <id>...] | approve <id> | discard <id> [--json] [--color=auto|always|never]";
|
|
1477
|
+
const sources: string[] = [], words: string[] = [];
|
|
1478
|
+
for (let i = 0; i < split.operands.length; i++) {
|
|
1479
|
+
const arg = split.operands[i]!;
|
|
1480
|
+
if (arg === "--source") { const id = split.operands[++i]; if (!id) return fail("--source needs a source id"); sources.push(id); } else words.push(arg);
|
|
1481
|
+
}
|
|
1482
|
+
const [sub, id] = words;
|
|
1483
|
+
if ((sub === "approve" || sub === "discard") && words.length === 2 && !sources.length) {
|
|
1484
|
+
if (!/^[1-9]\d*$/.test(id!)) return fail(usage);
|
|
1485
|
+
return showToolText(data => renderIntakeChange(sub, data, present.columns), await taskOp(sub === "approve" ? "intake_approve" : "intake_discard", { id: Number(id) }), present);
|
|
1486
|
+
}
|
|
1487
|
+
if (words.length !== 1 || !words[0]!.trim()) return fail(usage);
|
|
1488
|
+
return showToolText(data => renderIntake(data, present.columns), await taskOp("intake", { text: words[0], ...(sources.length ? { sources } : {}) }, false, undefined, 90_000), present);
|
|
1489
|
+
},
|
|
1490
|
+
|
|
1252
1491
|
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).
|
|
1492
|
+
// #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
1493
|
const options = outputOptions();
|
|
1255
1494
|
const { one, rest } = takeFlags(args, ["--task"], []);
|
|
1256
1495
|
const approved = rest.includes("--approved"), check = rest.includes("--check");
|
|
@@ -1415,14 +1654,19 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1415
1654
|
}
|
|
1416
1655
|
if (sub === "merge") {
|
|
1417
1656
|
// #786 T1: the guarded merge of one approved task; a person or the configured conductor asks, the daemon re-reads every gate.
|
|
1418
|
-
|
|
1657
|
+
// #899: the decision prints for a person; `--json` is the same view as a document. The presentation flags are read before the
|
|
1658
|
+
// request, so an invalid --color refuses before a mutation is sent, and they never reach the hub.
|
|
1659
|
+
const split = splitPresentation(args.slice(1)), display = outputOptions(split.flags);
|
|
1660
|
+
const task = Number(split.operands[0]), flags = takeFlags(split.operands.slice(1), [], []);
|
|
1419
1661
|
const dryRun = flags.rest.includes("--dry-run");
|
|
1420
|
-
if (flags.rest.some(arg => arg !== "--dry-run") || !Number.isSafeInteger(
|
|
1662
|
+
if (flags.rest.some(arg => arg !== "--dry-run") || !Number.isSafeInteger(task) || task <= 0) fail("usage: ahub sdlc merge <task> [--dry-run] [--json]");
|
|
1421
1663
|
const hub = await connect();
|
|
1422
1664
|
try {
|
|
1423
|
-
const reply = await hub.request({ t: "sdlc", op: "merge", id, ...(dryRun ? { dryRun: true } : {}) }, 60_000);
|
|
1665
|
+
const reply = await hub.request({ t: "sdlc", op: "merge", id: task, ...(dryRun ? { dryRun: true } : {}) }, 60_000);
|
|
1424
1666
|
if (!reply.ok) fail(reply.error);
|
|
1425
|
-
|
|
1667
|
+
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 } : {}) };
|
|
1668
|
+
if (display.json) console.log(JSON.stringify(view, null, 2));
|
|
1669
|
+
else printOutput(renderSdlcMerge(view, display.columns, Date.now()), display.color);
|
|
1426
1670
|
if (reply.decision === "refused" || reply.ran && reply.result?.state !== "merged") process.exitCode = 1;
|
|
1427
1671
|
} finally { hub.close(); }
|
|
1428
1672
|
return;
|
|
@@ -1481,6 +1725,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1481
1725
|
if (rest.length !== 1) fail("usage: ahub task show <id> [--json] [--full] [--color=auto|always|never]");
|
|
1482
1726
|
return shownTask(renderTaskShow)(taskOp("task_show", { id: rest[0] }));
|
|
1483
1727
|
}
|
|
1728
|
+
if (sub === "claim") {
|
|
1729
|
+
// #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.
|
|
1730
|
+
const { one, rest: positional } = takeFlags(rest, ["--model", "--model-said"], []);
|
|
1731
|
+
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>]");
|
|
1732
|
+
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 } };
|
|
1733
|
+
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 ? ")" : ""}`);
|
|
1734
|
+
}
|
|
1484
1735
|
if (sub === "escalate") return console.log(await taskOp("task_escalate", { id: rest[0] }));
|
|
1485
1736
|
if (sub === "assign") {
|
|
1486
1737
|
const { one, rest: positional } = takeFlags(rest, ["--role", "--revision"], []);
|
|
@@ -1495,14 +1746,18 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1495
1746
|
return console.log(await taskOp("task_unassign", { id: positional[0], role, ...(revision === undefined ? {} : { revision }) }));
|
|
1496
1747
|
}
|
|
1497
1748
|
if (sub === "label") {
|
|
1498
|
-
// #247: a
|
|
1749
|
+
// #247: a later verdict on an approved task, for research records (#933: an agent's own task only, attributed to it).
|
|
1499
1750
|
const { one, rest: positional } = takeFlags(rest, ["--created"], []);
|
|
1500
1751
|
const id = Number(positional[0]), label = positional[1] as Label;
|
|
1501
1752
|
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>]`);
|
|
1753
|
+
// #933: the daemon checks the current board row only, so an agent never names an earlier task with this id from before a reset.
|
|
1754
|
+
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
1755
|
const project = researchProject();
|
|
1756
|
+
// #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.
|
|
1757
|
+
if (identity.role === "tools") await taskOp("task_label_check", { id });
|
|
1503
1758
|
let record: TaskRecord;
|
|
1504
1759
|
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() }]);
|
|
1760
|
+
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
1761
|
return console.log(`task #${id} (approved ${record!.approvedAt}) labelled ${label} in the research records`);
|
|
1507
1762
|
}
|
|
1508
1763
|
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>");
|
|
@@ -1715,7 +1970,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1715
1970
|
return console.log(paint([{ text: result.text }], false));
|
|
1716
1971
|
}
|
|
1717
1972
|
if (sub === "undo") {
|
|
1718
|
-
const view = await request({ t: "settings_get" }), undoDef =
|
|
1973
|
+
const view = await request({ t: "settings_get" }), undoDef = undoableDef(view.undo, settingScope.instances, settingScope.catalog);
|
|
1719
1974
|
// A roster undo of a declared peer needs its confirmation prompt; without readable declarations, refuse rather than skip it.
|
|
1720
1975
|
if (typeof undoDef === "string" && settingScopeError) fail(`pending undo ${String(view.undo)}: ${undoDef}; the project config could not be read: ${settingScopeError}`);
|
|
1721
1976
|
const rosterPeer = typeof undoDef !== "string" && undoDef.group === "Roster" ? undoDef.path[1] : undefined;
|
|
@@ -1831,26 +2086,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1831
2086
|
const json = args.includes("--json");
|
|
1832
2087
|
const [sub, ...rest] = args.filter((a) => a !== "--json");
|
|
1833
2088
|
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
|
-
}
|
|
2089
|
+
const { one, addons, suite, repeat } = benchRunArgs(rest);
|
|
1848
2090
|
await benchPreflight(cwd, loadConfig(cwd).bench.enabled); // the suite's refs are checked once it is read
|
|
1849
2091
|
let interrupted = false;
|
|
1850
2092
|
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
2093
|
process.on("SIGINT", stop);
|
|
1852
2094
|
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));
|
|
2095
|
+
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
2096
|
return console.log(formatBenchReport(benchReport(readRuns().filter((r) => r.header.run === run))).join("\n"));
|
|
1855
2097
|
} finally { process.off("SIGINT", stop); }
|
|
1856
2098
|
}
|
|
@@ -1915,12 +2157,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1915
2157
|
research: () => {
|
|
1916
2158
|
// #247: measures from the opt-in research records; `export` writes them out, `backfill` builds them from events.jsonl.
|
|
1917
2159
|
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
|
-
}
|
|
2160
|
+
researchArgs();
|
|
1924
2161
|
if (sub === "backfill") {
|
|
1925
2162
|
const project = researchProject();
|
|
1926
2163
|
const written = appendRecords(project.id, taskRecords(readEvents(join(stateDir, "events.jsonl")), project.id, { version: VERSION, source: "backfill" }));
|
|
@@ -2084,9 +2321,14 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2084
2321
|
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
2322
|
return p;
|
|
2086
2323
|
};
|
|
2324
|
+
// #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".
|
|
2325
|
+
const unobserved = turn.unobserved ?? [];
|
|
2326
|
+
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
2327
|
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
|
-
|
|
2328
|
+
if (first.undone && !unobserved.length) return console.log(`turn ${id} is already undone: its files are as they were before it started`);
|
|
2329
|
+
if (first.undone) return console.log(`turn ${id}: its files are as they were before it started${unobservedNote}`);
|
|
2330
|
+
// "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.
|
|
2331
|
+
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
2332
|
console.log("(every change made in the project during the turn counts as its own, including any by Claude or by you)");
|
|
2091
2333
|
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
2334
|
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 +2357,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2115
2357
|
await stopProject(project);
|
|
2116
2358
|
console.log("hub stopped");
|
|
2117
2359
|
},
|
|
2360
|
+
// The counterpart of `ahub up`: the same verified graceful stop as `ahub kill`, never a second shutdown path.
|
|
2361
|
+
down: async () => { await commands.kill!(); },
|
|
2118
2362
|
|
|
2119
2363
|
reset: async () => {
|
|
2120
2364
|
if (args.some((a) => !["--all", "--yes"].includes(a))) fail("usage: ahub reset [--all] [--yes]");
|
|
@@ -2235,7 +2479,15 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2235
2479
|
}
|
|
2236
2480
|
if (args.includes("--orphans")) {
|
|
2237
2481
|
if(identity.role==="tools" && args.includes("--kill")) {
|
|
2238
|
-
|
|
2482
|
+
// #812: the conductor's shell asks the hub to stop its own orphan; #899: the answer prints for a person unless --json.
|
|
2483
|
+
const hub=await connect();
|
|
2484
|
+
try {
|
|
2485
|
+
const result=await hub.request({t:"conductor_operational",op:"orphans"},10_000);
|
|
2486
|
+
if(result.ok!==true)fail(String(result.error??"orphan inspection unavailable"),result.refusalAudited===true,result.authorityRefused);
|
|
2487
|
+
if (options.json) console.log(JSON.stringify(result,null,2));
|
|
2488
|
+
else printOutput(renderOrphanCleanup(result, options.columns), options.color);
|
|
2489
|
+
} finally{hub.close();}
|
|
2490
|
+
return;
|
|
2239
2491
|
}
|
|
2240
2492
|
return orphanDoctor(args.includes("--kill"), options);
|
|
2241
2493
|
}
|
|
@@ -2395,20 +2647,26 @@ function inspectDoctorRouting(cwd: string): { routing?: Routing; errors: string[
|
|
|
2395
2647
|
}
|
|
2396
2648
|
|
|
2397
2649
|
async function runConductorCommand(): Promise<void> {
|
|
2650
|
+
if (recoveryRunnerContinuation) return commands[cmd]!();
|
|
2398
2651
|
const operation=conductorOperation(cmd,args);
|
|
2399
2652
|
if(operation) {
|
|
2400
2653
|
const unavailable=`conductor authorization unavailable; nothing was executed; ${OPERATIONAL_PERSON_GUIDANCE}`;
|
|
2401
2654
|
const descriptor=readControl(stateDir);
|
|
2655
|
+
// Offline resume proves the source-issued local continuation inside the command; no console connection is borrowed.
|
|
2656
|
+
if (conductorPeer && cmd === "recovery" && (!descriptor?.pid || !descriptor.pidSignature || processLiveness(descriptor.pid, descriptor.pidSignature) !== "live")) return commands.recovery!();
|
|
2402
2657
|
if(!descriptor?.instanceId || !descriptor.projectId || !descriptor.pid || !descriptor.pidSignature || processLiveness(descriptor.pid,descriptor.pidSignature)!=="live")fail(unavailable);
|
|
2403
2658
|
let hub:ControlClient;
|
|
2404
2659
|
try {hub=await connect(3000);} catch {fail(unavailable);}
|
|
2405
2660
|
let release:(()=>void)|undefined;
|
|
2406
2661
|
try {
|
|
2407
2662
|
let result:any;
|
|
2408
|
-
|
|
2663
|
+
// #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.
|
|
2664
|
+
if(conductorLocalOperation(operation))LOCAL_OPERATION_ARGS[operation]?.();
|
|
2665
|
+
try {result=await hub.request({t:"conductor_operational",op:conductorLocalOperation(operation)?"admit":"authorize",command:operation},3000);} catch {fail(unavailable);}
|
|
2409
2666
|
if(result.ok!==true)fail(result.authorityRefused===true || result.authorityRefused===false?String(result.error??unavailable):unavailable,result.refusalAudited===true,result.authorityRefused);
|
|
2410
2667
|
if(!hub.instanceId)fail(unavailable);
|
|
2411
|
-
|
|
2668
|
+
// Recovery replaces its source instance. Its own per-project proofs fence every later connection.
|
|
2669
|
+
if (!conductorPeer || !["restart", "upgrade", "recovery"].includes(cmd)) release=ControlClient.pinInstance(stateDir,hub.instanceId);
|
|
2412
2670
|
await commands[cmd]!();
|
|
2413
2671
|
} finally {release?.();hub.close();}
|
|
2414
2672
|
return;
|
|
@@ -2430,11 +2688,14 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2430
2688
|
} else {
|
|
2431
2689
|
// Native TUIs return a human launch command through the daemon; an agent shell never executes it.
|
|
2432
2690
|
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) });
|
|
2691
|
+
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances, peerConfig.declaredExternalInstances);
|
|
2692
|
+
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
2693
|
if (selection.sessionId) fail("conductor starts accept no session override; a person starts a configured ACP session");
|
|
2436
2694
|
const hub = await connect();
|
|
2437
2695
|
try {
|
|
2696
|
+
const external = selection.peer !== cmd && (peerConfig.declaredExternalInstances?.some(row => row.id === selection.peer && row.kind === cmd)
|
|
2697
|
+
|| projectCatalog.externalDefinitions.some(definition => definition.kind === cmd));
|
|
2698
|
+
if (external) fail("external module instances require an explicit person start");
|
|
2438
2699
|
await requireActiveConfiguredAcp(hub, cmd, selection.peer, text => console.error(text));
|
|
2439
2700
|
console.log(await taskOp("hub_peer_start", { peer: selection.peer, mode: "headless" }, false, hub, 70_000));
|
|
2440
2701
|
} finally { hub.close(); }
|
|
@@ -2451,9 +2712,9 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2451
2712
|
console.log(await taskOp(op, input));
|
|
2452
2713
|
}
|
|
2453
2714
|
const runConfigured = configuredCommand ? async () => {
|
|
2454
|
-
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances);
|
|
2715
|
+
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances, peerConfig.declaredExternalInstances);
|
|
2455
2716
|
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 });
|
|
2717
|
+
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
2718
|
} : undefined;
|
|
2458
2719
|
const run = identity.role === "tools" && commandAccess === "conductor" ? runConductorCommand : (Object.hasOwn(commands, cmd) ? commands[cmd] : undefined) ?? runConfigured ?? (() => fail(`unknown command "${cmd}"; run ahub help`));
|
|
2459
2720
|
try {
|