@staix/agent-hub 0.24.0 → 0.26.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 +23 -0
- package/README.md +35 -8
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +2 -1
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +18 -3
- package/docs/agent-notes/tasks.md +12 -4
- package/docs/agent-notes/tests.md +3 -1
- package/docs/dogfood.md +16 -2
- package/docs/memory.md +1 -1
- package/docs/operations.md +264 -14
- package/docs/profiles.md +2 -0
- package/docs/quickstart.md +23 -6
- package/docs/security.md +198 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +25 -1
- package/docs/specs/2026-09-19-multi-project-design.md +12 -1
- package/docs/specs/2026-11-01-research-campaigns-design.md +354 -0
- package/docs/verification/2026-11-01-0.25.0.md +91 -0
- package/docs/verification/2026-11-01-0.26.0.md +54 -0
- package/docs/verified.json +45 -17
- package/package.json +4 -3
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +5294 -4797
- package/src/adapters/channel-peer.ts +4 -2
- package/src/adapters/claude-channel.ts +29 -6
- package/src/adapters/definitions/builtin-metadata.ts +6 -6
- package/src/adapters/definitions/native-cli-doctor.ts +30 -0
- package/src/adapters/definitions/qwen.ts +2 -0
- package/src/adapters/jcode.ts +3 -0
- package/src/adapters/registry-types.ts +2 -0
- package/src/adapters/registry.ts +2 -1
- package/src/cli/acon-register.ts +29 -0
- package/src/cli/conductor-recovery.ts +3 -2
- package/src/cli/handoff-args.ts +27 -0
- package/src/cli/handoff-output.ts +25 -0
- package/src/cli/help.ts +11 -8
- package/src/cli/identity.ts +7 -1
- package/src/cli/main.ts +138 -30
- package/src/cli/output.ts +8 -5
- package/src/cli/recovery-package.ts +94 -4
- package/src/cli/reset.ts +14 -4
- package/src/cli/runtime-version-warning.ts +28 -0
- package/src/cli/schedule-output.ts +8 -4
- package/src/cli/start-agents-run.ts +125 -0
- package/src/cli/study-output.ts +71 -0
- package/src/cli/study.ts +58 -0
- package/src/cli/task-output.ts +6 -0
- package/src/cli/upgrade-interactive.ts +4 -4
- package/src/cli/upgrade-runtime.ts +105 -57
- package/src/cli/upgrade.ts +90 -28
- package/src/hub/autonomy-defaults.ts +32 -7
- package/src/hub/board.ts +993 -8
- package/src/hub/budget.ts +3 -4
- package/src/hub/channel-project.ts +61 -0
- package/src/hub/child-process.ts +4 -3
- package/src/hub/conductor.ts +3 -4
- package/src/hub/config-apply.ts +13 -2
- package/src/hub/config-edit-host.ts +1 -0
- package/src/hub/config-tier.ts +3 -0
- package/src/hub/config-trust.ts +10 -2
- package/src/hub/control-client.ts +39 -10
- package/src/hub/daemon.ts +326 -25
- package/src/hub/db-schema.ts +143 -0
- package/src/hub/delivery-journal.ts +3 -4
- package/src/hub/envelope.ts +2 -0
- package/src/hub/events.ts +4 -1
- package/src/hub/execution-budget.ts +15 -4
- package/src/hub/folder-trigger.ts +3 -4
- package/src/hub/handoff.ts +126 -0
- package/src/hub/hub-tools.ts +12 -2
- package/src/hub/invocations.ts +3 -4
- package/src/hub/lifecycle.ts +87 -29
- package/src/hub/manager.ts +110 -87
- package/src/hub/no-writes.ts +1 -1
- package/src/hub/orca-env.ts +19 -0
- package/src/hub/path-policy.ts +12 -8
- package/src/hub/project-runtime-state.ts +139 -0
- package/src/hub/project-runtime.ts +62 -0
- package/src/hub/project.ts +14 -5
- package/src/hub/protocol-window.ts +19 -0
- package/src/hub/provenance.ts +30 -0
- package/src/hub/recovery-store.ts +102 -12
- package/src/hub/registry.ts +5 -0
- package/src/hub/research-campaign.ts +265 -0
- package/src/hub/research-experiments.ts +474 -0
- package/src/hub/research-runner.ts +172 -0
- package/src/hub/restart.ts +35 -0
- package/src/hub/runtime-capture-prune.ts +415 -0
- package/src/hub/runtime-prune-health.ts +112 -0
- package/src/hub/schedules.ts +213 -13
- package/src/hub/sdlc-merge.ts +7 -0
- package/src/hub/sdlc-queue.ts +13 -1
- package/src/hub/session-identity.ts +49 -0
- package/src/hub/settings.ts +4 -0
- package/src/hub/snapshots.ts +3 -4
- package/src/hub/start-agents.ts +44 -0
- package/src/hub/task-approval.ts +23 -0
- package/src/hub/tasks.ts +1005 -34
- package/src/hub/ui.ts +15 -6
- package/src/hub/usage-rollup.ts +3 -4
- package/src/memory/capture.ts +2 -2
- package/src/pi/tool-receipts.ts +3 -4
- package/src/ui/index.html +5 -3
- package/src/ui/office-space.ts +118 -0
- package/src/ui/plugin.ts +293 -0
- package/templates/AGENTS.block.md +1 -0
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +4 -4
- package/templates/profiles/writing.json +1 -1
- package/templates/roles/research/analyst.md +1 -0
- package/templates/roles/research/fact-checker.md +1 -0
- package/templates/roles/research/research-lead.md +1 -0
- package/templates/roles/research/researcher.md +1 -0
- package/templates/roles/research/statistician.md +1 -0
package/src/cli/main.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { assertConductorOperationLocal, authorizeConductorOperation, authorizeConductorLaunch, issueConductorOperation, preflightConductorRecovery, retireConductorOperation } from "./conductor-recovery.ts";
|
|
3
|
+
import { runtimePruneHealth } from "../hub/runtime-prune-health.ts";
|
|
3
4
|
import { conductorLocalOperation, conductorOperation, OPERATIONAL_PERSON_GUIDANCE } from "../hub/operational-authority.ts";
|
|
4
5
|
import { sayRecipients } from "./say-input.ts";
|
|
5
6
|
import type { DeliverableRow } from "../hub/tasks.ts";
|
|
@@ -16,7 +17,7 @@ import { readInstanceRoleConfig, readInstanceConfig, type InstanceRoleConfig } f
|
|
|
16
17
|
import { staleInstanceKey } from "../hub/roster.ts";
|
|
17
18
|
import { parseConfiguredAcpArguments, runConfiguredAcpEntry, requireActiveConfiguredAcp } from "./configured-acp-entry.ts";
|
|
18
19
|
import type { NativeCliEntryContext } from "../adapters/definitions/native-cli-entry-types.ts";
|
|
19
|
-
import { BUILTIN_CLI_ENTRIES, createProjectAdapterCatalog, definitionOf, DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "../adapters/registry.ts";
|
|
20
|
+
import { BUILTIN_CLI_ENTRIES, createProjectAdapterCatalog, definitionOf, DOCTOR_TOOL_PROBES, DOCTOR_MEMORY_CAPTURE_PEERS, DOCTOR_PLUGIN_PROBE, nativeDoctorCheck, nativeMemoryCaptureRow, type MemorySetupReading, nativeDoctorRecoveryIdentity, nativeDoctorOptionalProbes } from "../adapters/registry.ts";
|
|
20
21
|
import { parsePiFlags } from "../adapters/definitions/native-cli-pi.ts";
|
|
21
22
|
import { supportsBoardReleaseFilter, supportsBoardColumnMetadata } from "./board-read.ts";
|
|
22
23
|
import { matchesPolicy } from "../hub/path-policy.ts";
|
|
@@ -28,6 +29,10 @@ import { renderScheduleChange, renderScheduleList, renderScheduleRun } from "./s
|
|
|
28
29
|
import { renderIntake, renderIntakeChange } from "./intake-output.ts";
|
|
29
30
|
import { renderLearnChange, renderLearnList } from "./learn-output.ts";
|
|
30
31
|
import { renderCapabilities } from "./capabilities-output.ts";
|
|
32
|
+
import { parseStudyArgs } from "./study.ts";
|
|
33
|
+
import { renderStudyList, renderStudyReceipt, renderStudyShow } from "./study-output.ts";
|
|
34
|
+
import { renderHandoffReceipt, renderHandoffs } from "./handoff-output.ts";
|
|
35
|
+
import { parseHandoffArgs } from "./handoff-args.ts";
|
|
31
36
|
import { terminalText } from "./console-state.ts";
|
|
32
37
|
import { parseSettingsEditArgs, runSettingsEdit, SETTINGS_EDIT_USAGE } from "./settings-edit.ts";
|
|
33
38
|
import { autonomyEnvironment, runAutonomy } from "./autonomy.ts";
|
|
@@ -37,6 +42,7 @@ import { offlineAutonomyStatus } from "./autonomy-offline.ts";
|
|
|
37
42
|
import { autonomyHomeStatus, usedClaudeHomes } from "./autonomy-homes.ts";
|
|
38
43
|
import { discoverClaudeHomes } from "./claude-homes.ts";
|
|
39
44
|
import { setupClaudeHomes } from "./setup-homes.ts";
|
|
45
|
+
import { registerAconPlugin } from "./acon-register.ts";
|
|
40
46
|
import { petView, readPet, renderPet } from "./pet.ts";
|
|
41
47
|
import { startPetWindow, closePetWindow } from "../pet/window.ts";
|
|
42
48
|
import { loadPetConfig, savePetPalette } from "./pet-config.ts";
|
|
@@ -51,7 +57,7 @@ import { parseSettingText, settingDef, settingText, undoableDef, type SettingRow
|
|
|
51
57
|
import { startModeOf } from "../hub/start-mode.ts";
|
|
52
58
|
import { currentRouting, OVERLAY_FILE, parseOverlay, parseRouting, routingText, type Routing, type RoutingOverlay } from "../hub/routing.ts";
|
|
53
59
|
import { spawn, spawnSync } from "node:child_process";
|
|
54
|
-
import { chmodSync, closeSync, constants, existsSync, fstatSync, openSync, readFileSync, readSync, renameSync, writeFileSync } from "node:fs";
|
|
60
|
+
import { chmodSync, closeSync, constants, existsSync, fstatSync, openSync, readFileSync, readSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
55
61
|
import { randomUUID } from "node:crypto";
|
|
56
62
|
import { homedir } from "node:os";
|
|
57
63
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
@@ -79,11 +85,13 @@ import { childEnv, personEnv } from "../hub/child-process.ts";
|
|
|
79
85
|
import { abortRecovery, createOperation, disposeRecovery, recordRecoveryDelegation, repairedFreshSession, reattachRecovery, liveProjects, nextChoices, nextActionsText, publicOperation, recoveryCommand, registeredProjects, runRecovery, type RecoveryOperation, type Inspection } from "./upgrade.ts";
|
|
80
86
|
import { endPlannedPeer, makeRecoveryDriver, makeUpgradePlan, preserveSource } from "./upgrade-runtime.ts";
|
|
81
87
|
import { exactVersion, latestRelease, newerVersion } from "./recovery-package.ts";
|
|
88
|
+
import { runtimeVersionWarnings } from "./runtime-version-warning.ts";
|
|
82
89
|
import { operationScreen, planScreen, type ScreenIO, type UpgradeHost } from "./upgrade-interactive.ts";
|
|
83
90
|
import { readRecoveryPresentationFlags } from "./upgrade-presentation-flags.ts";
|
|
84
91
|
import { renderOperation, renderBlocked, renderSummary, type RecoveryPresentationOptions } from "./upgrade-presentation.ts";
|
|
85
92
|
import { renderBudgetResult, renderRecoveryStatus, renderUpgradePlan } from "./upgrade-output.ts";
|
|
86
93
|
import { recordTerminalLaunch, prepareTerminalLaunch, registerTerminalLaunch, type TerminalPeer } from "./terminal-recovery.ts";
|
|
94
|
+
import { effectiveStartAgents, inheritMachineStart, setupStartAgents, startProjectAgents } from "./start-agents-run.ts";
|
|
87
95
|
import { ensureMlx, inspectMlx, stopMlx } from "../models/mlx.ts";
|
|
88
96
|
import { setupOllamaModel } from "./models-setup.ts";
|
|
89
97
|
|
|
@@ -137,6 +145,16 @@ while (argv[0] === "--project" || argv[0] === "--live-test") {
|
|
|
137
145
|
if (!selector || selector.startsWith("--")) fail("--project needs a path or project ID");
|
|
138
146
|
}
|
|
139
147
|
}
|
|
148
|
+
// Upgrade accepts its documented command-local selector as well as the leading global form.
|
|
149
|
+
if (argv[0] === "upgrade") {
|
|
150
|
+
const at = argv.indexOf("--project", 1);
|
|
151
|
+
if (at >= 0) {
|
|
152
|
+
if (selector !== undefined || argv.indexOf("--project", at + 1) >= 0) fail("--project may appear only once");
|
|
153
|
+
selector = argv[at + 1];
|
|
154
|
+
if (!selector || selector.startsWith("--")) fail("--project needs a path or project ID");
|
|
155
|
+
argv.splice(at, 2);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
140
158
|
const [given = "help", ...rest] = argv;
|
|
141
159
|
let selected: { root: string; stateDir: string };
|
|
142
160
|
try {
|
|
@@ -380,10 +398,10 @@ function matchingProject(): Project | undefined {
|
|
|
380
398
|
finally { registry.close(); }
|
|
381
399
|
}
|
|
382
400
|
const hubManifest = () => !!readControl(stateDir) || existsSync(join(stateDir, "hub.pid"));
|
|
383
|
-
/** A reset is never part of an upgrade or recovery:
|
|
401
|
+
/** A reset is never part of an upgrade or recovery: this project's or a legacy machine hold refuses it, whatever
|
|
384
402
|
* AGENTHUB_RECOVERY_OPERATION says (assertLifecycleAvailable lets that operation's own processes through). */
|
|
385
403
|
function resetLockFree(): void {
|
|
386
|
-
const owner = recoveryLock();
|
|
404
|
+
const owner = recoveryLock(undefined, cwd);
|
|
387
405
|
if (owner) throw new Error(`${activeOperation(owner)}; nothing was changed`);
|
|
388
406
|
}
|
|
389
407
|
/** A manifest whose daemon is alive or uncertain, by inspectProject's rule; a crashed daemon's leftovers do not count. */
|
|
@@ -675,11 +693,11 @@ async function onTerminal(screens: (io: ScreenIO) => Promise<void>, presentation
|
|
|
675
693
|
}
|
|
676
694
|
|
|
677
695
|
function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost {
|
|
678
|
-
const scope =
|
|
696
|
+
const scope = cwd;
|
|
679
697
|
return {
|
|
680
698
|
plan: () => makeUpgradePlan(kind, version, scope),
|
|
681
699
|
apply: async (plan, interrupted) => {
|
|
682
|
-
assertLifecycleAvailable();
|
|
700
|
+
assertLifecycleAvailable(cwd);
|
|
683
701
|
const current = await makeUpgradePlan(kind, plan.version, scope);
|
|
684
702
|
if (current.fingerprint !== plan.fingerprint) throw new Error("the plan changed during review; review it again");
|
|
685
703
|
// The check above takes seconds: a Ctrl+C in that time is still a no.
|
|
@@ -688,7 +706,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
688
706
|
spawnRecovery(operation, true);
|
|
689
707
|
return operation;
|
|
690
708
|
},
|
|
691
|
-
lock: recoveryLock,
|
|
709
|
+
lock: (projectRoot = cwd) => recoveryLock(undefined, projectRoot),
|
|
692
710
|
read: (id) => readOperation<RecoveryOperation>(id),
|
|
693
711
|
runner: recoveryRunner,
|
|
694
712
|
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect),
|
|
@@ -704,8 +722,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
704
722
|
async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
705
723
|
const display = recoveryDisplay();
|
|
706
724
|
const { one, rest } = takeFlags(display.args, ["--to"], []);
|
|
707
|
-
if (rest.some((a) => !["--dry-run", "--yes"].includes(a)) || (kind === "restart" && one["--to"])) fail("usage: ahub upgrade [--to <version>] [--dry-run] [--yes] | ahub restart [--dry-run] [--yes]");
|
|
708
|
-
if (kind === "upgrade" && selector) fail("upgrade changes the shared package/plugin; omit --project to review all affected running projects");
|
|
725
|
+
if (rest.some((a) => !["--dry-run", "--yes"].includes(a)) || (kind === "restart" && one["--to"])) fail("usage: ahub upgrade [--project <path|id>] [--to <version>] [--dry-run] [--yes] | ahub restart [--dry-run] [--yes]");
|
|
709
726
|
const yes = rest.includes("--yes"), dry = rest.includes("--dry-run") || (display.json && !yes);
|
|
710
727
|
// #272: on a terminal, with neither flag, the plan is reviewed, applied and followed in one session.
|
|
711
728
|
const interactive = !dry && !yes && humanRecovery(display.json);
|
|
@@ -720,7 +737,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
720
737
|
// An older CLI must not coordinate a newer target (docs/operations.md): the target's own coordinator takes over.
|
|
721
738
|
if (conductorPeer && kind === "upgrade" && newerVersion(to!, VERSION)) fail("a supporting target coordinator is required; the person runs it in ahub console or a terminal");
|
|
722
739
|
if (kind === "upgrade" && newerVersion(to!, VERSION) && process.env.AGENTHUB_UPGRADE_HANDOVER !== "1") {
|
|
723
|
-
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", ...liveTestArgs, "upgrade", "--to", to!, ...rest, ...display.flags, ...(dry && !rest.includes("--dry-run") ? ["--dry-run"] : [])];
|
|
740
|
+
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", "--project", cwd, ...liveTestArgs, "upgrade", "--to", to!, ...rest, ...display.flags, ...(dry && !rest.includes("--dry-run") ? ["--dry-run"] : [])];
|
|
724
741
|
console.error(`ahub: ${to} is newer than this CLI (${VERSION}), so its own coordinator runs the upgrade: bun ${argv.join(" ")}`);
|
|
725
742
|
if (interactive) {
|
|
726
743
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -738,7 +755,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
738
755
|
}
|
|
739
756
|
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");
|
|
740
757
|
if (interactive) return onTerminal((io) => planScreen(upgradeHost(kind, to ?? VERSION), io), display.options);
|
|
741
|
-
const plan = await makeUpgradePlan(kind, to ?? VERSION,
|
|
758
|
+
const plan = await makeUpgradePlan(kind, to ?? VERSION, cwd, undefined, conductorPeer);
|
|
742
759
|
if (conductorPeer) await preflightConductorRecovery(plan, conductorPeer);
|
|
743
760
|
const jsonApply = display.json && yes && !dry;
|
|
744
761
|
// #814: a person reading a pipe or a dry run gets the plan screen's rows, with every blocker, reconnect-only session and
|
|
@@ -752,7 +769,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
752
769
|
if (display.json) for (const blocker of blockers) console.error(`ahub: blocker: ${blocker}`);
|
|
753
770
|
if (dry) return;
|
|
754
771
|
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"); }
|
|
755
|
-
assertLifecycleAvailable();
|
|
772
|
+
assertLifecycleAvailable(cwd);
|
|
756
773
|
if (!yes) {
|
|
757
774
|
if (!process.stdin.isTTY) fail("review --dry-run and use --yes in non-interactive sessions");
|
|
758
775
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -760,14 +777,14 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
760
777
|
if (!/^(y|yes)$/i.test((await rl.question("Apply this project list and restore its sessions? [y/N] ")).trim())) return;
|
|
761
778
|
} finally { rl.close(); }
|
|
762
779
|
}
|
|
763
|
-
const current = await makeUpgradePlan(kind, plan.version,
|
|
780
|
+
const current = await makeUpgradePlan(kind, plan.version, cwd, undefined, conductorPeer);
|
|
764
781
|
if (current.fingerprint !== plan.fingerprint) fail("plan changed during review; run the command again");
|
|
765
782
|
const routingDefaults = conductorPeer ? await preflightConductorRecovery(plan, conductorPeer) : undefined;
|
|
766
783
|
const sourceRoot = preserveSource(plan);
|
|
767
784
|
let operation: RecoveryOperation;
|
|
768
785
|
if (conductorPeer) {
|
|
769
786
|
const id = randomUUID();
|
|
770
|
-
const pending: RecoveryOperation = { schema:
|
|
787
|
+
const pending: RecoveryOperation = { schema: 3, 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: {} })) };
|
|
771
788
|
const target = await makeRecoveryDriver().stage(pending);
|
|
772
789
|
pending.targetRoot = target.root; pending.targetDigest = target.digest; pending.targetFeatures = target.targetFeatures;
|
|
773
790
|
await issueConductorOperation(pending, conductorPeer);
|
|
@@ -863,9 +880,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
863
880
|
printRecovery(renderBlocked(result, nextChoices(result, undefined, live), display.options));
|
|
864
881
|
return;
|
|
865
882
|
}
|
|
866
|
-
// #272: without an id,
|
|
883
|
+
// #272: without an id, this project's hold (or a retained legacy machine hold); bare on a terminal, its screen.
|
|
867
884
|
const named = !!given && operationIdPattern.test(given);
|
|
868
|
-
const id = named ? given : recoveryLock();
|
|
885
|
+
const id = named ? given : recoveryLock(undefined, cwd);
|
|
869
886
|
const rest = named || given === undefined ? more : [given, ...more];
|
|
870
887
|
if (action === undefined && humanRecovery(display.json)) {
|
|
871
888
|
if (!id) return console.log("no recovery operation is open");
|
|
@@ -1004,27 +1021,42 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1004
1021
|
},
|
|
1005
1022
|
|
|
1006
1023
|
manager: async () => {
|
|
1007
|
-
|
|
1024
|
+
// #608: the dashboard's "start" counts as the person's `ahub up`: the manager starts the project's default agent set after the hub is ready.
|
|
1025
|
+
const manager = await startManager({ lifecycle, afterStart: async (project) => {
|
|
1026
|
+
const chosen = effectiveStartAgents(project.root, loadConfig(project.root).start.agents, hubHome(), [project.root]);
|
|
1027
|
+
return startProjectAgents(project, chosen.agents);
|
|
1028
|
+
} });
|
|
1008
1029
|
for (const signal of ["SIGINT", "SIGTERM"] as const) process.on(signal, () => void manager.stop());
|
|
1009
1030
|
await manager.stopped;
|
|
1010
1031
|
},
|
|
1011
1032
|
|
|
1012
1033
|
setup: async () => {
|
|
1013
1034
|
recoveryLauncherCurrent?.();
|
|
1014
|
-
assertLifecycleAvailable();
|
|
1035
|
+
assertLifecycleAvailable(cwd);
|
|
1015
1036
|
const root = join(import.meta.dir, "..", "..");
|
|
1037
|
+
const setupFlags = takeFlags(args.filter(arg => arg !== "--yes"), ["--agents"], []);
|
|
1038
|
+
if (setupFlags.rest.length) fail("usage: ahub setup [--yes] [--agents <a,b>]");
|
|
1016
1039
|
const registry = new Registry(), rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined;
|
|
1017
1040
|
try {
|
|
1018
1041
|
const found = recoveryLauncherCurrent || delegatedPeer ? await discoverClaudeHomes({ home: homedir(), env: process.env }) : await usedClaudeHomes(registry, homedir(), process.env);
|
|
1019
1042
|
for (const notice of found.notices) console.log(terminalText(notice));
|
|
1020
1043
|
await setupClaudeHomes({ homes: found.homes, env: process.env, packageRoot: root, yes: args.includes("--yes"),
|
|
1021
1044
|
interactive: !!rl,
|
|
1022
|
-
current: () => { recoveryLauncherCurrent?.(); assertLifecycleAvailable(); },
|
|
1045
|
+
current: () => { recoveryLauncherCurrent?.(); assertLifecycleAvailable(cwd); },
|
|
1023
1046
|
print: line => console.log(terminalText(line)), ask: async prompt => rl ? rl.question(prompt) : null,
|
|
1024
1047
|
probe: (argv, options) => { recoveryLauncherCurrent?.(); const result = spawnSync(argv[0]!, argv.slice(1), { encoding: "utf8", env: options.env, timeout: options.timeoutMs, maxBuffer: 1024 * 1024 }); recoveryLauncherCurrent?.(); return { status: result.status ?? -1, stdout: result.stdout ?? "" }; },
|
|
1025
1048
|
run: async (argv, env) => { recoveryLauncherCurrent?.(); const result = spawnSync(argv[0]!, argv.slice(1), { stdio: "inherit", env, timeout: 30_000 }); recoveryLauncherCurrent?.(); return result.status ?? -1; },
|
|
1026
1049
|
});
|
|
1050
|
+
// #608: the one question about the default agent set, after the Claude homes and before doctor.
|
|
1051
|
+
if (!recoveryLauncherCurrent && !delegatedPeer) {
|
|
1052
|
+
const refused = await setupStartAgents({ home: hubHome(), roots: [...registry.list().map(row => row.root), cwd], ...(setupFlags.one["--agents"] !== undefined ? { flag: setupFlags.one["--agents"] } : {}), yes: args.includes("--yes"),
|
|
1053
|
+
...(rl ? { ask: async (prompt: string) => { try { return await rl.question(prompt); } catch { return null; } } } : {}), print: line => console.log(terminalText(line)) });
|
|
1054
|
+
if (refused) fail(refused);
|
|
1055
|
+
}
|
|
1027
1056
|
} finally { registry.close(); rl?.close(); }
|
|
1057
|
+
// #935: register the acon console plugin (acon reads it at its next start; no acon install is needed for this).
|
|
1058
|
+
const acon = registerAconPlugin(hubHome(), join(root, "src", "ui", "plugin.ts"));
|
|
1059
|
+
console.log("error" in acon ? `acon plugin not registered: ${acon.error}` : acon.changed ? `acon plugin registered: ${acon.path}` : `acon plugin up to date: ${acon.path}`);
|
|
1028
1060
|
if (recoveryLauncherCurrent) recoveryLauncherCurrent();
|
|
1029
1061
|
else await commands.doctor!();
|
|
1030
1062
|
},
|
|
@@ -1042,7 +1074,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1042
1074
|
if (defaults.notice) console.error(defaults.notice);
|
|
1043
1075
|
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");
|
|
1044
1076
|
}
|
|
1045
|
-
assertLifecycleAvailable();
|
|
1077
|
+
assertLifecycleAvailable(cwd);
|
|
1046
1078
|
if (newProject && delegatedPeer && readAutonomyDefaults(hubHome(), [cwd]).enabled) fail("machine autonomy inheritance is person-only; delegated live tests cannot apply it");
|
|
1047
1079
|
const changed = init(cwd, requestedProfile, args.includes("--shadow"), args.includes("--release-guard"));
|
|
1048
1080
|
const project = registeredProject();
|
|
@@ -1051,6 +1083,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1051
1083
|
const registry = new Registry();
|
|
1052
1084
|
try { for (const line of await inheritMachineAutonomy(project, hubHome(), autonomyEnvironment(process.env, !!process.stdin.isTTY), registry)) console.log(terminalText(line)); }
|
|
1053
1085
|
finally { registry.close(); }
|
|
1086
|
+
// #608: the machine's default agent set, copied the same way (person-only: a delegated live test never inherits it).
|
|
1087
|
+
if (!delegatedPeer) { const roots = (() => { const r = new Registry(); try { return r.list().map(row => row.root); } finally { r.close(); } })(); for (const line of inheritMachineStart(cwd, hubHome(), [...roots, cwd])) console.log(terminalText(line)); }
|
|
1054
1088
|
}
|
|
1055
1089
|
const config = projectConfig(), examples = resolveProfile(config.profile, config.profile_overrides).example_checks;
|
|
1056
1090
|
if (examples.length) console.log(terminalText("Example checks (advisory):\n" + examples.map(example => "- " + example).join("\n")));
|
|
@@ -1078,12 +1112,20 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1078
1112
|
if (recoveryLauncherCurrent) { recoveryLauncherCurrent(); return; }
|
|
1079
1113
|
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
1080
1114
|
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
1115
|
+
// #608: the default agent set, only for a person's own `ahub up` (never an agent's, a live test's, or an upgrade or recovery relaunch, which set AGENTHUB_RECOVERY_OPERATION).
|
|
1116
|
+
if (!args.includes("--no-agents") && !liveTest && caller.role === "console" && identity.role === "console" && !process.env.AGENTHUB_RECOVERY_OPERATION) {
|
|
1117
|
+
try {
|
|
1118
|
+
const chosen = effectiveStartAgents(cwd, loadConfig(cwd).start.agents, hubHome(), [cwd]);
|
|
1119
|
+
if (chosen.notice) console.error(chosen.notice);
|
|
1120
|
+
for (const line of await startProjectAgents({ root: cwd, stateDir }, chosen.agents)) console.log(terminalText(line));
|
|
1121
|
+
} catch (error) { console.error(`default agents were not started: ${terminalText((error as Error).message)}`); }
|
|
1122
|
+
}
|
|
1081
1123
|
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir, liveTest });
|
|
1082
1124
|
},
|
|
1083
1125
|
|
|
1084
1126
|
...Object.fromEntries(BUILTIN_CLI_ENTRIES.map(entry => [entry.command, async () => {
|
|
1085
1127
|
// Preserve the original guard before native effects; previews remain read-only and precede this guard.
|
|
1086
|
-
if (entry.lifecycleAdmission && !args.includes("--print-command") && !args.includes("--dry-run")) assertLifecycleAvailable();
|
|
1128
|
+
if (entry.lifecycleAdmission && !args.includes("--print-command") && !args.includes("--dry-run")) assertLifecycleAvailable(cwd);
|
|
1087
1129
|
const nativeConnect = async () => {
|
|
1088
1130
|
const hub = await connectLaunch();
|
|
1089
1131
|
try { if (selectedInstance) { requirePeerInstances(hub.features); requireActiveInstance((await hub.request({ t: "status" })).status, selectedInstance); } return hub; }
|
|
@@ -1457,6 +1499,40 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1457
1499
|
return showToolText(data => renderScheduleChange(sub as "pause" | "resume", data, present.columns, Date.now()), await taskOp(sub === "pause" ? "schedule_pause" : "schedule_resume", { id }), present);
|
|
1458
1500
|
},
|
|
1459
1501
|
|
|
1502
|
+
study: async () => {
|
|
1503
|
+
// #601: research campaigns. The hub keeps every campaign and decides every rule (Tasks.study*); this process parses argv, reads the contract file the person names and asks.
|
|
1504
|
+
// Person only for draft, revise, approve and close (a console control op the daemon refuses on any other connection, and `study` has no agent form in classifyPeerCommand besides the
|
|
1505
|
+
// read-only `show`); an agent drafts through the hub_study_* tools. `show` is a read: for a private campaign, task or source the hub sends ids, counts and states only.
|
|
1506
|
+
const split = splitPresentation(args, { edges: true });
|
|
1507
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1508
|
+
const parsed = parseStudyArgs(split.operands);
|
|
1509
|
+
if ("usage" in parsed) return fail(parsed.usage);
|
|
1510
|
+
const contract = (file: string): unknown => {
|
|
1511
|
+
// One descriptor decides everything: opened non-blocking and without following a final symlink (a FIFO would otherwise block the open), checked to be a regular file with fstat, and
|
|
1512
|
+
// read for at most the cap plus one byte, so a file that grows or is replaced after the check, or a device that reports size zero, cannot exhaust memory or hang this process.
|
|
1513
|
+
const cap = 64 * 1024;
|
|
1514
|
+
let text: string;
|
|
1515
|
+
try {
|
|
1516
|
+
const fd = openSync(resolve(invokedFrom, file), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1517
|
+
try {
|
|
1518
|
+
if (!fstatSync(fd).isFile()) return fail("the contract file is not a regular file");
|
|
1519
|
+
const bytes = Buffer.alloc(cap + 1); let got = 0;
|
|
1520
|
+
while (got < bytes.length) { const n = readSync(fd, bytes, got, bytes.length - got, null); if (!n) break; got += n; }
|
|
1521
|
+
if (got > cap) return fail("the contract file is over 64 KiB");
|
|
1522
|
+
text = bytes.subarray(0, got).toString("utf8");
|
|
1523
|
+
} finally { closeSync(fd); }
|
|
1524
|
+
}
|
|
1525
|
+
catch { return fail("cannot read the contract file"); }
|
|
1526
|
+
try { return JSON.parse(text); } catch { return fail("the contract file is not valid JSON"); }
|
|
1527
|
+
};
|
|
1528
|
+
if (parsed.op === "show") return showToolText(data => parsed.id ? renderStudyShow(data, present.columns) : renderStudyList(data, present.columns), await taskOp("hub_study_show", parsed.id ? { id: parsed.id } : parsed.after ? { after: parsed.after } : {}), present);
|
|
1529
|
+
const request = parsed.op === "draft" ? { parentTaskId: parsed.parentTaskId, contract: contract(parsed.file) }
|
|
1530
|
+
: parsed.op === "revise" ? { id: parsed.id, revision: parsed.revision, contract: contract(parsed.file) }
|
|
1531
|
+
: parsed.op === "approve_protocol" ? { hash: parsed.hash }
|
|
1532
|
+
: parsed.op === "approve" ? { id: parsed.id, revision: parsed.revision } : { id: parsed.id, revision: parsed.revision, reason: parsed.reason };
|
|
1533
|
+
return showToolText(data => renderStudyReceipt(parsed.op, data, present.columns), await taskOp(`study_${parsed.op}`, request, false, undefined, parsed.op === "approve" ? 90_000 : undefined), present);
|
|
1534
|
+
},
|
|
1535
|
+
|
|
1460
1536
|
capabilities: async () => {
|
|
1461
1537
|
// #445 C1: what each attached peer offers and which of it you share. Person only, like `learn`: `capabilities` has no entry in classifyPeerCommand, so an agent shell is refused before this runs and the daemon refuses any caller but the person again. It lists; it never calls anything.
|
|
1462
1538
|
const split = splitPresentation(args);
|
|
@@ -1530,6 +1606,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1530
1606
|
if (!selection.label) fail("GitHub import requires --label");
|
|
1531
1607
|
return console.log(await taskOp("github_import", selection));
|
|
1532
1608
|
}
|
|
1609
|
+
if (args[0] === "--handoffs" || args.includes("--handoffs")) {
|
|
1610
|
+
// #604: what this project handed to other projects and the last state each reported (read-only).
|
|
1611
|
+
const options = outputOptions();
|
|
1612
|
+
return showToolText(data => renderHandoffs(data, options.columns), await taskOp("hub_task_handoffs", {}), options);
|
|
1613
|
+
}
|
|
1533
1614
|
const options = outputOptions();
|
|
1534
1615
|
const { one, rest } = takeFlags(args, ["--release", "--release-id"], []);
|
|
1535
1616
|
if (one["--release"] !== undefined && !one["--release"].trim() || one["--release"] !== undefined && one["--release-id"] !== undefined || one["--release-id"] !== undefined && !/^[1-9]\d*$/.test(one["--release-id"])) fail("choose --release <nonempty name> or --release-id <positive id>");
|
|
@@ -1773,7 +1854,16 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1773
1854
|
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 } : {}) }]);
|
|
1774
1855
|
return console.log(`task #${id} (approved ${record!.approvedAt}) labelled ${label} in the research records`);
|
|
1775
1856
|
}
|
|
1776
|
-
if (sub
|
|
1857
|
+
if (sub === "handoff") {
|
|
1858
|
+
// #604: work that belongs to ANOTHER project that runs agent-hub goes onto that project's board. The person's form (a console command: the classifier gives an agent `console` for it, and an
|
|
1859
|
+
// agent uses hub_task_handoff); the hub resolves the target, asks its daemon and records the answer. Nothing is queued: a target that is not running is named with the action.
|
|
1860
|
+
const present = outputOptions(splitPresentation(rest, { valueFlags: ["--project", "--path", "--issue", "--task", "--detail"], edges: true }).flags);
|
|
1861
|
+
const parsed = parseHandoffArgs(splitPresentation(rest, { valueFlags: ["--project", "--path", "--issue", "--task", "--detail"], edges: true }).operands);
|
|
1862
|
+
if ("usage" in parsed) return fail(parsed.usage);
|
|
1863
|
+
const text = await taskOp("hub_task_handoff", { ...parsed });
|
|
1864
|
+
return showToolText(data => renderHandoffReceipt(data, present.columns), text, present);
|
|
1865
|
+
}
|
|
1866
|
+
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> | handoff --project <root|id> <title...>");
|
|
1777
1867
|
// `--class` is the explicit form. A first word that is a class name is still taken as the class (the documented
|
|
1778
1868
|
// short form), but said out loud: "review the auth module" would otherwise be filed as class review, silently.
|
|
1779
1869
|
const urgent = rest.includes("--urgent"), backlog = rest.includes("--backlog");
|
|
@@ -1903,7 +1993,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1903
1993
|
try {
|
|
1904
1994
|
const environment = autonomyEnvironment(process.env, !!process.stdin.isTTY), home = hubHome();
|
|
1905
1995
|
if (args.includes("--all-projects")) {
|
|
1906
|
-
assertLifecycleAvailable();
|
|
1996
|
+
assertLifecycleAvailable(cwd);
|
|
1997
|
+
for (const project of registry.list()) assertLifecycleAvailable(project.root);
|
|
1907
1998
|
await runAllProjectAutonomy(args, { home, environment, projects: registry.list(),
|
|
1908
1999
|
prepare: (project, enabled) => prepareAutonomyProject(project, environment, enabled, registry),
|
|
1909
2000
|
ask: async prompt => rl ? rl.question(prompt) : null, print: line => console.log(terminalText(line)),
|
|
@@ -1938,7 +2029,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1938
2029
|
contextLines: [...(defaults.notice ? [defaults.notice] : []), "On/off also changes this machine's autonomy default for new projects; --all-projects applies the change to every registered project."],
|
|
1939
2030
|
confirmPrompt: "Apply this project's preset and this machine's autonomy default? [y/N] ",
|
|
1940
2031
|
beforeApply: action => {
|
|
1941
|
-
assertLifecycleAvailable();
|
|
2032
|
+
assertLifecycleAvailable(cwd);
|
|
1942
2033
|
if (action === "off" && defaults.fingerprint === "untrusted") console.log("Unsafe machine defaults are already ignored; their file was left unchanged.");
|
|
1943
2034
|
else writeAutonomyDefaults(home, action === "on", defaults.fingerprint, roots);
|
|
1944
2035
|
console.log(`Machine autonomy default: ${action}.`);
|
|
@@ -2494,7 +2585,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2494
2585
|
throw new Error(holder?.instanceId?.startsWith("reset-") ? `another ahub reset of this project is running (pid ${holder.pid}); nothing was reset` : "a hub started after the stop; nothing was reset, run ahub reset again");
|
|
2495
2586
|
}
|
|
2496
2587
|
if (liveManifest()) throw new Error("a hub started after the stop; nothing was reset, run ahub reset again");
|
|
2497
|
-
//
|
|
2588
|
+
// A recovery that took this project's hold after the first check must not run beside its reset.
|
|
2498
2589
|
resetLockFree();
|
|
2499
2590
|
if (all) {
|
|
2500
2591
|
let archived: string;
|
|
@@ -2579,6 +2670,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2579
2670
|
const row = (ok: boolean | undefined, name: string, detail: string) => { checks.push(nativeDoctorCheck(ok, name, detail)); };
|
|
2580
2671
|
|
|
2581
2672
|
try {
|
|
2673
|
+
for (const check of runtimePruneHealth()) row(check.ok, check.name, check.detail);
|
|
2582
2674
|
const catalog = loadRoleCatalog(cwd);
|
|
2583
2675
|
for (const error of catalog.errors) row(false, "catalog role", `${error.id}: ${error.reason}`);
|
|
2584
2676
|
try {
|
|
@@ -2590,11 +2682,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2590
2682
|
row(!!v, bin, v ?? "not found on PATH");
|
|
2591
2683
|
}
|
|
2592
2684
|
const up = await healthy();
|
|
2685
|
+
let daemonVersion: unknown;
|
|
2593
2686
|
row(up, "ahub daemon", up ? readControl(stateDir)!.url : "not running (ahub up)");
|
|
2594
2687
|
if (up) {
|
|
2595
2688
|
const hub = await connect();
|
|
2596
2689
|
try {
|
|
2597
2690
|
const { status } = await hub.request({ t: "status" });
|
|
2691
|
+
daemonVersion = status.version; // Authenticated reply, never the unauthenticated disk manifest.
|
|
2598
2692
|
const pending = Object.values(status.peers as Record<string, PeerRow>).reduce((n, peer) => n + (peer.needsReview ?? 0), 0);
|
|
2599
2693
|
row(!status.deliveryError && pending === 0, "delivery recovery", status.deliveryError ?? (pending ? `${pending} deliveries need review; run ahub queue list` : "journal healthy; no deliveries need review"));
|
|
2600
2694
|
// #285 AC10: where Kimi's number comes from, and whether the hub can trust it right now. An older hub answers without `kimiSource`: no row, not a failure.
|
|
@@ -2610,6 +2704,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2610
2704
|
}
|
|
2611
2705
|
const plugin = pluginState(parseList<InstalledPlugin>(spawnSync(DOCTOR_PLUGIN_PROBE.command, [...DOCTOR_PLUGIN_PROBE.argv], { encoding: "utf8" }).stdout ?? ""), join(import.meta.dir, "..", ".."));
|
|
2612
2706
|
row(plugin.state === "current", DOCTOR_PLUGIN_PROBE.name, plugin.state === "missing" ? "missing: run ahub setup" : plugin.state === "current" ? `agent-hub@agent-hub ${plugin.version}` : `agent-hub@agent-hub is stale (${plugin.why}): run ahub setup`);
|
|
2707
|
+
if (up) checks.push(...runtimeVersionWarnings(daemonVersion, VERSION, plugin.state === "missing" ? undefined : plugin.version));
|
|
2613
2708
|
|
|
2614
2709
|
const config = loadConfig(cwd);
|
|
2615
2710
|
row(!config.ignored, "config", config.ignored ? `${config.ignored.join("; ")} (move them to .agenthub/config.local.json)` : "no machine-local field ignored");
|
|
@@ -2658,11 +2753,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2658
2753
|
let memoryLibraryReady = false;
|
|
2659
2754
|
try { assertAgentMemVersion({ name: "@staix/agent-mem", version: AGENT_MEM_VERSION }); memoryLibraryReady = true; } catch { /* report the failed exact pin without opening a store */ }
|
|
2660
2755
|
row(memoryLibraryReady, "memory library", memoryLibraryReady ? `amem ${AGENT_MEM_VERSION}; in-process, no claude-mem worker` : "installed amem does not match the exact dependency pin");
|
|
2661
|
-
|
|
2662
|
-
|
|
2756
|
+
// #1008: memory capture is agent-mem's own setup state, read as structured data (`amem setup <tool> --json`, a preview that writes nothing), never dotfiles text. The caller's
|
|
2757
|
+
// identity is NOT scrubbed: agent-mem screens a plan for an agent, and a refusal reads as unknown here instead of being laundered into a person's answer.
|
|
2663
2758
|
for (const tool of DOCTOR_MEMORY_CAPTURE_PEERS) {
|
|
2664
|
-
const
|
|
2665
|
-
row(
|
|
2759
|
+
const found = nativeMemoryCaptureRow(tool, readMemorySetup(tool));
|
|
2760
|
+
row(found.ok, `memory capture: ${tool}`, found.detail);
|
|
2666
2761
|
}
|
|
2667
2762
|
} finally {
|
|
2668
2763
|
// Preserve findings already collected if a later probe (for example config parsing) throws.
|
|
@@ -2672,6 +2767,19 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2672
2767
|
},
|
|
2673
2768
|
};
|
|
2674
2769
|
|
|
2770
|
+
/**
|
|
2771
|
+
* One bounded read of agent-mem's setup state for a tool: `amem setup <tool> --json` with no mode flag is a preview and writes nothing. No shell, a 10 s timeout, a 256 KiB output cap,
|
|
2772
|
+
* and the answer is parsed as JSON here and judged by `nativeMemoryCaptureRow`; anything that does not parse, exits non-zero or times out is "unavailable", never a guess.
|
|
2773
|
+
* ponytail: runs the `amem` found on PATH (the hub pins @staix/agent-mem exactly, but the person's own `amem` may be another version); the upgrade is to call `planSetup` from the pinned
|
|
2774
|
+
* library in-process once it is exported from its public entry.
|
|
2775
|
+
*/
|
|
2776
|
+
function readMemorySetup(tool: string): MemorySetupReading {
|
|
2777
|
+
const found = spawnSync("amem", ["setup", tool, "--json"], { encoding: "utf8", timeout: 10_000, maxBuffer: 256 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
2778
|
+
if (found.error) return { kind: "unavailable", reason: (found.error as NodeJS.ErrnoException).code === "ENOENT" ? "`amem` is not on PATH" : "`amem setup` could not be run" };
|
|
2779
|
+
if (found.status !== 0 || typeof found.stdout !== "string") return { kind: "unavailable", reason: found.status === null ? "`amem setup` timed out" : `\`amem setup ${tool}\` refused or failed` };
|
|
2780
|
+
try { return { kind: "read", plan: JSON.parse(found.stdout) }; } catch { return { kind: "unavailable", reason: "`amem setup --json` did not print JSON" }; }
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2675
2783
|
function sanitizeRoutingParseError(file: "routing.toml" | "routing.local.toml", error: unknown): string {
|
|
2676
2784
|
const raw = ((error as Error)?.message || "invalid TOML").split("\n")[0]!.trim();
|
|
2677
2785
|
const cleaned = raw
|
|
@@ -2793,7 +2901,7 @@ async function runConductorCommand(): Promise<void> {
|
|
|
2793
2901
|
}
|
|
2794
2902
|
const runConfigured = configuredCommand ? async () => {
|
|
2795
2903
|
const selection = parseConfiguredAcpArguments(cmd, args, projectCatalog, declaredInstances, peerConfig.declaredExternalInstances);
|
|
2796
|
-
if (!selection.preview) assertLifecycleAvailable();
|
|
2904
|
+
if (!selection.preview) assertLifecycleAvailable(cwd);
|
|
2797
2905
|
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 });
|
|
2798
2906
|
} : undefined;
|
|
2799
2907
|
const run = identity.role === "tools" && commandAccess === "conductor" ? runConductorCommand : (Object.hasOwn(commands, cmd) ? commands[cmd] : undefined) ?? runConfigured ?? (() => fail(`unknown command "${cmd}"; run ahub help`));
|
package/src/cli/output.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { cells, TABLES, table, fitLine, wrap, duration, relative, plural, termin
|
|
|
7
7
|
import { taskProgress } from "../ui/task-progress.ts";
|
|
8
8
|
import { backendLabel } from "./status-lines.ts";
|
|
9
9
|
import { ownerIsolationText } from "./role-holders.ts";
|
|
10
|
+
import { schedulePeriodText, scheduleWhen } from "../hub/schedules.ts";
|
|
10
11
|
import { formatReport, formatTaskReport, type Report, type TaskReport } from "../hub/report.ts";
|
|
11
12
|
import type { Counter, LimitsResult, TotalResult, UsageResult } from "../hub/usage-rollup.ts";
|
|
12
13
|
|
|
@@ -20,7 +21,7 @@ export interface StatusOutput {
|
|
|
20
21
|
/** #843 AC3: installed agent CLIs not on a verified version; ids, version, state and a receipt path only. */
|
|
21
22
|
agentDrift?: readonly { id: string; version: string; state: string; receipt: string }[];
|
|
22
23
|
/** #512a: one row per machine-local schedule; ids, times and a state only, never task text. */
|
|
23
|
-
schedules?: readonly { id: string; every
|
|
24
|
+
schedules?: readonly { id: string; every?: string; cron?: string; zone?: string; class: string; queue: string; next: number; last?: number; task?: number; state: string; coalesced?: number; error?: string }[];
|
|
24
25
|
taskStalls?: readonly { task: number; cause: { kind: string; peer?: string; dependency?: number; code: string } }[];
|
|
25
26
|
pendingTaskOffers?: readonly { task: number; peer: string }[];
|
|
26
27
|
isolatedChecks?: { pending?: number; orphans?: string[]; unknownReceipts?: readonly string[]; maintenanceUnavailable?: boolean };
|
|
@@ -218,8 +219,8 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
218
219
|
for (const pending of data.retainedConfig) out.push(...detail("retained reason", `${pending.key}: ${pending.reason}`, columns, "attention"));
|
|
219
220
|
}
|
|
220
221
|
for (const row of data.schedules ?? []) {
|
|
221
|
-
const when = (at: number) => new Date(at).toISOString().replace("T", " ").slice(0, 16) + "Z";
|
|
222
|
-
const text = `${row.state === "paused" ? `paused (ahub schedule resume ${row.id}); would be next ${when(row.next)}` : row.state === "scheduled" ? `next ${when(row.next)}` : row.state === "due" ? "due now" : row.state === "waiting" ? `due, waiting for task #${row.task} to close` : "due, held (the hub is stopping or recovering)"};
|
|
222
|
+
const when = (at: number) => row.zone ? scheduleWhen(row, at) : new Date(at).toISOString().replace("T", " ").slice(0, 16) + "Z";
|
|
223
|
+
const text = `${row.state === "paused" ? `paused (ahub schedule resume ${row.id}); would be next ${when(row.next)}` : row.state === "scheduled" ? `next ${when(row.next)}` : row.state === "due" ? "due now" : row.state === "waiting" ? `due, waiting for task #${row.task} to close` : "due, held (the hub is stopping or recovering)"}; ${schedulePeriodText(row)}; ${row.class} to ${row.queue}${row.last ? `; last ${when(row.last)}${row.task ? ` as task #${row.task}` : ""}` : "; never run"}${row.coalesced ? `; ${row.coalesced} skipped run${row.coalesced === 1 ? "" : "s"} coalesced` : ""}${row.error ? `; last error: ${row.error}` : ""}`;
|
|
223
224
|
out.push(...detail(`schedule ${row.id}`, text, columns, row.error ? "attention" : undefined));
|
|
224
225
|
}
|
|
225
226
|
if (data.queueBackpressure) {
|
|
@@ -347,6 +348,8 @@ export function renderBacklog(cards: any[], columns?: number, now = Date.now(),
|
|
|
347
348
|
return [...rows(names, table, columns), [], ...lines(summary, columns)];
|
|
348
349
|
}
|
|
349
350
|
/** #724: one line per task. TITLE goes last and is cut to the room left; at a narrow width REVIEWER, CLASS and AGE give way first. */
|
|
351
|
+
/** #604: a task another project handed over says so on the board: the project id and the peer, both ids and nothing a handoff wrote. */
|
|
352
|
+
const handoffMark = (task: any): string => task?.handoffOrigin ? ` (handed off from ${String(task.handoffOrigin.projectId).slice(0, 24)} by ${String(task.handoffOrigin.peer).slice(0, 32)})` : "";
|
|
350
353
|
function compactBoard(tasks: any[], data: OutputCell[][], columns: number, staged: boolean): Span[][] {
|
|
351
354
|
const head = ["ID", "STATE", "OWNER", "REVIEWER", "CLASS", "AGE", "AFTER", ...(staged ? ["STAGE"] : []), "TITLE"];
|
|
352
355
|
let table = tasks.map((task, i) => {
|
|
@@ -355,7 +358,7 @@ function compactBoard(tasks: any[], data: OutputCell[][], columns: number, stage
|
|
|
355
358
|
const meter = /^\[[^\]]*\](?: cancelled)?/.exec(row[7]?.text ?? "")?.[0] ?? "[----]";
|
|
356
359
|
const stage = task.lifecycle ? `${meter} ${task.lifecycle.stage}: ${stageCode(String(task.lifecycle.next ?? ""))}` : meter;
|
|
357
360
|
const short = task.lifecycle ? `${meter} ${stageCode(String(task.lifecycle.next ?? ""))}` : meter;
|
|
358
|
-
return [row[0]!, row[1]!, row[2]!, row[3]!, row[4]!, row[5]!, span(after, undefined, true), ...(staged ? [{ ...span(stage, row[7]!.tone, true), short }] : []), span(flat(task.title))];
|
|
361
|
+
return [row[0]!, row[1]!, row[2]!, row[3]!, row[4]!, row[5]!, span(after, undefined, true), ...(staged ? [{ ...span(stage, row[7]!.tone, true), short }] : []), span(`${flat(task.title)}${handoffMark(task)}`)];
|
|
359
362
|
});
|
|
360
363
|
let names = [...head];
|
|
361
364
|
const drop = (name: string) => { const at = names.indexOf(name); if (at < 0) return; names = names.filter((_, i) => i !== at); table = table.map(row => row.filter((_, i) => i !== at)); };
|
|
@@ -389,7 +392,7 @@ export function renderBoard(tasks: any[], columns?: number, now = Date.now(), fu
|
|
|
389
392
|
const row = cells(s, task, false, now, stages);
|
|
390
393
|
row[2] = span(row[2]!.text, peerTone(task.owner ?? ""));
|
|
391
394
|
row[3] = span(row[3]!.text, peerTone(task.reviewer ?? ""));
|
|
392
|
-
row[6] = span(`${task.title}${task.deps?.length ? ` after ${task.deps.map((id: number) => `#${id}`).join(", ")}` : ""}${task.signals?.includes("pii") ? ` (ahub task show ${task.id})` : ""}`);
|
|
395
|
+
row[6] = span(`${task.title}${handoffMark(task)}${task.deps?.length ? ` after ${task.deps.map((id: number) => `#${id}`).join(", ")}` : ""}${task.signals?.includes("pii") ? ` (ahub task show ${task.id})` : ""}`);
|
|
393
396
|
if (allTasks === null) row.pop();
|
|
394
397
|
return row;
|
|
395
398
|
});
|