@staix/agent-hub 0.12.23 → 0.12.24
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 +98 -0
- package/README.md +8 -3
- package/docs/addons/claude-mem.md +24 -0
- package/docs/addons/codex-plugin.md +8 -0
- package/docs/addons/dot-admit.md +8 -0
- package/docs/addons/open-code-review.md +8 -0
- package/docs/addons/ponytail.md +23 -0
- package/docs/addons/ripwire.md +22 -0
- package/docs/agent-notes/README.md +5 -0
- package/docs/agent-notes/adapters.md +8 -1
- package/docs/agent-notes/bus.md +3 -1
- package/docs/agent-notes/daemon.md +7 -1
- package/docs/agent-notes/local-worker.md +4 -2
- package/docs/agent-notes/tasks.md +4 -0
- package/docs/agent-notes/tests.md +2 -1
- package/docs/bench.md +28 -3
- package/docs/cooperbench.md +4 -0
- package/docs/data-policy.md +36 -0
- package/docs/events.md +3 -0
- package/docs/gsd-collaboration-analysis.md +48 -0
- package/docs/memory.md +231 -0
- package/docs/operations.md +651 -14
- package/docs/plans/2026-10-11-issues-109-sequential.md +81 -0
- package/docs/plans/2026-10-11-routing-evaluation-proposal.md +30 -0
- package/docs/profiles.md +57 -0
- package/docs/quickstart.md +1 -1
- package/docs/research.md +31 -1
- package/docs/roles.md +85 -0
- package/docs/sdlc.md +170 -0
- package/docs/security.md +111 -4
- package/docs/smoke.md +282 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +601 -7
- package/docs/specs/2026-10-12-backlog-implementation-plan.md +186 -0
- package/docs/specs/2026-10-12-office-view-design.md +212 -0
- package/docs/text-projection.md +19 -0
- package/docs/verification/2026-10-11-acp-watch-t0.md +33 -0
- package/docs/verification/2026-10-11-agent-config-inventory.md +20 -0
- package/docs/verification/2026-10-11-jcode-peer-contract.md +27 -0
- package/docs/verification/2026-10-13-0.12.24.md +94 -0
- package/docs/verification/2026-10-13-board-phone.md +31 -0
- package/docs/verification/2026-10-13-fsmonitor-fixture-attribution.md +19 -0
- package/docs/verified.json +22 -13
- package/package.json +2 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/ROLE-NOTICE.md +26 -0
- package/plugins/agent-hub/server.js +1175 -339
- package/scripts/check-docs.mjs +129 -0
- package/src/adapters/acp.ts +112 -13
- package/src/adapters/claude-channel.ts +48 -11
- package/src/adapters/codex-appserver.ts +11 -0
- package/src/adapters/jcode.ts +110 -0
- package/src/adapters/local-worker.ts +10 -7
- package/src/adapters/pi.ts +94 -20
- package/src/cli/attention.ts +60 -0
- package/src/cli/bench.ts +24 -5
- package/src/cli/board-read.ts +12 -0
- package/src/cli/console-state.ts +103 -17
- package/src/cli/console.ts +54 -8
- package/src/cli/facts-hook.ts +12 -4
- package/src/cli/help.ts +35 -9
- package/src/cli/identity.ts +18 -7
- package/src/cli/init.ts +48 -10
- package/src/cli/jcode-mcp.ts +58 -0
- package/src/cli/kimi-mcp.ts +53 -0
- package/src/cli/launch.ts +16 -1
- package/src/cli/main.ts +392 -57
- package/src/cli/memory.ts +193 -0
- package/src/cli/output.ts +45 -4
- package/src/cli/pet-config.ts +1 -0
- package/src/cli/pet.ts +83 -0
- package/src/cli/preview.ts +38 -5
- package/src/cli/status-lines.ts +1 -1
- package/src/cli/tail-render.ts +1 -0
- package/src/cli/upgrade-runtime.ts +32 -3
- package/src/cli/watch.ts +31 -0
- package/src/hub/activity.ts +53 -0
- package/src/hub/ask.ts +10 -5
- package/src/hub/bench.ts +10 -3
- package/src/hub/board.ts +222 -16
- package/src/hub/bus.ts +48 -9
- package/src/hub/citations.ts +7 -0
- package/src/hub/conductor.ts +12 -7
- package/src/hub/config-trust.ts +4 -2
- package/src/hub/crash.ts +9 -0
- package/src/hub/daemon.ts +910 -120
- package/src/hub/delivery-journal.ts +5 -0
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +12 -2
- package/src/hub/facts.ts +26 -7
- package/src/hub/git.ts +8 -2
- package/src/hub/github-import.ts +28 -0
- package/src/hub/hub-tools.ts +49 -10
- package/src/hub/inference.ts +1 -1
- package/src/hub/lifecycle-schema.ts +17 -0
- package/src/hub/listen.ts +15 -0
- package/src/hub/manager.ts +12 -5
- package/src/hub/path-policy.ts +96 -0
- package/src/hub/peer-catalog.ts +28 -0
- package/src/hub/permission-mode.ts +19 -5
- package/src/hub/planning.ts +70 -0
- package/src/hub/profiles.ts +74 -0
- package/src/hub/projected-diff.ts +73 -0
- package/src/hub/projection-exec.ts +24 -0
- package/src/hub/research.ts +144 -21
- package/src/hub/roles.ts +123 -0
- package/src/hub/roster.ts +23 -0
- package/src/hub/routing.ts +18 -5
- package/src/hub/sdlc-queue.ts +112 -0
- package/src/hub/sdlc-release.ts +125 -0
- package/src/hub/sdlc.ts +250 -0
- package/src/hub/settings.ts +34 -15
- package/src/hub/shipped-roles.ts +25 -0
- package/src/hub/snapshots.ts +89 -15
- package/src/hub/start-mode.ts +7 -5
- package/src/hub/supervision.ts +1 -0
- package/src/hub/task-sweep.ts +1 -1
- package/src/hub/tasks.ts +892 -87
- package/src/hub/text-projection.ts +126 -0
- package/src/hub/tooling.ts +159 -0
- package/src/hub/ui.ts +16 -2
- package/src/hub/workflow.ts +71 -0
- package/src/local/sandbox.ts +6 -4
- package/src/local/tools.ts +11 -6
- package/src/memory/brief.ts +28 -14
- package/src/memory/import.ts +55 -0
- package/src/memory/recall.ts +84 -0
- package/src/memory/store.ts +750 -0
- package/src/memory/sync.ts +145 -0
- package/src/pet/NOTICE.md +10 -0
- package/src/pet/characters.ts +1438 -0
- package/src/pet/config.ts +31 -0
- package/src/pet/frame.ts +17 -0
- package/src/pet/pixels.ts +28 -0
- package/src/role-text.d.ts +1 -0
- package/src/ui/index.html +842 -46
- package/src/ui/needs.ts +58 -0
- package/src/ui/office-model.ts +440 -0
- package/src/ui/peer-palette.ts +16 -0
- package/src/ui/task-board.ts +49 -0
- package/src/ui/task-progress.ts +5 -1
- package/templates/AGENTS.block.md +3 -2
- package/templates/config.json +5 -1
- package/templates/profiles/dev.json +22 -0
- package/templates/profiles/general.json +26 -0
- package/templates/profiles/research.json +57 -0
- package/templates/profiles/writing.json +28 -0
- package/templates/roles/NOTICE.md +26 -0
- package/templates/roles/dev/backend-engineer.md +26 -0
- package/templates/roles/dev/code-reviewer.md +26 -0
- package/templates/roles/dev/frontend-engineer.md +26 -0
- package/templates/roles/dev/security-reviewer.md +26 -0
- package/templates/roles/dev/software-architect.md +26 -0
- package/templates/roles/dev/test-engineer.md +26 -0
- package/templates/roles/research/analyst.md +26 -0
- package/templates/roles/research/fact-checker.md +26 -0
- package/templates/roles/research/research-lead.md +26 -0
- package/templates/roles/research/researcher.md +26 -0
- package/templates/roles/research/statistician.md +26 -0
package/src/cli/main.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { supportsBoardReleaseFilter, supportsBoardColumnMetadata } from "./board-read.ts";
|
|
3
|
+
import { matchesPolicy } from "../hub/path-policy.ts";
|
|
4
|
+
import { projectedDiff } from "../hub/projected-diff.ts";
|
|
5
|
+
import { TextProjections } from "../hub/text-projection.ts";
|
|
6
|
+
import { terminalText } from "./console-state.ts";
|
|
7
|
+
import { petView, readPet, renderPet } from "./pet.ts";
|
|
8
|
+
import { loadPetConfig } from "./pet-config.ts";
|
|
9
|
+
import { profileName, projectProfile, resolveProfile } from "../hub/profiles.ts";
|
|
10
|
+
import { catalogAssignments, loadRoleCatalog, selectedRoles } from "../hub/roles.ts";
|
|
11
|
+
import { attachWatch } from "./watch.ts";
|
|
12
|
+
import { verifiedJcodeCommand } from "../adapters/jcode.ts";
|
|
13
|
+
import { configureJcodeMcp } from "./jcode-mcp.ts";
|
|
14
|
+
import { qwenSessionId } from "../hub/crash.ts";
|
|
2
15
|
import { isPermissionMode } from "../hub/permission-mode.ts";
|
|
3
16
|
import { parseSettingText, settingDef, settingText, type SettingRow } from "../hub/settings.ts";
|
|
17
|
+
import { isSupportedPeer } from "../hub/peer-catalog.ts";
|
|
4
18
|
import { startModeOf } from "../hub/start-mode.ts";
|
|
5
19
|
import { currentRouting } from "../hub/routing.ts";
|
|
6
20
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -11,7 +25,7 @@ import { basename, dirname, join, relative, resolve } from "node:path";
|
|
|
11
25
|
import { ControlClient, readControl } from "../hub/control-client.ts";
|
|
12
26
|
import { loadConfig } from "../hub/daemon.ts";
|
|
13
27
|
import { factsHook } from "./facts-hook.ts";
|
|
14
|
-
import { projectContext, realPath } from "../hub/project.ts";
|
|
28
|
+
import { hubHome, projectContext, realPath } from "../hub/project.ts";
|
|
15
29
|
import { Registry, type Project } from "../hub/registry.ts";
|
|
16
30
|
import { inspectProject, startProject, stopProject, runProjectDaemon } from "../hub/lifecycle.ts";
|
|
17
31
|
import { processLiveness, processSignature } from "../pi/process-signature.ts";
|
|
@@ -20,7 +34,8 @@ import { OmniRoute } from "../omniroute/client.ts";
|
|
|
20
34
|
import { MemoryClient } from "../memory/client.ts";
|
|
21
35
|
import { init, planInit } from "./init.ts";
|
|
22
36
|
import { launcherPreview } from "./preview.ts";
|
|
23
|
-
import { cleanupStaleClaudeSettings, cleanupClaudeSettings, buildLaunch, claudeObservationHooks, nativeLaunchEnv, recordClaudeLaunch, runClaudeLaunch, type ClaudeLaunchRecord, UNATTENDED_WARNING } from "./launch.ts";
|
|
37
|
+
import { cleanupStaleClaudeSettings, cleanupClaudeSettings, buildLaunch, buildKimiTuiLaunch, claudeObservationHooks, nativeLaunchEnv, recordClaudeLaunch, runClaudeLaunch, type ClaudeLaunchRecord, UNATTENDED_WARNING } from "./launch.ts";
|
|
38
|
+
import { configureKimiMcp } from "./kimi-mcp.ts";
|
|
24
39
|
import { nextStep, parseList, pluginState, type InstalledPlugin, type Marketplace } from "./setup.ts";
|
|
25
40
|
import { CLASSES } from "../hub/board.ts";
|
|
26
41
|
import { VERSION } from "../version.ts";
|
|
@@ -39,9 +54,11 @@ import { setupOllamaModel } from "./models-setup.ts";
|
|
|
39
54
|
|
|
40
55
|
import { contextLine, type PeerRow } from "./status-lines.ts";
|
|
41
56
|
import { parseSince, readEvents } from "../hub/events.ts";
|
|
42
|
-
import { armRuns, benchCompare, benchCsv, benchReport, formatCompare, formatReport as formatBenchReport, formatRuns, readRuns, runSummary, sameSuite } from "../hub/bench.ts";
|
|
57
|
+
import { armRuns, benchCompare, benchCsv, benchReport, formatCompare, formatReport as formatBenchReport, formatRuns, MEMORY_MODES, readRuns, runSummary, sameSuite, type MemoryMode } from "../hub/bench.ts";
|
|
43
58
|
import { benchPreflight, defaultIo, runBench } from "./bench.ts";
|
|
44
|
-
import {
|
|
59
|
+
import { ADDONS, isAddon, type AddonName } from "../hub/tooling.ts";
|
|
60
|
+
import { goalCommand, memoryCommand } from "./memory.ts";
|
|
61
|
+
import { addonReport, appendRecords, formatAddons, formatResearch, LABELS, labelTarget, RESEARCH_SCHEMA, projectKey, readStores, researchReport, taskRecords, toCsv, type Label, type TaskRecord } from "../hub/research.ts";
|
|
45
62
|
import { summarize, summarizeByTask } from "../hub/report.ts";
|
|
46
63
|
import { hasTree, planUndo, repoOf, restore, Turns } from "../hub/snapshots.ts";
|
|
47
64
|
import { pathWarnings } from "../hub/conflicts.ts";
|
|
@@ -49,7 +66,7 @@ import { classifyPeerCommand, cliCommandLabel, detectCliIdentity, peerCommandRef
|
|
|
49
66
|
import { recordCliAudit } from "./identity-audit.ts";
|
|
50
67
|
import { runConsole } from "./console.ts";
|
|
51
68
|
import { paint, permissionBoundary, resolveColor, wrap, type Span } from "./console-state.ts";
|
|
52
|
-
import { outputWidth, renderStatus, renderBoard, renderBudget, renderDoctor, renderProjects, renderQueue, renderTurns, renderOrphans, renderQueueShow, renderModelsStatus, renderExecutionBudgetStatus, renderReport, type DoctorCheck } from "./output.ts";
|
|
69
|
+
import { outputWidth, renderRoster, renderStatus, renderBoard, renderBoardColumns, boardColumnModel, renderBudget, renderDoctor, renderProjects, renderQueue, renderTurns, renderOrphans, renderQueueShow, renderModelsStatus, renderExecutionBudgetStatus, renderReport, type DoctorCheck } from "./output.ts";
|
|
53
70
|
import { renderHelp } from "./help.ts";
|
|
54
71
|
import { renderTailEvent } from "./tail-render.ts";
|
|
55
72
|
import { archiveProblem, archiveState, damagedState, failureText, MANIFEST, planReset, resetLines, resetRuntime, startState, type ResetPlan } from "./reset.ts";
|
|
@@ -176,7 +193,7 @@ async function healthy(): Promise<boolean> {
|
|
|
176
193
|
finally { hub?.close(); }
|
|
177
194
|
}
|
|
178
195
|
|
|
179
|
-
function exec(bin: string, argv: string[], tool?: "claude" | "codex"): never {
|
|
196
|
+
function exec(bin: string, argv: string[], tool?: "claude" | "codex" | "kimi"): never {
|
|
180
197
|
const env = tool ? nativeLaunchEnv(tool, childEnv()) : childEnv();
|
|
181
198
|
const res = spawnSync(bin, argv, { cwd, stdio: "inherit", env: { ...env, AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } });
|
|
182
199
|
if (res.error) fail(`cannot run ${bin}: ${res.error.message}`);
|
|
@@ -311,13 +328,13 @@ function fail(message: string): never {
|
|
|
311
328
|
process.exit(1);
|
|
312
329
|
}
|
|
313
330
|
|
|
314
|
-
async function taskOp(op: string, a: Record<string, unknown>, throwOnError = false): Promise<string> {
|
|
315
|
-
const hub = await connect();
|
|
331
|
+
async function taskOp(op: string, a: Record<string, unknown>, throwOnError = false, connection?: ControlClient): Promise<string> {
|
|
332
|
+
const hub = connection ?? await connect();
|
|
316
333
|
try {
|
|
317
|
-
const res = await hub.request({ t: "task", op, args: a });
|
|
334
|
+
const res = await hub.request({ t: "task", op, args: a }, op === "github_import" ? 60_000 : 30_000);
|
|
318
335
|
if (!res.ok) { if (throwOnError) throw new Error(res.error); audit("refused"); fail(res.error); }
|
|
319
336
|
return res.text;
|
|
320
|
-
} finally { hub.close(); }
|
|
337
|
+
} finally { if (!connection) hub.close(); }
|
|
321
338
|
}
|
|
322
339
|
|
|
323
340
|
/** Reads hub.db's turn records; `none` when the hub never kept one (snapshots off, no git work tree, an older hub). */
|
|
@@ -465,6 +482,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
465
482
|
});
|
|
466
483
|
process.exit(code);
|
|
467
484
|
}
|
|
485
|
+
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");
|
|
468
486
|
if (interactive) return onTerminal((io) => planScreen(upgradeHost(kind, to ?? VERSION), io));
|
|
469
487
|
const plan = await makeUpgradePlan(kind, to ?? VERSION, kind === "restart" ? cwd : undefined);
|
|
470
488
|
console.log(JSON.stringify(plan, null, 2));
|
|
@@ -625,14 +643,19 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
625
643
|
},
|
|
626
644
|
|
|
627
645
|
init: () => {
|
|
646
|
+
const flags = takeFlags(args.filter(arg => arg !== "--dry-run" && arg !== "--json" && arg !== "--shadow"), ["--profile"], []);
|
|
647
|
+
if (flags.rest.length) fail("usage: ahub init [--profile dev|research|writing|general] [--shadow] [--dry-run --json]");
|
|
648
|
+
const requestedProfile = flags.one["--profile"] === undefined ? undefined : profileName(flags.one["--profile"]);
|
|
628
649
|
if (args.includes("--dry-run")) {
|
|
629
|
-
const plan = planInit(cwd);
|
|
650
|
+
const plan = planInit(cwd, requestedProfile, args.includes("--shadow"));
|
|
630
651
|
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");
|
|
631
652
|
}
|
|
632
653
|
assertLifecycleAvailable();
|
|
633
|
-
const changed = init(cwd);
|
|
654
|
+
const changed = init(cwd, requestedProfile, args.includes("--shadow"));
|
|
634
655
|
registeredProject();
|
|
635
656
|
console.log(changed.length ? changed.map((p) => `${existsSync(p) ? "wrote" : "removed"} ${p}`).join("\n") : "already up to date");
|
|
657
|
+
const config = projectConfig(), examples = resolveProfile(config.profile, config.profile_overrides).example_checks;
|
|
658
|
+
if (examples.length) console.log(terminalText("Example checks (advisory):\n" + examples.map(example => "- " + example).join("\n")));
|
|
636
659
|
},
|
|
637
660
|
|
|
638
661
|
// Internal: the detached daemon process started by ahub up or the manager.
|
|
@@ -643,10 +666,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
643
666
|
},
|
|
644
667
|
|
|
645
668
|
console: async () => {
|
|
646
|
-
if (args.some(arg => arg !== "--panels" && !arg.startsWith("--color=")) || args.filter(arg => arg.startsWith("--color=")).length > 1) fail("usage: ahub console [--panels] [--color=auto|always|never]");
|
|
669
|
+
if (args.some(arg => arg !== "--panels" && arg !== "--no-pet" && !arg.startsWith("--color=")) || args.filter(arg => arg.startsWith("--color=")).length > 1) fail("usage: ahub console [--panels] [--no-pet] [--color=auto|always|never]");
|
|
647
670
|
const color = resolveColor(args.find(arg => arg.startsWith("--color="))?.slice("--color=".length), { isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR });
|
|
648
|
-
if (color instanceof Error) return fail("usage: ahub console [--panels] [--color=auto|always|never]");
|
|
649
|
-
await runConsole({ client: await connect(), cwd, stateDir, panels: args.includes("--panels"), color });
|
|
671
|
+
if (color instanceof Error) return fail("usage: ahub console [--panels] [--no-pet] [--color=auto|always|never]");
|
|
672
|
+
await runConsole({ client: await connect(), cwd, stateDir, panels: args.includes("--panels"), noPet: args.includes("--no-pet"), color });
|
|
650
673
|
},
|
|
651
674
|
|
|
652
675
|
up: async () => {
|
|
@@ -727,18 +750,89 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
727
750
|
exec(launch.cmd, launch.args, "codex");
|
|
728
751
|
},
|
|
729
752
|
|
|
753
|
+
jcode: async () => {
|
|
754
|
+
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
755
|
+
try { return console.log(JSON.stringify(launcherPreview("jcode", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
756
|
+
catch { fail("cannot preview Jcode launch: invalid arguments or configuration (details withheld)"); }
|
|
757
|
+
}
|
|
758
|
+
assertLifecycleAvailable();
|
|
759
|
+
const { one, rest } = takeFlags(args, ["--mode", "--model", "--provider-profile", "--session-id"], []);
|
|
760
|
+
if (rest.length || (one["--mode"] !== undefined && one["--mode"] !== "headless")) fail("usage: ahub jcode [--mode headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>]");
|
|
761
|
+
if (one["--session-id"] !== undefined && !qwenSessionId(one["--session-id"])) fail("invalid Jcode session identity");
|
|
762
|
+
const hub = await connect();
|
|
763
|
+
try {
|
|
764
|
+
const snapshot = (await hub.request({ t: "status" })).status;
|
|
765
|
+
if (snapshot?.roster?.find((row: any) => row.peer === "jcode")?.on !== true) fail("jcode is off roster; add it with ahub roster add jcode");
|
|
766
|
+
const written = configureJcodeMcp(cwd, stateDir, resolve(import.meta.dir, "../../plugins/agent-hub/server.js"));
|
|
767
|
+
if (written.tracked) console.error(".jcode/mcp.json is tracked; review this local machine-path change before committing it");
|
|
768
|
+
const reply = await hub.request({ t: "start", peer: "jcode", args: { mode: "headless", model: one["--model"], providerProfile: one["--provider-profile"], sessionId: one["--session-id"] }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 65_000);
|
|
769
|
+
if (!reply.ok) fail(reply.error);
|
|
770
|
+
console.log(reply.already ? "jcode is already attached" : "jcode attached (headless); native policy unmanaged, NO hub sandbox");
|
|
771
|
+
} finally { hub.close(); }
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
qwen: async () => {
|
|
775
|
+
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
776
|
+
try { return console.log(JSON.stringify(launcherPreview("qwen", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
777
|
+
catch { fail("cannot preview Qwen launch: invalid arguments or configuration (details withheld)"); }
|
|
778
|
+
}
|
|
779
|
+
assertLifecycleAvailable();
|
|
780
|
+
const { one, rest } = takeFlags(args, ["--mode", "--model", "--session-id"], []);
|
|
781
|
+
if (one["--session-id"] !== undefined && !qwenSessionId(one["--session-id"])) fail("invalid Qwen session identity");
|
|
782
|
+
if (rest.length || (one["--mode"] !== undefined && one["--mode"] !== "headless")) fail("Qwen TUI is not offered before verification; usage: ahub qwen [--mode headless] [--model <alias>] [--session-id <id>]");
|
|
783
|
+
const hub = await connect();
|
|
784
|
+
try {
|
|
785
|
+
const reply = await hub.request({ t: "start", peer: "qwen", args: { mode: "headless", ...(one["--model"] ? { model: one["--model"] } : {}), ...(one["--session-id"] ? { sessionId: one["--session-id"] } : {}) }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 35_000);
|
|
786
|
+
if (!reply.ok) fail(reply.error);
|
|
787
|
+
console.log(reply.already ? "qwen is already attached" : "qwen attached (headless); use ahub say @qwen and the console for approvals");
|
|
788
|
+
} finally { hub.close(); }
|
|
789
|
+
},
|
|
730
790
|
kimi: async () => {
|
|
731
791
|
if (args.includes("--print-command") || args.includes("--dry-run")) {
|
|
732
792
|
try { return console.log(JSON.stringify(launcherPreview("kimi", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
733
793
|
catch { fail("cannot preview launch: invalid arguments or configuration (details withheld to protect credentials)"); }
|
|
734
794
|
}
|
|
735
|
-
|
|
736
|
-
const
|
|
795
|
+
assertLifecycleAvailable();
|
|
796
|
+
const { one, rest } = takeFlags(args, ["--mode", "--model"], []);
|
|
797
|
+
const mode = one["--mode"] ?? startModeOf(projectConfig().peers, "kimi"), model = one["--model"];
|
|
798
|
+
if (rest.length || !["tui", "headless"].includes(mode)) fail("usage: ahub kimi [--mode tui|headless] [--model <alias>]");
|
|
799
|
+
if (mode === "tui" && one["--mode"] === undefined && !(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
800
|
+
if (model) fail("Kimi's TUI needs a terminal for these options; run in a terminal or add --mode headless");
|
|
801
|
+
const hub = await connect();
|
|
802
|
+
try {
|
|
803
|
+
const reply = await hub.request({ t: "peer_start", peer: "kimi" }, 35_000);
|
|
804
|
+
if (!reply.ok) fail(reply.error);
|
|
805
|
+
return console.log(reply.already ? "kimi is already attached" : `kimi: opened a terminal running ${reply.command}; it attaches pull-only when its TUI is ready`);
|
|
806
|
+
} finally { hub.close(); }
|
|
807
|
+
}
|
|
737
808
|
const hub = await connect();
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
809
|
+
try {
|
|
810
|
+
if (mode === "tui") {
|
|
811
|
+
const launch = buildKimiTuiLaunch(projectConfig().kimi_cmd, model);
|
|
812
|
+
const snapshot = (await hub.request({ t: "status" })).status;
|
|
813
|
+
if (snapshot?.roster?.find((row: any) => row.peer === "kimi")?.on === false) fail("kimi is off roster; add it with ahub roster add kimi");
|
|
814
|
+
const peer = snapshot?.peers?.kimi;
|
|
815
|
+
if (peer && !["offline"].includes(peer.state) && !peer.pullOnly) fail("Kimi is running headless; run ahub stop kimi before starting its TUI");
|
|
816
|
+
const admission = await hub.request({ t: "start", peer: "kimi", args: { mode: "tui" }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
817
|
+
if (!admission.ok) fail(admission.error);
|
|
818
|
+
let result: ReturnType<typeof spawnSync>;
|
|
819
|
+
try {
|
|
820
|
+
const written = configureKimiMcp(cwd, stateDir, resolve(import.meta.dir, "../../plugins/agent-hub/server.js"), "tui");
|
|
821
|
+
if (written.tracked) console.error(".kimi-code/mcp.json is tracked; the agent-hub entry contains machine paths. Review this local change before committing it.");
|
|
822
|
+
result = spawnSync(launch.cmd, launch.args, { cwd, stdio: "inherit", env: { ...nativeLaunchEnv("kimi", childEnv()), AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } });
|
|
823
|
+
} finally {
|
|
824
|
+
if (admission.admissionId) await hub.request({ t: "kimi_launch_end", admissionId: admission.admissionId }).catch(() => {});
|
|
825
|
+
}
|
|
826
|
+
if (result.error) fail(`cannot run ${launch.cmd}: ${result.error.message}`);
|
|
827
|
+
process.exit(result.status ?? 1);
|
|
828
|
+
}
|
|
829
|
+
const peer = (await hub.request({ t: "status" })).status?.peers?.kimi;
|
|
830
|
+
if (peer?.pullOnly && peer.state !== "offline") fail("Kimi runs in a terminal; end it there before starting a headless one");
|
|
831
|
+
configureKimiMcp(cwd, stateDir, resolve(import.meta.dir, "../../plugins/agent-hub/server.js"), "headless");
|
|
832
|
+
const res = await hub.request({ t: "start", peer: "kimi", args: { mode: "headless", model }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
833
|
+
if (!res.ok) fail(res.error);
|
|
834
|
+
console.log(res.already ? "kimi is already attached" : 'kimi attached (headless). Talk to it with: ahub say @kimi "..."');
|
|
835
|
+
} finally { hub.close(); }
|
|
742
836
|
},
|
|
743
837
|
|
|
744
838
|
pi: async () => {
|
|
@@ -839,7 +933,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
839
933
|
hub.close();
|
|
840
934
|
if (!res.ok) fail(res.error);
|
|
841
935
|
if (res.recorded) return console.log("recorded only ([FYI]); no peer was interrupted");
|
|
842
|
-
console.log(res.targets.length ? `queued for: ${res.targets.join(", ")}` : "no peers attached; nothing delivered");
|
|
936
|
+
console.log(paint([{ text: res.targets.length ? `queued for: ${res.targets.join(", ")}` : "no peers attached; nothing delivered" }], false));
|
|
937
|
+
if (typeof res.notice === "string" && res.notice) console.log(paint([{ text: res.notice }], false));
|
|
843
938
|
},
|
|
844
939
|
|
|
845
940
|
queue: async () => {
|
|
@@ -873,6 +968,30 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
873
968
|
} finally { hub.close(); }
|
|
874
969
|
},
|
|
875
970
|
|
|
971
|
+
watch: async () => {
|
|
972
|
+
const peer = args[0];
|
|
973
|
+
if (peer !== "kimi" && peer !== "qwen") fail("usage: ahub watch kimi|qwen [--no-thoughts] [--color=auto|always|never]");
|
|
974
|
+
if (args.slice(1).some(arg => arg !== "--no-thoughts" && !/^--color=(auto|always|never)$/.test(arg))) fail("unsupported watch option");
|
|
975
|
+
const color = resolveColor(args.find(arg => arg.startsWith("--color="))?.slice(8), { isTTY: !!process.stdout.isTTY, ...process.env });
|
|
976
|
+
if (color instanceof Error) fail(color.message);
|
|
977
|
+
const hub = await connect();
|
|
978
|
+
const input = process.stdin;
|
|
979
|
+
const raw = !!input.isRaw;
|
|
980
|
+
let leave = () => {};
|
|
981
|
+
const closed = new Promise<void>(resolve => { leave = resolve; });
|
|
982
|
+
const key = (chunk: Buffer) => { if (chunk.includes(3) || chunk.toString().includes("q")) leave(); };
|
|
983
|
+
hub.onClose = leave;
|
|
984
|
+
process.on("SIGINT", leave);
|
|
985
|
+
try {
|
|
986
|
+
await attachWatch(hub, peer, line => console.log(line), { columns: () => process.stdout.columns || 80, color, noThoughts: args.includes("--no-thoughts") });
|
|
987
|
+
if (input.isTTY) { input.setRawMode(true); input.on("data", key); input.resume(); }
|
|
988
|
+
await closed;
|
|
989
|
+
} finally {
|
|
990
|
+
input.off("data", key); process.off("SIGINT", leave);
|
|
991
|
+
if (input.isTTY) input.setRawMode(raw);
|
|
992
|
+
input.pause(); hub.close();
|
|
993
|
+
}
|
|
994
|
+
},
|
|
876
995
|
tail: async () => {
|
|
877
996
|
const hub = await connect();
|
|
878
997
|
hub.onPush = (msg) => {
|
|
@@ -930,28 +1049,122 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
930
1049
|
printOutput(renderBudget(data, options.columns, Date.now(), options.full), options.color);
|
|
931
1050
|
},
|
|
932
1051
|
|
|
1052
|
+
backlog: async () => {
|
|
1053
|
+
const [sub, id, revision, value, ...reason] = args;
|
|
1054
|
+
if (sub === "list" && args.length === 1) return console.log(await taskOp("hub_backlog_list", {}));
|
|
1055
|
+
if (sub === "add" && args.length >= 3) return console.log(await taskOp("hub_task_propose", { class: id, title: args.slice(2).join(" "), backlog: true }));
|
|
1056
|
+
if (!/^[1-9]\d*$/.test(id ?? "")) fail("usage: ahub backlog list|add <class> <title...>|pull|return <id>|plan|pin <id> <revision> <value> <reason...>|undo <id> <revision>");
|
|
1057
|
+
if (["pull", "return"].includes(sub ?? "") && args.length === 2) return console.log(await taskOp(sub === "pull" ? "backlog_pull" : "backlog_return", { id }));
|
|
1058
|
+
if (!/^\d+$/.test(revision ?? "")) fail("planning revision is required");
|
|
1059
|
+
if (sub === "pull-ready" && args.length >= 4) return console.log(await taskOp("hub_backlog_pull", { id: Number(id), revision: Number(revision), reason: args.slice(3).join(" ") }));
|
|
1060
|
+
if (sub === "split" && value && reason.length) {
|
|
1061
|
+
let parts: unknown;try { parts=JSON.parse(value); } catch { return fail("split parts must be JSON"); }
|
|
1062
|
+
return console.log(await taskOp("hub_backlog_split", { id: Number(id), revision: Number(revision), parts, reason: reason.join(" ") }));
|
|
1063
|
+
}
|
|
1064
|
+
if (sub === "undo" && args.length === 3) return console.log(await taskOp("hub_backlog_undo", { id: Number(id), revision: Number(revision) }));
|
|
1065
|
+
if (!["plan", "pin"].includes(sub ?? "") || !value || !reason.length) fail("planning needs fields and a reason");
|
|
1066
|
+
let fields: unknown;
|
|
1067
|
+
try { fields = sub === "pin" ? { pinned: value === "true" ? true : value === "false" ? false : undefined } : JSON.parse(value); }
|
|
1068
|
+
catch { return fail("planning fields must be JSON"); }
|
|
1069
|
+
if (sub === "pin" && value !== "true" && value !== "false") fail("pin must be true or false");
|
|
1070
|
+
console.log(await taskOp("hub_backlog_plan", { id: Number(id), revision: Number(revision), fields, reason: reason.join(" ") }));
|
|
1071
|
+
},
|
|
1072
|
+
|
|
1073
|
+
release: async () => {
|
|
1074
|
+
const [sub, name, revision, value] = args;
|
|
1075
|
+
if (sub === "list" && args.length === 1) return console.log(await taskOp("hub_release_list", {}));
|
|
1076
|
+
if (sub === "show" && name && args.length === 2) {
|
|
1077
|
+
return console.log(await taskOp("release_show", { name }));
|
|
1078
|
+
}
|
|
1079
|
+
if (sub === "new" && name && revision && args.length === 4 && /^\d+$/.test(value ?? "")) return console.log(await taskOp("release_new", { name, goal: revision, order: Number(value) }));
|
|
1080
|
+
if (!name || !/^\d+$/.test(revision ?? "")) fail("usage: ahub release new <name> <goal> <order>|list|show <name>|plan <name> <revision> <dates-json>|activate|freeze|ship|drop <name> <revision>");
|
|
1081
|
+
const states: Record<string, string> = { activate: "active", freeze: "frozen", ship: "shipped", drop: "dropped" };
|
|
1082
|
+
let fields: unknown;
|
|
1083
|
+
if (sub === "plan" && value && args.length === 4) {
|
|
1084
|
+
try { fields = JSON.parse(value); } catch { return fail("release dates must be JSON"); }
|
|
1085
|
+
} else if (sub && Object.hasOwn(states, sub) && args.length === 3) fields = { state: states[sub] };
|
|
1086
|
+
else return fail("invalid release command");
|
|
1087
|
+
console.log(await taskOp("release_change", { name, revision: Number(revision), fields }));
|
|
1088
|
+
},
|
|
1089
|
+
|
|
933
1090
|
board: async () => {
|
|
1091
|
+
if (args[0] === "import") {
|
|
1092
|
+
if (args[1] !== "github" || (args.length - 2) % 2 !== 0) fail("usage: ahub board import github --label <label> [--milestone <milestone>] [--repo <owner/repo>]");
|
|
1093
|
+
const selection: Record<string, string> = {};
|
|
1094
|
+
for (let i = 2; i < args.length; i += 2) { const key = args[i]!, value = args[i+1]; if (!["--repo", "--label", "--milestone"].includes(key) || !value || Object.hasOwn(selection, key.slice(2))) fail("invalid GitHub import options"); selection[key.slice(2)] = value; }
|
|
1095
|
+
if (!selection.label) fail("GitHub import requires --label");
|
|
1096
|
+
return console.log(await taskOp("github_import", selection));
|
|
1097
|
+
}
|
|
934
1098
|
const options = outputOptions();
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
|
|
1099
|
+
const { one, rest } = takeFlags(args, ["--release", "--release-id"], []);
|
|
1100
|
+
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>");
|
|
1101
|
+
const release = one["--release-id"] ? "#"+one["--release-id"] : one["--release"];
|
|
1102
|
+
const boardHub = await connect();
|
|
1103
|
+
try {
|
|
1104
|
+
let boardStatus: any;
|
|
1105
|
+
if (release) { const reply = await boardHub.request({ t: "status" }); boardStatus = reply.status; if (!supportsBoardReleaseFilter(boardStatus)) fail("running hub does not support release filters; use a compatible daemon"); }
|
|
1106
|
+
const ready = rest.includes("--ready"), byColumn = rest.includes("--columns");
|
|
1107
|
+
const state = rest.find((a) => !a.startsWith("--"));
|
|
1108
|
+
if (byColumn && !boardStatus) boardStatus = (await boardHub.request({ t: "status" })).status;
|
|
1109
|
+
if (byColumn && !supportsBoardColumnMetadata(boardStatus)) fail("running hub does not support accurate board column metadata; use a compatible daemon");
|
|
1110
|
+
const tasks = JSON.parse(await taskOp("hub_task_list", { ...(ready ? { ready: true } : state ? { state } : {}), ...(release ? { release } : {}), ...(byColumn ? { columnMetadata: true } : {}) }, false, boardHub)) as any[];
|
|
1111
|
+
if (options.json && !byColumn) return console.log(JSON.stringify(tasks, null, 2));
|
|
939
1112
|
let allTasks: any[] | null = tasks;
|
|
940
|
-
if (ready || state) {
|
|
941
|
-
try { allTasks = JSON.parse(await taskOp("hub_task_list", {}, true)) as any[]; }
|
|
1113
|
+
if (ready || state || release) {
|
|
1114
|
+
try { allTasks = JSON.parse(await taskOp("hub_task_list", byColumn ? { columnMetadata: true } : {}, true, boardHub)) as any[]; }
|
|
942
1115
|
catch (error) {
|
|
1116
|
+
if (byColumn) fail("board column dependency context unavailable; retry with a complete snapshot");
|
|
943
1117
|
allTasks = null;
|
|
944
1118
|
console.error(paint([{ text: `ahub: board dependency stages unavailable; showing filtered rows without stages: ${String((error as Error).message).replace(/[\n\t]/g, " ")}` }], false));
|
|
945
1119
|
}
|
|
946
1120
|
}
|
|
947
|
-
|
|
1121
|
+
if (byColumn) {
|
|
1122
|
+
const status = (await boardHub.request({ t: "status" })).status; if (!status?.peers) fail("peer status unavailable"); if (options.json) console.log(JSON.stringify({ tasks, ...boardColumnModel(tasks, status.peers, allTasks) }, null, 2)); else printOutput(renderBoardColumns(tasks, status.peers, options.columns, Date.now(), options.full, allTasks), options.color);
|
|
1123
|
+
} else printOutput(renderBoard(tasks, options.columns, Date.now(), options.full, allTasks), options.color);
|
|
1124
|
+
} finally { boardHub.close(); }
|
|
1125
|
+
},
|
|
1126
|
+
|
|
1127
|
+
sdlc: async () => {
|
|
1128
|
+
const sub = args[0], id = args[1] === undefined ? undefined : Number(args[1]);
|
|
1129
|
+
if (sub === "queue" || sub === "release") {
|
|
1130
|
+
const repo = args[1], liveIssues = args.slice(2).map(Number);
|
|
1131
|
+
if (!repo || args.length > 34 || (sub === "queue" && args.length !== 2) || liveIssues.some(id => !Number.isSafeInteger(id) || id <= 0)) fail("usage: ahub sdlc queue <owner/repository> | release <owner/repository> [live-issue...]");
|
|
1132
|
+
const hub = await connect();
|
|
1133
|
+
try { const reply = await hub.request({ t: "sdlc", op: sub, repo, liveIssues }, 30_000); if (!reply.ok) fail(reply.error); console.log(JSON.stringify(reply, null, 2)); }
|
|
1134
|
+
finally { hub.close(); }
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (sub === "record") {
|
|
1138
|
+
if (args.length !== 3 || !Number.isSafeInteger(id) || id! <= 0 || Buffer.byteLength(args[2]!) > 32768) fail("usage: ahub sdlc record <task> '<receipt JSON>'");
|
|
1139
|
+
let receipt: unknown; try { receipt = JSON.parse(args[2]!); } catch { fail("receipt must be valid JSON"); }
|
|
1140
|
+
return console.log(await taskOp("hub_sdlc_record", { id, receipt }));
|
|
1141
|
+
}
|
|
1142
|
+
if (!["status", "explain", "export"].includes(sub ?? "") || args.length > 2 || (["explain", "export"].includes(sub ?? "") && id === undefined) || (id !== undefined && (!Number.isSafeInteger(id) || id <= 0))) fail("usage: ahub sdlc status [task] | explain <task> | export <task>");
|
|
1143
|
+
const hub = await connect();
|
|
1144
|
+
try {
|
|
1145
|
+
const reply = await hub.request({ t: "sdlc", op: sub, ...(id === undefined ? {} : { id }) }, 30_000);
|
|
1146
|
+
if (!reply.ok) fail(reply.error);
|
|
1147
|
+
console.log(JSON.stringify(reply, null, 2));
|
|
1148
|
+
} finally { hub.close(); }
|
|
948
1149
|
},
|
|
949
1150
|
|
|
950
1151
|
task: async () => {
|
|
951
1152
|
const [sub, ...rest] = args;
|
|
1153
|
+
if (sub === "role") {
|
|
1154
|
+
if (rest.length !== 2 || !/^[1-9]\d*$/.test(rest[0]!)) fail("usage: ahub task role <id> <role|none>");
|
|
1155
|
+
const hub = await connect();
|
|
1156
|
+
try { const reply = await hub.request({ t: "task_catalog_role", id: Number(rest[0]), role: rest[1] === "none" ? null : rest[1] }); if (!reply.ok) fail(reply.error); console.log(reply.text); }
|
|
1157
|
+
finally { hub.close(); }
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
952
1160
|
if (sub === "show") return console.log(await taskOp("task_show", { id: rest[0] }));
|
|
953
1161
|
if (sub === "escalate") return console.log(await taskOp("task_escalate", { id: rest[0] }));
|
|
954
|
-
if (sub === "assign")
|
|
1162
|
+
if (sub === "assign") {
|
|
1163
|
+
const { one, rest: positional } = takeFlags(rest, ["--role"], []);
|
|
1164
|
+
const role = one["--role"] ?? "owner";
|
|
1165
|
+
if (positional.length !== 2 || !["owner", "reviewer"].includes(role)) fail("usage: ahub task assign <id> <peer> [--role owner|reviewer]");
|
|
1166
|
+
return console.log(await taskOp("task_assign", { id: positional[0], peer: positional[1], ...(role === "reviewer" ? { role } : {}) }));
|
|
1167
|
+
}
|
|
955
1168
|
if (sub === "label") {
|
|
956
1169
|
// #247: a person's later verdict on an approved task (the identity gate keeps it human-only), for research records.
|
|
957
1170
|
const { one, rest: positional } = takeFlags(rest, ["--created"], []);
|
|
@@ -966,20 +1179,20 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
966
1179
|
if (sub !== "propose" || rest.length < 1) fail("usage: ahub task propose [<class>] <title...> | show <id> | assign <id> <peer> | escalate <id>");
|
|
967
1180
|
// `--class` is the explicit form. A first word that is a class name is still taken as the class (the documented
|
|
968
1181
|
// short form), but said out loud: "review the auth module" would otherwise be filed as class review, silently.
|
|
969
|
-
const urgent = rest.includes("--urgent");
|
|
970
|
-
const flags = takeFlags(rest.filter((a) => a !== "--urgent"), ["--owner", "--detail", "--class"], ["--path", "--after"]);
|
|
971
|
-
const positional = !flags.one["--class"] && (CLASSES as readonly string[]).includes(flags.rest[0] ?? "") && flags.rest.length > 1;
|
|
1182
|
+
const urgent = rest.includes("--urgent"), backlog = rest.includes("--backlog");
|
|
1183
|
+
const flags = takeFlags(rest.filter((a) => a !== "--urgent" && a !== "--backlog"), ["--owner", "--detail", "--class"], ["--path", "--after"]);
|
|
1184
|
+
const positional = !flags.one["--class"] && ((CLASSES as readonly string[]).includes(flags.rest[0] ?? "") || Object.hasOwn((() => { const config = projectConfig(); return resolveProfile(config.profile, config.profile_overrides); })().classes, flags.rest[0] ?? "")) && flags.rest.length > 1;
|
|
972
1185
|
const cls = flags.one["--class"] ?? (positional ? flags.rest[0] : undefined);
|
|
973
1186
|
const title = (positional ? flags.rest.slice(1) : flags.rest).join(" ");
|
|
974
1187
|
if (positional) console.error(`note: "${cls}" was taken as the class and left out of the title; use --class <c> when the title itself starts with that word`);
|
|
975
|
-
console.log(await taskOp("hub_task_propose", { ...(cls ? { class: cls } : {}), title, owner: flags.one["--owner"], detail: flags.one["--detail"], ...(flags.many["--path"]?.length ? { refs: { paths: flags.many["--path"] } } : {}), ...(flags.many["--after"]?.length ? { after: flags.many["--after"].map(Number) } : {}), ...(urgent ? { urgent: true } : {}) }));
|
|
1188
|
+
console.log(await taskOp("hub_task_propose", { ...(cls ? { class: cls } : {}), title, owner: flags.one["--owner"], detail: flags.one["--detail"], ...(flags.many["--path"]?.length ? { refs: { paths: flags.many["--path"] } } : {}), ...(flags.many["--after"]?.length ? { after: flags.many["--after"].map(Number) } : {}), ...(urgent ? { urgent: true } : {}), ...(backlog ? { backlog: true } : {}) }));
|
|
976
1189
|
},
|
|
977
1190
|
|
|
978
1191
|
review: async () => {
|
|
979
|
-
const { many, rest } = takeFlags(args, [], ["--unmet"]);
|
|
1192
|
+
const { one, many, rest } = takeFlags(args, ["--head"], ["--unmet"]);
|
|
980
1193
|
const [id, verdict, ...note] = rest;
|
|
981
1194
|
if (!id || !verdict) fail("usage: ahub review <id> approved|changes_requested [note...] [--unmet <item>]...");
|
|
982
|
-
console.log(await taskOp("hub_review", { id: Number(id), verdict, note: note.join(" "), ...(many["--unmet"]?.length ? { unmet: many["--unmet"] } : {}) }));
|
|
1195
|
+
console.log(await taskOp("hub_review", { id: Number(id), verdict, ...(one["--head"] ? { head: one["--head"] } : {}), note: note.join(" "), ...(many["--unmet"]?.length ? { unmet: many["--unmet"] } : {}) }));
|
|
983
1196
|
},
|
|
984
1197
|
|
|
985
1198
|
remember: async () => console.log(await taskOp("hub_remember", { text: freeText(args, "ahub remember <text>") })),
|
|
@@ -1047,16 +1260,42 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1047
1260
|
// #269: the terminal's side of the settings registry. Everything the dashboard's Settings section can do, and the
|
|
1048
1261
|
// same refusals; it talks to the running hub, so a setting is validated and announced in one place.
|
|
1049
1262
|
// ponytail: needs a running hub, also for a value read only at hub start; a file-only path if that gets in the way.
|
|
1263
|
+
roster: async () => {
|
|
1264
|
+
const options = outputOptions(), flags = args.filter(arg => arg.startsWith("--")), positional = outputArguments().filter(arg => arg !== "--yes" && arg !== "--now");
|
|
1265
|
+
const [sub = "list", peer, ...extra] = positional;
|
|
1266
|
+
if (!["list", "add", "remove"].includes(sub) || extra.length || (sub === "list" ? peer !== undefined : !isSupportedPeer(peer)) || flags.some(flag => !["--json", "--full", "--yes", "--now"].includes(flag) && !flag.startsWith("--color=")) || (sub !== "list" && options.json) || (sub !== "remove" && flags.some(flag => ["--yes", "--now"].includes(flag)))) fail("usage: ahub roster [--json] | add <peer> | remove <peer> [--yes] [--now]");
|
|
1267
|
+
const hub = await connect();
|
|
1268
|
+
try {
|
|
1269
|
+
const request = async (message: Record<string, unknown>) => { const reply = await hub.request(message, 35_000); if (reply.ok !== true) fail(String(reply.error ?? "roster request refused")); return reply; };
|
|
1270
|
+
if (sub === "list") {
|
|
1271
|
+
const view = await request({ t: "settings_get" });
|
|
1272
|
+
if (!Array.isArray(view.roster)) fail("upgrade the running hub to use ahub roster");
|
|
1273
|
+
return options.json ? console.log(JSON.stringify(view.roster, null, 2)) : printOutput(renderRoster(view.roster, options.columns), options.color);
|
|
1274
|
+
}
|
|
1275
|
+
const key = `roster.${peer}`, value = sub === "add";
|
|
1276
|
+
const preview = await request({ t: "settings_preview", key, value });
|
|
1277
|
+
printOutput((preview.lines ?? []).map((text: string) => [{ text }]), options.color);
|
|
1278
|
+
if (sub === "remove" && !flags.includes("--yes")) {
|
|
1279
|
+
if (!process.stdin.isTTY) fail("not a terminal: nothing was changed; rerun with --yes after reviewing the preview");
|
|
1280
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1281
|
+
let answer: string; try { answer = await rl.question(`Remove ${peer}? [y/N] `); } finally { rl.close(); }
|
|
1282
|
+
if (!/^(y|yes)$/i.test(answer.trim())) return console.log("nothing was changed");
|
|
1283
|
+
}
|
|
1284
|
+
const result = await request({ t: "settings_set", key, value, ...(sub === "remove" ? { confirm: peer, now: flags.includes("--now") } : {}) });
|
|
1285
|
+
printOutput([[{ text: result.text }]], options.color);
|
|
1286
|
+
} finally { hub.close(); }
|
|
1287
|
+
},
|
|
1050
1288
|
settings: async () => {
|
|
1051
1289
|
const usage = "usage: ahub settings [list] [--json] | get <key> [--json] | set <key> <value|inherit> [--yes] [--preview] | undo";
|
|
1052
1290
|
const flags = args.filter((arg) => arg.startsWith("--")), [sub = "list", key, text, ...extra] = args.filter((arg) => !arg.startsWith("--"));
|
|
1053
|
-
const allowed: Record<string, string[]> = { list: ["--json"], get: ["--json"], set: ["--yes", "--preview"], undo: [] };
|
|
1291
|
+
const allowed: Record<string, string[]> = { list: ["--json"], get: ["--json"], set: ["--yes", "--preview", "--now"], undo: [] };
|
|
1054
1292
|
if (!Object.hasOwn(allowed, sub) || flags.some((flag) => !allowed[sub]!.includes(flag)) || extra.length) fail(usage);
|
|
1055
1293
|
if ((sub === "get" && (!key || text !== undefined)) || (sub === "set" && (!key || text === undefined)) || ((sub === "list" || sub === "undo") && key !== undefined)) fail(usage);
|
|
1056
1294
|
const def = key === undefined ? undefined : settingDef(key);
|
|
1057
1295
|
if (typeof def === "string") fail(`${key}: ${def}`);
|
|
1296
|
+
if (flags.includes("--now") && (typeof def !== "object" || def.group !== "Roster")) fail(usage);
|
|
1058
1297
|
const value = sub === "set" ? parseSettingText(def!, text!) : undefined;
|
|
1059
|
-
const peer = def?.group === "Permissions" ? def.path.at(-1)! : undefined;
|
|
1298
|
+
const peer = def?.group === "Permissions" || def?.group === "Roster" ? def.path.at(-1)! : undefined;
|
|
1060
1299
|
if (sub === "set" && value === "never-ask" && !flags.includes("--preview")) {
|
|
1061
1300
|
if (!flags.includes("--yes")) fail("never-ask requires --yes; nothing was changed");
|
|
1062
1301
|
console.error(`never-ask: ${permissionBoundary(peer!)}; approval prompts are disabled`);
|
|
@@ -1071,19 +1310,29 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1071
1310
|
}
|
|
1072
1311
|
return reply;
|
|
1073
1312
|
};
|
|
1074
|
-
if (sub === "undo")
|
|
1313
|
+
if (sub === "undo") {
|
|
1314
|
+
const view = await request({ t: "settings_get" }), undoDef = settingDef(view.undo);
|
|
1315
|
+
const rosterPeer = typeof undoDef !== "string" && undoDef.group === "Roster" ? undoDef.path[1] : undefined;
|
|
1316
|
+
let confirm: string | undefined;
|
|
1317
|
+
if (rosterPeer) {
|
|
1318
|
+
if (!process.stdin.isTTY) fail("undoing a roster setting needs confirmation in a terminal; use ahub settings set for an explicit scripted change");
|
|
1319
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1320
|
+
try { confirm = (await rl.question(`Type ${rosterPeer} to confirm this roster undo: `)).trim(); } finally { rl.close(); }
|
|
1321
|
+
}
|
|
1322
|
+
return console.log(paint([{ text: (await request({ t: "settings_undo", ...(confirm ? { confirm } : {}) })).text }], false));
|
|
1323
|
+
}
|
|
1075
1324
|
if (sub === "set" && flags.includes("--preview")) {
|
|
1076
1325
|
const preview = await request({ t: "settings_preview", key, value });
|
|
1077
|
-
for (const line of preview.lines ?? []) console.log(line);
|
|
1326
|
+
for (const line of preview.lines ?? []) console.log(paint([{ text: line }], false));
|
|
1078
1327
|
// Each side is `route explain`: the task line, "if it were assigned now:", then the trace.
|
|
1079
1328
|
for (const task of preview.tasks ?? []) console.log([task.before[0], " before:", ...task.before.slice(2).map((line: string) => ` ${line}`), " after:", ...task.after.slice(2).map((line: string) => ` ${line}`)].join("\n"));
|
|
1080
1329
|
if (preview.hidden) console.log(`${preview.hidden} more open tasks are not shown`);
|
|
1081
1330
|
return console.log("preview only; nothing was changed");
|
|
1082
1331
|
}
|
|
1083
1332
|
if (sub === "set") {
|
|
1084
|
-
const reply = await request({ t: "settings_set", key, value, ...(flags.includes("--yes") && peer ? { confirm: peer } : {}) });
|
|
1085
|
-
console.log(reply.text);
|
|
1086
|
-
if (typeof reply.note === "string" && reply.note) console.log(reply.note);
|
|
1333
|
+
const reply = await request({ t: "settings_set", key, value, now: flags.includes("--now"), ...(flags.includes("--yes") && peer ? { confirm: peer } : {}) });
|
|
1334
|
+
console.log(paint([{ text: reply.text }], false));
|
|
1335
|
+
if (typeof reply.note === "string" && reply.note) console.log(paint([{ text: reply.note }], false));
|
|
1087
1336
|
return;
|
|
1088
1337
|
}
|
|
1089
1338
|
const view = await request({ t: "settings_get" });
|
|
@@ -1093,9 +1342,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1093
1342
|
let group = "";
|
|
1094
1343
|
for (const row of rows) {
|
|
1095
1344
|
if (sub === "list" && row.group !== group) console.log(`${group ? "\n" : ""}${group = row.group}`);
|
|
1096
|
-
const facts = [`from ${row.source}`, row.applies === "live" ? "applies at once" : row.applies === "peer start" ? "applies at the peer's next start" : "read at hub start", ...(row.pending !== undefined ? [`${settingText(row.pending)} at the next hub start`] : []), ...(row.note ? [row.note] : [])];
|
|
1345
|
+
const facts = [`from ${row.source}`, row.applies === "live" ? "applies at once" : row.applies === "peer start" ? "applies at the peer's next start" : "read at hub start", ...(row.pending !== undefined ? [`${settingText(row.pending)} at the next ${row.applies === "peer start" ? "peer" : "hub"} start`] : []), ...(row.note ? [row.note] : [])];
|
|
1097
1346
|
console.log(` ${row.key.padEnd(width)} ${settingText(row.value).padEnd(valueWidth)} ${facts.join("; ")}`);
|
|
1098
|
-
if (sub === "get") console.log(` ${row.label}; written to ${row.file}; takes ${row.type === "boolean" ? "true, false" : row.type === "peers" ? "a comma list of peer ids" : (row.values ?? []).join(", ")}${row.file === "this hub run" ? "" : ", or inherit to remove the machine-local value"}${row.risk === "raises" ? "; a dashboard needs a settings session (ahub ui --settings) to raise it" : ""}`);
|
|
1347
|
+
if (sub === "get") console.log(` ${row.label}; written to ${row.file}; takes ${row.type === "boolean" ? "true, false" : row.type === "peers" ? "a comma list of peer ids" : row.type === "roles" ? "up to two comma-separated catalog ids" : (row.values ?? []).join(", ")}${row.file === "this hub run" ? "" : ", or inherit to remove the machine-local value"}${row.risk === "raises" ? "; a dashboard needs a settings session (ahub ui --settings) to raise it" : ""}`);
|
|
1099
1348
|
}
|
|
1100
1349
|
if (sub === "list" && view.undo) console.log(`\nahub settings undo puts back the last write (${view.undo})`);
|
|
1101
1350
|
} finally { hub.close(); }
|
|
@@ -1110,6 +1359,15 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1110
1359
|
hub.close();
|
|
1111
1360
|
},
|
|
1112
1361
|
|
|
1362
|
+
pet: () => {
|
|
1363
|
+
if (args.some(arg => arg !== "--short")) return fail("usage: ahub pet [--short]");
|
|
1364
|
+
const short = args.includes("--short");
|
|
1365
|
+
let character: import("../pet/frame.ts").CharacterId | undefined;
|
|
1366
|
+
try { character = loadPetConfig(cwd).character; } catch { /* explicit structural reads remain available with malformed presentation config */ }
|
|
1367
|
+
const text = renderPet(readPet(stateDir), short, character);
|
|
1368
|
+
process.stdout.write(short ? text : text + "\n");
|
|
1369
|
+
},
|
|
1370
|
+
|
|
1113
1371
|
status: async () => {
|
|
1114
1372
|
const options = outputOptions();
|
|
1115
1373
|
if (args.includes("--all")) return printProjects(options);
|
|
@@ -1119,6 +1377,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1119
1377
|
if (options.json) return console.log(JSON.stringify(status, null, 2));
|
|
1120
1378
|
const quota = await hub.request({ t: "budget" });
|
|
1121
1379
|
printOutput(renderStatus({ ...status, budget: quota.budget ?? {} }, options.columns, Date.now(), options.full), options.color);
|
|
1380
|
+
if (status.needs) { const pet = loadPetConfig(cwd); if (pet.enabled) console.log(renderPet(petView(status), false, pet.character)); }
|
|
1381
|
+
// #258: open long-term tasks and who holds them, as of this machine's last memory sync.
|
|
1382
|
+
if (existsSync(join(hubHome(), "memory", "index.db"))) {
|
|
1383
|
+
try { const lines = await goalCommand(["list"], cwd); if (!lines.startsWith("no open")) console.log(`\nlong-term tasks\n${lines}`); } catch { /* memory unreadable: status goes on */ }
|
|
1384
|
+
}
|
|
1122
1385
|
} finally { hub.close(); }
|
|
1123
1386
|
},
|
|
1124
1387
|
|
|
@@ -1131,18 +1394,26 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1131
1394
|
const json = args.includes("--json");
|
|
1132
1395
|
const [sub, ...rest] = args.filter((a) => a !== "--json");
|
|
1133
1396
|
if (sub === "run") {
|
|
1134
|
-
const { one, rest: positional } = takeFlags(rest, ["--arm", "--repeat", "--tasks"], []);
|
|
1397
|
+
const { one, many, rest: positional } = takeFlags(rest, ["--arm", "--repeat", "--tasks", "--memory"], ["--addon"]);
|
|
1398
|
+
if (one["--memory"] !== undefined && !(MEMORY_MODES as readonly string[]).includes(one["--memory"])) fail(`--memory takes ${MEMORY_MODES.join(", ")}`);
|
|
1399
|
+
// #248: `--addon ripwire=off` declares the arm's add-on state; the run refuses peers that do not match it.
|
|
1400
|
+
const addons: Partial<Record<AddonName, "on" | "off">> = {};
|
|
1401
|
+
for (const spec of many["--addon"] ?? []) {
|
|
1402
|
+
const [name, state] = spec.split("=");
|
|
1403
|
+
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(", ")}`);
|
|
1404
|
+
addons[name] = state;
|
|
1405
|
+
}
|
|
1135
1406
|
const suite = positional[0];
|
|
1136
1407
|
const repeat = one["--repeat"] === undefined ? 1 : Number(one["--repeat"]);
|
|
1137
1408
|
if (!suite || positional.length !== 1 || !one["--arm"]?.trim() || !Number.isSafeInteger(repeat) || repeat < 1 || repeat > 100) {
|
|
1138
|
-
fail("usage: ahub bench run <suite.json> --arm <label> [--repeat 1..100] [--tasks a,b]");
|
|
1409
|
+
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]");
|
|
1139
1410
|
}
|
|
1140
1411
|
await benchPreflight(cwd, loadConfig(cwd).bench.enabled); // the suite's refs are checked once it is read
|
|
1141
1412
|
let interrupted = false;
|
|
1142
1413
|
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)"); };
|
|
1143
1414
|
process.on("SIGINT", stop);
|
|
1144
1415
|
try {
|
|
1145
|
-
const run = await runBench({ cwd, stateDir, suitePath: resolve(invokedFrom, suite!), arm: one["--arm"]!.trim(), repeat, only: one["--tasks"]?.split(",").map((t) => t.trim()).filter(Boolean), stopped: () => interrupted }, defaultIo(stateDir, cwd));
|
|
1416
|
+
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));
|
|
1146
1417
|
return console.log(formatBenchReport(benchReport(readRuns().filter((r) => r.header.run === run))).join("\n"));
|
|
1147
1418
|
} finally { process.off("SIGINT", stop); }
|
|
1148
1419
|
}
|
|
@@ -1182,12 +1453,25 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1182
1453
|
}
|
|
1183
1454
|
fail("usage: ahub bench run|list|status|report|compare|export (ahub help bench)");
|
|
1184
1455
|
},
|
|
1456
|
+
memory: async () => {
|
|
1457
|
+
// #258: the hub's own memory on this machine; reads are anyone's, changes a person's.
|
|
1458
|
+
try { console.log(await memoryCommand(args, cwd)); } catch (e) { fail((e as Error).message); }
|
|
1459
|
+
},
|
|
1460
|
+
goal: async () => {
|
|
1461
|
+
// #258: long-term tasks; a link goes through the project's hub, so Tasks validates the board task.
|
|
1462
|
+
const link = async (goal: string, task: string) => {
|
|
1463
|
+
const hub = await connect();
|
|
1464
|
+
try { const r = await hub.request({ t: "goal_link", goal, task: Number(task) }); if (!r.ok) throw new Error(String(r.error)); return String(r.text); }
|
|
1465
|
+
finally { hub.close(); }
|
|
1466
|
+
};
|
|
1467
|
+
try { console.log(await goalCommand(args, cwd, { link })); } catch (e) { fail((e as Error).message); }
|
|
1468
|
+
},
|
|
1185
1469
|
research: () => {
|
|
1186
1470
|
// #247: measures from the opt-in research records; `export` writes them out, `backfill` builds them from events.jsonl.
|
|
1187
1471
|
const [sub] = args;
|
|
1188
1472
|
// Another project is `ahub --project <dir> research`; anything unknown is refused rather than ignored.
|
|
1189
1473
|
const flags = new Set(["--all", "--json", "--since", "--format"]), valued = new Set(["--since", "--format"]);
|
|
1190
|
-
for (let i = sub === "export" || sub === "backfill" ? 1 : 0; i < args.length; i++) {
|
|
1474
|
+
for (let i = sub === "export" || sub === "backfill" || sub === "addons" ? 1 : 0; i < args.length; i++) {
|
|
1191
1475
|
if (!flags.has(args[i]!)) fail(`ahub research: unknown argument ${args[i]} (another project: ahub --project <dir> research)`);
|
|
1192
1476
|
if (valued.has(args[i]!)) i++;
|
|
1193
1477
|
}
|
|
@@ -1205,7 +1489,12 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1205
1489
|
const kept = records.filter((r) => r.kind === "label" || Date.parse(r.approvedAt) >= from);
|
|
1206
1490
|
return void process.stdout.write(args.includes("--format") && format === "csv" ? toCsv(kept) : kept.map((r) => JSON.stringify(r)).join("\n") + (kept.length ? "\n" : ""));
|
|
1207
1491
|
}
|
|
1208
|
-
if (sub
|
|
1492
|
+
if (sub === "addons") {
|
|
1493
|
+
// #248: approved tasks with each add-on on against off, by the tooling fingerprint in their records.
|
|
1494
|
+
const rows = addonReport(records, from);
|
|
1495
|
+
return console.log(args.includes("--json") ? JSON.stringify(rows, null, 2) : formatAddons(rows).join("\n"));
|
|
1496
|
+
}
|
|
1497
|
+
if (sub !== undefined && !sub.startsWith("--")) fail("usage: ahub research [--since 30d] [--all] [--json] | addons [--since] [--all] [--json] | export [--format jsonl|csv] [--since] [--all] | backfill");
|
|
1209
1498
|
const r = researchReport(records, from);
|
|
1210
1499
|
console.log(args.includes("--json") ? JSON.stringify(r, null, 2) : formatResearch(r).join("\n"));
|
|
1211
1500
|
},
|
|
@@ -1284,9 +1573,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1284
1573
|
const { start_tree: startTree, end_tree: endTree } = turn;
|
|
1285
1574
|
if (!startTree || !endTree) fail(`turn ${id} has no ${startTree ? "end" : "start"} snapshot: the hub stopped during it, or a snapshot failed (see hub.log)`);
|
|
1286
1575
|
const repo = repoOf(cwd) ?? fail(`${cwd} is not in a git work tree`);
|
|
1287
|
-
if (!hasTree(repo
|
|
1576
|
+
if (!hasTree(repo, startTree) || !hasTree(repo, endTree)) fail(`turn ${id}'s snapshots are gone from the git object store (git gc prunes them after two weeks)`);
|
|
1288
1577
|
// Another peer's turn that overlapped this one in time has its changes in this turn's diff as well.
|
|
1289
|
-
const
|
|
1578
|
+
const undoConfig = projectConfig();
|
|
1579
|
+
const keep = Math.max(1, Number(undoConfig.snapshots.keep) || 20);
|
|
1290
1580
|
const overlapping = turnRecords((t) => t.overlapping(turn, keep), { paths: [] as string[], unknown: [] as string[] });
|
|
1291
1581
|
if (overlapping.unknown.length) fail(`refusing to undo ${id}: these turns of other peers ran at the same time and their changes are not known (still running, cut short, or not snapshotted): ${overlapping.unknown.join(", ")}`);
|
|
1292
1582
|
let reverted = false;
|
|
@@ -1304,6 +1594,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1304
1594
|
if (first.undone) return console.log(`turn ${id} is already undone: its files are as they were before it started`);
|
|
1305
1595
|
console.log(first.restore.length ? `turn ${id} changed:\n ${first.restore.join("\n ")}` : `turn ${id} changed no files`);
|
|
1306
1596
|
console.log("(every change made in the project during the turn counts as its own, including any by Claude or by you)");
|
|
1597
|
+
const projections = new TextProjections({ cwd, stateDir, config: undoConfig.text_projection ?? {}, deny: undoConfig.local.deny, readAllow: undoConfig.local.read_allow, log: line => console.error(line) });
|
|
1598
|
+
const preview = projectedDiff({ repo, from: endTree, to: startTree, paths: first.restore, projections, deny: undoConfig.local.deny, privatePath: path => matchesPolicy(cwd, join(repo.top, path), undoConfig.pii?.paths ?? []) });
|
|
1599
|
+
if (preview) console.log(terminalText("Undo preview (end snapshot to start snapshot):\n" + preview));
|
|
1600
|
+
|
|
1307
1601
|
if (!args.includes("--yes")) return console.log("nothing was changed; add --yes to restore these files");
|
|
1308
1602
|
if (args.includes("--context")) {
|
|
1309
1603
|
console.log(await taskOp("turn_revert", { turn: id }));
|
|
@@ -1406,6 +1700,32 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1406
1700
|
console.log("Claude Code sessions attached to this hub lost their hub session: relaunch them with ahub claude");
|
|
1407
1701
|
},
|
|
1408
1702
|
|
|
1703
|
+
role: async () => {
|
|
1704
|
+
const catalog = loadRoleCatalog(cwd);
|
|
1705
|
+
if (args[0] === "list") {
|
|
1706
|
+
const { one, rest } = takeFlags(args.slice(1), ["--group"], []);
|
|
1707
|
+
if (rest.length) fail("usage: ahub role list [--group <group>]");
|
|
1708
|
+
return console.log(paint([{ text: JSON.stringify({ roles: [...catalog.roles.values()].filter(role => !one["--group"] || role.group === one["--group"]).map(({ body: _body, review: _review, ...role }) => role), errors: catalog.errors }, null, 2) }], false));
|
|
1709
|
+
}
|
|
1710
|
+
if (args[0] === "show" && args.length === 2) {
|
|
1711
|
+
const role = catalog.roles.get(args[1]!); if (!role) fail("unknown catalog role; run ahub role list");
|
|
1712
|
+
return console.log(paint([{ text: JSON.stringify(role, null, 2) }], false));
|
|
1713
|
+
}
|
|
1714
|
+
if (["assign", "clear"].includes(args[0] ?? "")) {
|
|
1715
|
+
const [op, peer, id] = args;
|
|
1716
|
+
if (!peer || !isSupportedPeer(peer) || !id && op === "assign" || args.length > 3) fail("usage: ahub role assign <peer> <id> | clear <peer> [<id>]");
|
|
1717
|
+
const hub = await connect();
|
|
1718
|
+
try {
|
|
1719
|
+
const view = await hub.request({ t: "settings_get" }); if (!view.ok) fail(view.error);
|
|
1720
|
+
const row = (view.rows as SettingRow[]).find(row => row.key === `catalog_roles.${peer}`); if (!row) fail("this hub does not support catalog role settings");
|
|
1721
|
+
const current = Array.isArray(row!.pending) ? row!.pending : Array.isArray(row!.value) ? row!.value : [];
|
|
1722
|
+
const value = op === "assign" ? [...new Set([...current, id!])] : id ? current.filter(role => role !== id) : [];
|
|
1723
|
+
const reply = await hub.request({ t: "settings_set", key: row!.key, value }); if (!reply.ok) fail(reply.error); console.log(reply.text);
|
|
1724
|
+
} finally { hub.close(); }
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
fail("usage: ahub role list [--group <group>] | show <id> | assign <peer> <id> | clear <peer> [<id>]");
|
|
1728
|
+
},
|
|
1409
1729
|
doctor: async () => {
|
|
1410
1730
|
const options = outputOptions();
|
|
1411
1731
|
if (args.includes("--orphans")) return orphanDoctor(args.includes("--kill"), options);
|
|
@@ -1415,16 +1735,22 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1415
1735
|
};
|
|
1416
1736
|
const checks: DoctorCheck[] = [];
|
|
1417
1737
|
const row = (ok: boolean | undefined, name: string, detail: string) => {
|
|
1418
|
-
const section: DoctorCheck["section"] = ["bun", "claude", "codex", "kimi"].includes(name) ? "Tools"
|
|
1738
|
+
const section: DoctorCheck["section"] = ["bun", "claude", "codex", "kimi", "qwen", "jcode"].includes(name) ? "Tools"
|
|
1419
1739
|
: name === "config" || name === "retired setting" ? "Config"
|
|
1420
1740
|
: name.startsWith("memory") ? "Memory"
|
|
1421
1741
|
: ["omniroute", "omniroute key", "local fixed_model", "switchyard", "pi mlx"].includes(name) ? "Models" : "Hub";
|
|
1422
|
-
const optional = ["claude", "codex", "kimi", "ahub daemon", "switchyard", "pi mlx"].includes(name) || section === "Memory";
|
|
1742
|
+
const optional = ["claude", "codex", "kimi", "qwen", "jcode", "ahub daemon", "switchyard", "pi mlx"].includes(name) || section === "Memory";
|
|
1423
1743
|
const level: DoctorCheck["level"] = ok === undefined ? (["switchyard", "omniroute"].includes(name) ? "warn" : "unknown") : ok ? "ok" : optional ? "warn" : "fail";
|
|
1424
1744
|
checks.push({ section, level, name, detail });
|
|
1425
1745
|
};
|
|
1426
1746
|
|
|
1427
1747
|
try {
|
|
1748
|
+
const catalog = loadRoleCatalog(cwd);
|
|
1749
|
+
for (const error of catalog.errors) row(false, "catalog role", `${error.id}: ${error.reason}`);
|
|
1750
|
+
try {
|
|
1751
|
+
const config = loadConfig(cwd), assigned = catalogAssignments(config.catalog_roles);
|
|
1752
|
+
for (const peer of Object.keys(assigned)) selectedRoles(peer, config.roles, assigned, catalog);
|
|
1753
|
+
} catch { row(false, "catalog roles", "invalid assignment id, shape or flow base"); }
|
|
1428
1754
|
for (const bin of ["bun", "claude", "codex", "kimi"]) {
|
|
1429
1755
|
const v = version(bin);
|
|
1430
1756
|
row(!!v, bin, v ?? "not found on PATH");
|
|
@@ -1450,7 +1776,14 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1450
1776
|
|
|
1451
1777
|
const config = loadConfig(cwd);
|
|
1452
1778
|
row(!config.ignored, "config", config.ignored ? `${config.ignored.join("; ")} (move them to .agenthub/config.local.json)` : "no machine-local field ignored");
|
|
1453
|
-
for (const line of config.retired ?? []) row(false, "retired setting", line);
|
|
1779
|
+
for (const line of config.retired ?? []) row(false, "retired setting", line);
|
|
1780
|
+
if (config.roster.jcode === true) {
|
|
1781
|
+
row(verifiedJcodeCommand(config.jcode_cmd, cwd), "jcode", "CLI/source identity only; provider, project MCP and native teardown require separate receipts; native policy unmanaged");
|
|
1782
|
+
}
|
|
1783
|
+
if (config.roster.qwen === true) {
|
|
1784
|
+
const v = version(config.qwen_cmd[0]!);
|
|
1785
|
+
row(v === "0.25.0", "qwen", v === "0.25.0" ? "CLI reports 0.25.0; native ACP identity/modes and provider access require a separate live check" : "Qwen Code 0.25.0 executable not confirmed; provider and ACP readiness unknown");
|
|
1786
|
+
} // issue #83
|
|
1454
1787
|
const omni = new OmniRoute(config.omniroute);
|
|
1455
1788
|
const gateway = await omni.base();
|
|
1456
1789
|
row(gateway ? true : config.omniroute.urls.length || process.env.AGENTHUB_OMNIROUTE_URL ? false : undefined, "omniroute", gateway ? `${new URL(gateway).host} healthy` : config.omniroute.urls.length || process.env.AGENTHUB_OMNIROUTE_URL ? "no candidate reachable (VPN off?); ahub local cannot run" : "not configured: set omniroute.urls in .agenthub/config.local.json (any OpenAI-compatible gateway); ahub local cannot run");
|
|
@@ -1493,8 +1826,10 @@ async function runConductorCommand(): Promise<void> {
|
|
|
1493
1826
|
let input: Record<string, unknown>;
|
|
1494
1827
|
if (cmd === "task") {
|
|
1495
1828
|
op = args[0] === "assign" ? "hub_task_assign" : "hub_task_escalate";
|
|
1496
|
-
|
|
1497
|
-
|
|
1829
|
+
const { one, rest: positional } = takeFlags(args.slice(1), args[0] === "assign" ? ["--role"] : [], []);
|
|
1830
|
+
const role = one["--role"] ?? "owner";
|
|
1831
|
+
if (!positional[0] || positional.length !== (args[0] === "assign" ? 2 : 1) || !["owner", "reviewer"].includes(role)) fail("usage: ahub task assign <id> <peer> [--role owner|reviewer] | escalate <id>");
|
|
1832
|
+
input = { id: Number(positional[0]), ...(args[0] === "assign" ? { peer: positional[1], ...(role === "reviewer" ? { role } : {}) } : {}) };
|
|
1498
1833
|
} else if (cmd === "pause" || cmd === "resume") {
|
|
1499
1834
|
if (args.length !== 1) fail(`usage: ahub ${cmd} <peer>`);
|
|
1500
1835
|
op = cmd === "pause" ? "hub_peer_hold" : "hub_peer_release";
|