@staix/agent-hub 0.16.0 → 0.18.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 +34 -0
- package/README.md +2 -2
- package/docs/agent-notes/README.md +5 -0
- package/docs/agent-notes/adapters.md +20 -5
- package/docs/agent-notes/benchmarks.md +1 -0
- package/docs/agent-notes/daemon.md +5 -3
- package/docs/agent-notes/local-worker.md +1 -0
- package/docs/agent-notes/tasks.md +9 -3
- package/docs/agent-notes/tests.md +6 -3
- package/docs/dogfood.md +21 -4
- package/docs/memory.md +1 -1
- package/docs/operations.md +150 -34
- package/docs/profiles.md +36 -0
- package/docs/quickstart.md +14 -4
- package/docs/roles.md +1 -0
- package/docs/sdlc.md +39 -13
- package/docs/security.md +41 -9
- package/docs/smoke.md +51 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +56 -10
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +3 -1
- package/docs/verification/2026-10-25-0.17.0.md +74 -0
- package/docs/verification/2026-10-25-0.18.0.md +76 -0
- package/docs/verification/2026-10-25-codex-0164-reviewer-tool-call.md +52 -0
- package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
- package/docs/verification/2026-10-25-kimi-231-reviewer-compat.md +48 -0
- package/docs/verification/2026-10-25-kimi-usage-ac11.md +48 -0
- package/docs/verification/2026-10-25-qwen-dual-output-hub-e2e.md +23 -0
- package/docs/verification/2026-10-25-qwen-dual-output-t0.md +20 -0
- package/docs/verification/2026-10-25-remote-docker-transport.md +46 -0
- package/docs/verification/2026-10-25-reviewer-no-verdict-diagnostics.md +35 -0
- package/docs/verification/profiles/YYYY-MM-DD-general.md +46 -0
- package/docs/verification/profiles/YYYY-MM-DD-research.md +61 -0
- package/docs/verification/profiles/YYYY-MM-DD-writing.md +46 -0
- package/docs/verified.json +28 -19
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1115 -192
- package/src/adapters/codex-review-isolation.ts +10 -1
- package/src/adapters/compat.ts +7 -2
- package/src/adapters/conformance/enrollment.ts +18 -4
- package/src/adapters/conformance/factories.ts +4 -1
- package/src/adapters/conformance/kit.ts +11 -0
- package/src/adapters/definitions/builtin-metadata.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-qwen.ts +32 -4
- package/src/adapters/definitions/native-cli-preview-qwen.ts +9 -1
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/definitions/native-recovery.ts +8 -0
- package/src/adapters/definitions/qwen.ts +7 -4
- package/src/adapters/local-worker.ts +5 -3
- package/src/adapters/qwen-dual.ts +578 -0
- package/src/adapters/registry-types.ts +5 -1
- package/src/adapters/registry.ts +9 -1
- package/src/adapters/reviewer-mcp.ts +12 -6
- package/src/adapters/reviewer-print.ts +100 -17
- package/src/cli/console-state.ts +9 -2
- package/src/cli/help.ts +29 -28
- package/src/cli/identity.ts +87 -6
- package/src/cli/main.ts +190 -69
- package/src/cli/output.ts +7 -2
- package/src/cli/sdlc-output.ts +242 -0
- package/src/cli/task-output.ts +212 -0
- package/src/cli/upgrade-output.ts +57 -0
- package/src/cli/upgrade.ts +4 -3
- package/src/hub/board.ts +17 -0
- package/src/hub/check-runner.ts +252 -22
- package/src/hub/conductor.ts +5 -0
- package/src/hub/config-apply.ts +16 -7
- package/src/hub/config-edit-host.ts +1 -1
- package/src/hub/config-tier.ts +1 -1
- package/src/hub/daemon.ts +487 -94
- package/src/hub/dogfood-profile.ts +248 -0
- package/src/hub/dogfood-read.ts +77 -14
- package/src/hub/dogfood.ts +5 -1
- package/src/hub/events.ts +1 -1
- package/src/hub/host-executable.ts +2 -1
- package/src/hub/hub-tools.ts +2 -2
- package/src/hub/memory-plan-guard.ts +8 -33
- package/src/hub/operational-authority.ts +60 -0
- package/src/hub/operational-view.ts +141 -0
- package/src/hub/orphan-process.ts +69 -0
- package/src/hub/permission-mode.ts +1 -1
- package/src/hub/read-only-shell-proof.ts +80 -0
- package/src/hub/read-only-shell.ts +162 -0
- package/src/hub/reviewer-pool-config.ts +3 -2
- package/src/hub/reviewer-pool.ts +3 -0
- package/src/hub/routing.ts +23 -6
- package/src/hub/sdlc-merge.ts +299 -0
- package/src/hub/sdlc-mirror.ts +129 -0
- package/src/hub/sdlc.ts +58 -12
- package/src/hub/settings.ts +11 -5
- package/src/hub/snapshots.ts +60 -0
- package/src/hub/task-repository.ts +31 -5
- package/src/hub/tasks.ts +266 -28
- package/src/local/tools.ts +17 -4
- package/src/memory/brief.ts +45 -5
- package/src/ui/index.html +2 -1
package/src/cli/main.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { conductorOperation } from "../hub/operational-authority.ts";
|
|
2
3
|
import type { DeliverableRow } from "../hub/tasks.ts";
|
|
3
4
|
import { kimiSourceRow } from "../hub/budget-notes.ts";
|
|
4
5
|
import { hubGitSync, hubGitEnv } from "../hub/git.ts";
|
|
@@ -66,8 +67,8 @@ import { exactVersion, latestRelease, newerVersion } from "./recovery-package.ts
|
|
|
66
67
|
import { operationScreen, planScreen, type ScreenIO, type UpgradeHost } from "./upgrade-interactive.ts";
|
|
67
68
|
import { readRecoveryPresentationFlags } from "./upgrade-presentation-flags.ts";
|
|
68
69
|
import { renderOperation, renderBlocked, renderSummary, type RecoveryPresentationOptions } from "./upgrade-presentation.ts";
|
|
70
|
+
import { renderBudgetResult, renderRecoveryStatus, renderUpgradePlan } from "./upgrade-output.ts";
|
|
69
71
|
import { recordTerminalLaunch, prepareTerminalLaunch, registerTerminalLaunch, type TerminalPeer } from "./terminal-recovery.ts";
|
|
70
|
-
import { stopAndArchiveSummary } from "./recovery-guidance.ts";
|
|
71
72
|
import { ensureMlx, inspectMlx, stopMlx } from "../models/mlx.ts";
|
|
72
73
|
import { setupOllamaModel } from "./models-setup.ts";
|
|
73
74
|
|
|
@@ -94,6 +95,8 @@ import { runConsole } from "./console.ts";
|
|
|
94
95
|
import { paint, permissionBoundary, resolveColor, wrap, type Span } from "./console-state.ts";
|
|
95
96
|
import { outputWidth, renderRoster, renderStatus, renderBoard, renderBacklog, renderBoardColumns, boardColumnModel, renderBudget, renderDoctor, renderProjects, renderQueue, renderTurns, renderOrphans, renderQueueShow, renderModelsStatus, renderExecutionBudgetStatus, renderReport, renderUsage, renderLimits, renderTotal, type DoctorCheck } from "./output.ts";
|
|
96
97
|
import { renderHelp, configuredAcpHelp } from "./help.ts";
|
|
98
|
+
import { renderBacklogResult, renderGroom, renderReleaseChange, renderReleaseList, renderReleaseNew, renderReleaseShow, renderTaskCancel, renderTaskShow } from "./task-output.ts";
|
|
99
|
+
import { renderAuthorization, renderSdlcCosts, renderSdlcExplain, renderSdlcMetrics, renderSdlcQueue, renderSdlcRelease, renderSdlcStatus } from "./sdlc-output.ts";
|
|
97
100
|
import { renderTailEvent } from "./tail-render.ts";
|
|
98
101
|
import { archiveProblem, archiveState, damagedState, failureText, MANIFEST, planReset, resetLines, resetRuntime, startState, type ResetPlan } from "./reset.ts";
|
|
99
102
|
|
|
@@ -201,6 +204,7 @@ const projectConfig = () => {
|
|
|
201
204
|
return config;
|
|
202
205
|
};
|
|
203
206
|
const stateDir = selected.stateDir;
|
|
207
|
+
var operationalAuditReady=false, operationalRefusalRecorded=false;
|
|
204
208
|
const caller = declaredInstances ? detectCliIdentity(process.env, declaredInstances) : unboundCaller;
|
|
205
209
|
const commandLabel = cliCommandLabel(cmd, args, projectCatalog);
|
|
206
210
|
const commandAccess = classifyPeerCommand(cmd, args, projectCatalog);
|
|
@@ -208,8 +212,10 @@ const commandAccess = classifyPeerCommand(cmd, args, projectCatalog);
|
|
|
208
212
|
const delegationRefused = liveTest && caller.role === "tools" && commandAccess !== "allowed" && liveTestRefusal(cmd, args);
|
|
209
213
|
const delegatedPeer = liveTest && caller.role === "tools" && commandAccess !== "allowed" && !delegationRefused ? caller.peer : undefined;
|
|
210
214
|
const identity = delegatedPeer ? { role: "console" as const } : caller;
|
|
215
|
+
operationalAuditReady=identity.role==="tools" && commandAccess==="conductor" && conductorOperation(cmd,args)!==undefined;
|
|
211
216
|
function audit(outcome: "run" | "refused" | "invalid"): void {
|
|
212
217
|
if (caller.role === "console") return;
|
|
218
|
+
if(outcome==="refused" && operationalAuditReady){if(operationalRefusalRecorded)return;operationalRefusalRecorded=true;}
|
|
213
219
|
try { recordCliAudit(stateDir, caller.role === "tools" ? caller.peer : "unknown", commandLabel, outcome); }
|
|
214
220
|
catch {
|
|
215
221
|
if (delegatedPeer) fail("live-test delegation audit unavailable; nothing was executed");
|
|
@@ -249,7 +255,7 @@ if (delegatedPeer && !recoveryAudit && !noAuditConsumer) {
|
|
|
249
255
|
}
|
|
250
256
|
fail((error as Error).message);
|
|
251
257
|
}
|
|
252
|
-
} else if (identity.role === "tools" || delegatedPeer) audit("run");
|
|
258
|
+
} else if ((identity.role === "tools" || delegatedPeer) && !operationalAuditReady) audit("run");
|
|
253
259
|
function assertDelegatedAuditConsumer(): void {
|
|
254
260
|
if (delegatedAuditHub && ((!delegatedConnectionTaken && !delegatedAuditHub.isOpen) || readControl(stateDir)?.instanceId !== delegatedAuditHub.instanceId)) {
|
|
255
261
|
throw new Error("delegated audit consumer changed; refusing further actions; inspect the result before retrying");
|
|
@@ -260,18 +266,52 @@ const liveTestArgs = liveTest ? ["--live-test"] : [];
|
|
|
260
266
|
const invokedFrom = process.cwd(); // relative paths a person types (a bench suite) are theirs, not the project's
|
|
261
267
|
try { process.chdir(cwd); } catch { fail(`project directory is unavailable: ${cwd}`); }
|
|
262
268
|
/** One-shot output flags do not alter the fetched JSON document. */
|
|
263
|
-
function outputOptions() {
|
|
264
|
-
const flags =
|
|
265
|
-
if (flags.length > 1 ||
|
|
269
|
+
function outputOptions(source: readonly string[] = args) {
|
|
270
|
+
const flags = source.filter(arg => arg.startsWith("--color="));
|
|
271
|
+
if (flags.length > 1 || source.includes("--color")) fail(`usage: ahub ${cmd} [--color=auto|always|never] [--json] [--full]`);
|
|
266
272
|
const color = resolveColor(flags[0]?.slice("--color=".length), { isTTY: !!process.stdout.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR });
|
|
267
273
|
if (color instanceof Error) fail(`usage: ahub ${cmd} [--color=auto|always|never] [--json] [--full]`);
|
|
268
|
-
return { color: color === true, json:
|
|
274
|
+
return { color: color === true, json: source.includes("--json"), full: source.includes("--full"),
|
|
269
275
|
columns: outputWidth({ isTTY: !!process.stdout.isTTY, columns: process.stdout.columns, COLUMNS: process.env.COLUMNS }) };
|
|
270
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* #814 T2: split `--json`, `--full` and `--color=` from a command that also takes free text. A value flag's value is never a
|
|
279
|
+
* presentation flag (`task cancel --reason --json` records the reason `--json`), and with `edges` only a leading or trailing run
|
|
280
|
+
* counts, so a reason that mentions `--json` in the middle of a sentence keeps its words. A reason that ENDS in a bare
|
|
281
|
+
* presentation flag loses it: the documented cost of accepting the flag after the reason.
|
|
282
|
+
*/
|
|
283
|
+
function splitPresentation(argv: readonly string[], opts: { valueFlags?: readonly string[]; edges?: boolean } = {}): { operands: string[]; flags: string[] } {
|
|
284
|
+
const isFlag = (arg: string) => arg === "--json" || arg === "--full" || arg.startsWith("--color=");
|
|
285
|
+
const operands: string[] = [], flags: string[] = [], value = new Set(opts.valueFlags ?? []);
|
|
286
|
+
let lead = true;
|
|
287
|
+
const middle: string[] = [];
|
|
288
|
+
for (let i = 0; i < argv.length; i++) {
|
|
289
|
+
const arg = argv[i]!;
|
|
290
|
+
if (value.has(arg)) { (opts.edges ? middle : operands).push(arg); if (i + 1 < argv.length) (opts.edges ? middle : operands).push(argv[++i]!); lead = false; continue; }
|
|
291
|
+
if (!isFlag(arg)) { (opts.edges ? middle : operands).push(arg); lead = false; continue; }
|
|
292
|
+
if (!opts.edges || lead) flags.push(arg); else middle.push(arg);
|
|
293
|
+
}
|
|
294
|
+
if (!opts.edges) return { operands, flags };
|
|
295
|
+
// a trailing run of presentation flags moves out of the middle; anything earlier stays operand text
|
|
296
|
+
let end = middle.length;
|
|
297
|
+
const trailing: string[] = [];
|
|
298
|
+
while (end > 0 && isFlag(middle[end - 1]!) && !value.has(middle[end - 2] ?? "")) trailing.unshift(middle[--end]!);
|
|
299
|
+
return { operands: middle.slice(0, end), flags: [...flags, ...trailing] };
|
|
300
|
+
}
|
|
271
301
|
/** Remove only presentation flags from read-only command positional arguments. */
|
|
272
302
|
function outputArguments(): string[] {
|
|
273
303
|
return args.filter(arg => arg !== "--json" && arg !== "--full" && !arg.startsWith("--color="));
|
|
274
304
|
}
|
|
305
|
+
/** `sdlc` operands: the read subcommands that print for a person (#814) drop `--json`, `--full` and `--color=`; every other subcommand keeps its argv exactly. */
|
|
306
|
+
/** A hub tool answers JSON text. Text that does not parse is printed as it came: a renderer never sees it, so it cannot read as an empty report. */
|
|
307
|
+
function showToolText(render: (data: any, columns?: number, now?: number) => Span[][], text: string, present: ReturnType<typeof outputOptions>): void {
|
|
308
|
+
let data: unknown;
|
|
309
|
+
try { data = JSON.parse(text); } catch { return console.log(text); }
|
|
310
|
+
if (present.json) return console.log(text);
|
|
311
|
+
printOutput(render(data, present.columns, Date.now()), present.color);
|
|
312
|
+
}
|
|
313
|
+
const SDLC_READ = ["status", "explain", "queue", "release", "metrics", "costs", "authorize-release"];
|
|
314
|
+
function sdlcOperands(): string[] { return SDLC_READ.includes(args[0] ?? "") ? outputArguments() : args; }
|
|
275
315
|
function printOutput(lines: Span[][], color: boolean): void {
|
|
276
316
|
for (const line of lines) console.log(paint(line, color));
|
|
277
317
|
}
|
|
@@ -447,7 +487,8 @@ function startupDiagnostic(error: unknown): string {
|
|
|
447
487
|
return message.replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim().slice(0, 512) || "invalid startup configuration";
|
|
448
488
|
}
|
|
449
489
|
|
|
450
|
-
function fail(message: string): never {
|
|
490
|
+
function fail(message: string, refusalAudited=false): never {
|
|
491
|
+
if(operationalAuditReady){if(refusalAudited)operationalRefusalRecorded=true;if(!operationalRefusalRecorded)audit("refused");}
|
|
451
492
|
console.error(`ahub: ${message}`);
|
|
452
493
|
process.exit(1);
|
|
453
494
|
}
|
|
@@ -459,6 +500,7 @@ async function taskOp(op: string, a: Record<string, unknown>, throwOnError = fal
|
|
|
459
500
|
const res = await hub.request({ t: "task", op, args: a }, timeoutMs ?? (op === "github_import" ? 60_000 : 30_000));
|
|
460
501
|
if (!res.ok) {
|
|
461
502
|
if (throwOnError) throw new Error(res.error);
|
|
503
|
+
if(res.refusalAudited===true)operationalRefusalRecorded=true;
|
|
462
504
|
audit("refused");
|
|
463
505
|
// #711: an agent shell refused something only the person may do is told why it counts as an agent. The refusal and the authority stay as they were.
|
|
464
506
|
const cause = agentShellCause(identity, String(res.error));
|
|
@@ -509,7 +551,9 @@ async function hold(t: "pause" | "resume"): Promise<void> {
|
|
|
509
551
|
}
|
|
510
552
|
|
|
511
553
|
function recoveryDisplay() {
|
|
512
|
-
|
|
554
|
+
// A terminal gives its own width; a pipe gives COLUMNS when set, else 80 (#814: a person reads a pipe too).
|
|
555
|
+
const width = process.stdout.columns ?? outputWidth({ isTTY: false, COLUMNS: process.env.COLUMNS }) ?? 80;
|
|
556
|
+
return readRecoveryPresentationFlags(args, { isTTY: !!process.stdout.isTTY, columns: width, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR });
|
|
513
557
|
}
|
|
514
558
|
const humanRecovery = (json: boolean) => !json && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
515
559
|
const printRecovery = (lines: readonly string[]) => { for (const line of lines) console.log(line); };
|
|
@@ -656,12 +700,15 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
656
700
|
if (interactive) return onTerminal((io) => planScreen(upgradeHost(kind, to ?? VERSION), io), display.options);
|
|
657
701
|
const plan = await makeUpgradePlan(kind, to ?? VERSION, kind === "restart" ? cwd : undefined);
|
|
658
702
|
const jsonApply = display.json && yes && !dry;
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
703
|
+
// #814: a person reading a pipe or a dry run gets the plan screen's rows, with every blocker, reconnect-only session and
|
|
704
|
+
// fresh start in them. `--json` keeps the document and the stderr lines (#206) exactly as they were.
|
|
705
|
+
if (display.json) {
|
|
706
|
+
if (!jsonApply) console.log(JSON.stringify(plan, null, 2));
|
|
707
|
+
for (const p of plan.projects) for (const peer of p.reconnectOnly ?? []) console.error(`ahub: ${p.project.id}: ${peer} is reconnect-only (unmanaged session): its plugin reattaches to the new hub; no terminal is closed or relaunched`);
|
|
708
|
+
for (const p of plan.projects) for (const peer of p.freshStart ?? []) console.error(`ahub: ${p.project.id}: ${peer} restarts as a new session: its thread has no rollout and the hub recorded no turn on it, so nothing is lost`);
|
|
709
|
+
} else printRecovery(renderUpgradePlan(plan, VERSION, display.options));
|
|
663
710
|
const blockers = [...plan.blockers, ...plan.projects.flatMap((p) => p.blockers.map((b) => `${p.project.id}: ${b}`))];
|
|
664
|
-
for (const blocker of blockers) console.error(`ahub: blocker: ${blocker}`);
|
|
711
|
+
if (display.json) for (const blocker of blockers) console.error(`ahub: blocker: ${blocker}`);
|
|
665
712
|
if (dry) return;
|
|
666
713
|
if (blockers.length) { if (jsonApply) console.log(JSON.stringify(plan, null, 2)); fail("plan has blockers; no runtime was changed"); }
|
|
667
714
|
assertLifecycleAvailable();
|
|
@@ -724,14 +771,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
724
771
|
return;
|
|
725
772
|
}
|
|
726
773
|
const result = await reattachRecovery(selected.id, selected.peer, makeRecoveryDriver());
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
} else {
|
|
732
|
-
console.log(JSON.stringify(publicOperation(result), null, 2));
|
|
733
|
-
if (!display.json) console.log(nextActionsText(result));
|
|
734
|
-
}
|
|
774
|
+
if (display.json) { console.log(JSON.stringify(publicOperation(result), null, 2)); return; }
|
|
775
|
+
printRecovery(renderOperation(result, undefined, display.options));
|
|
776
|
+
const live = await liveProjects(result, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
777
|
+
printRecovery(renderBlocked(result, nextChoices(result, undefined, live), display.options));
|
|
735
778
|
return;
|
|
736
779
|
}
|
|
737
780
|
// #272: without an id, the operation that holds the machine's lock; bare on a terminal, its screen.
|
|
@@ -761,7 +804,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
761
804
|
if (action === "status") {
|
|
762
805
|
// #215: abort and resume are offered by what the open sources show, read as the runner's errors read them.
|
|
763
806
|
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
764
|
-
console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
807
|
+
if (display.json) console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
808
|
+
else printRecovery(renderRecoveryStatus(operation, runner, nextChoices(operation, runner, live), display.options));
|
|
765
809
|
}
|
|
766
810
|
else if (action === "abort") {
|
|
767
811
|
await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer));
|
|
@@ -780,11 +824,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
780
824
|
console.log(`${one["--fresh-session"]}: a fresh session is accepted and the lost one is recorded; resuming`);
|
|
781
825
|
return spawnRecovery(result, false, humanRecovery(false) ? display.options : undefined);
|
|
782
826
|
}
|
|
783
|
-
if (
|
|
784
|
-
else
|
|
785
|
-
console.log(JSON.stringify(publicOperation(result), null, 2));
|
|
786
|
-
if (!display.json) for (const line of stopAndArchiveSummary(result)) console.log(line);
|
|
787
|
-
}
|
|
827
|
+
if (display.json) console.log(JSON.stringify(publicOperation(result), null, 2));
|
|
828
|
+
else printRecovery(renderSummary(result, await recoveryEndInspection(result), display.options));
|
|
788
829
|
}
|
|
789
830
|
else if (["completed", "cancelled"].includes(operation.phase)) console.log(display.json ? JSON.stringify(publicOperation(operation, runner), null, 2) : `recovery is already ${operation.phase}`);
|
|
790
831
|
// #215: an operation being abandoned is never resumed, whichever coordinator started it (an older one would).
|
|
@@ -973,7 +1014,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
973
1014
|
|
|
974
1015
|
models: async () => {
|
|
975
1016
|
const action = outputArguments()[0] ?? "status";
|
|
976
|
-
const options =
|
|
1017
|
+
const options = ["status", "start", "setup"].includes(action) ? outputOptions() : undefined;
|
|
977
1018
|
const showStatus = (data: unknown) => options!.json ? console.log(JSON.stringify(data, null, 2)) : printOutput(renderModelsStatus(data, options!.columns, Date.now()), options!.color);
|
|
978
1019
|
const configured = projectConfig().mlx;
|
|
979
1020
|
if (configured.enabled === false) {
|
|
@@ -985,13 +1026,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
985
1026
|
const modelPath = configured.modelPath ? resolve(cwd, configured.modelPath) : join(homedir(), ".agenthub", "models", "qwen3-8b-mlx");
|
|
986
1027
|
const mlxOptions = configured.provider === "ollama" ? configured : { ...configured, runtimeDir, modelPath };
|
|
987
1028
|
if (action === "status") return showStatus(await inspectMlx(mlxOptions));
|
|
988
|
-
if (action === "start") { const handle = await ensureMlx(mlxOptions); return
|
|
1029
|
+
if (action === "start") { const handle = await ensureMlx(mlxOptions); return showStatus(handle.status()); }
|
|
989
1030
|
if (action === "stop") { await stopMlx(mlxOptions); return console.log("MLX stopped"); }
|
|
990
1031
|
if (action !== "setup") fail("usage: ahub models setup|status|start|stop");
|
|
991
1032
|
if (configured.provider === "ollama") {
|
|
992
1033
|
await setupOllamaModel(configured);
|
|
993
1034
|
const handle = await ensureMlx(configured);
|
|
994
|
-
return
|
|
1035
|
+
return showStatus(handle.status());
|
|
995
1036
|
}
|
|
996
1037
|
const python = join(runtimeDir, "bin", "python");
|
|
997
1038
|
const run = (argv: string[]) => { const result = spawnSync(argv[0]!, argv.slice(1), { cwd, stdio: "inherit" }); if (result.status !== 0) fail(`models setup failed: ${argv.join(" ")}`); };
|
|
@@ -1024,7 +1065,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1024
1065
|
const { one, rest: flags } = takeFlags(rest, ["--peer"], []);
|
|
1025
1066
|
if (flags.length) fail("usage: ahub queue list [--peer <id>] [--json] [--full] [--color=auto|always|never]");
|
|
1026
1067
|
const result = await hub.request({ t: "queue", op: "list", ...(one["--peer"] ? { peer: one["--peer"] } : {}) });
|
|
1027
|
-
if (!result.ok) fail(result.error);
|
|
1068
|
+
if (!result.ok) fail(result.error,result.refusalAudited===true);
|
|
1028
1069
|
if (options.json) return console.log(JSON.stringify(result.deliveries, null, 2));
|
|
1029
1070
|
printOutput(renderQueue(result.deliveries, options.columns, Date.now()), options.color);
|
|
1030
1071
|
return;
|
|
@@ -1036,7 +1077,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1036
1077
|
if (others.length || !one["--peer"] || (one["--state"] !== undefined && !["queued", "needs_review"].includes(one["--state"]))) fail("usage: ahub queue discard --peer <id> --reason <text> [--state queued|needs_review] [--yes]");
|
|
1037
1078
|
if (yes && !one["--reason"]?.trim()) fail("ahub queue discard needs --reason <text> with --yes: it is recorded on every discarded delivery");
|
|
1038
1079
|
const result = await hub.request({ t: "queue", op: "discard", peer: one["--peer"], ...(one["--state"] ? { state: one["--state"] } : {}), ...(yes ? { confirmed: true, reason: one["--reason"] } : {}) }, 60_000);
|
|
1039
|
-
if (!result.ok) fail(result.error);
|
|
1080
|
+
if (!result.ok) fail(result.error,result.refusalAudited===true);
|
|
1040
1081
|
if (options.json) return console.log(JSON.stringify(result, null, 2));
|
|
1041
1082
|
if (!yes) return console.log(`${result.count} ${result.state} deliver${result.count === 1 ? "y" : "ies"} for ${result.peer}; nothing discarded. Run again with --reason <text> --yes to discard them.`);
|
|
1042
1083
|
console.log(`discarded ${result.discarded} of ${result.count} ${result.state} deliver${result.count === 1 ? "y" : "ies"} for ${result.peer}${result.refused ? `; ${result.refused} refused (${Object.entries(result.refusedBy ?? {}).map(([why, n]) => `${why} ${n}`).join(", ")})` : ""}`);
|
|
@@ -1047,7 +1088,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1047
1088
|
if (operation === "show") {
|
|
1048
1089
|
if (flags.length) fail("usage: ahub queue show <id>");
|
|
1049
1090
|
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
1050
|
-
if (!shown.ok) fail(shown.error);
|
|
1091
|
+
if (!shown.ok) fail(shown.error,shown.refusalAudited===true);
|
|
1051
1092
|
if (options.json) return console.log(JSON.stringify(shown.delivery, null, 2));
|
|
1052
1093
|
return printOutput(renderQueueShow(shown.delivery, options.columns, Date.now()), options.color);
|
|
1053
1094
|
}
|
|
@@ -1057,12 +1098,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1057
1098
|
let revision = parsedRevision;
|
|
1058
1099
|
if (revision === undefined) {
|
|
1059
1100
|
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
1060
|
-
if (!shown.ok) fail(shown.error);
|
|
1101
|
+
if (!shown.ok) fail(shown.error,shown.refusalAudited===true);
|
|
1061
1102
|
revision = shown.delivery.revision;
|
|
1062
1103
|
}
|
|
1063
1104
|
const result = await hub.request({ t: "queue", op: "resolve", id, revision, action: one["--action"], reason: one["--reason"] });
|
|
1064
|
-
if (!result.ok) fail(result.error);
|
|
1065
|
-
console.log(JSON.stringify(result.delivery, null, 2));
|
|
1105
|
+
if (!result.ok) fail(result.error,result.refusalAudited===true);
|
|
1106
|
+
if (options.json) console.log(JSON.stringify(result.delivery, null, 2));
|
|
1107
|
+
else printOutput(renderQueueShow(result.delivery, options.columns, Date.now()), options.color);
|
|
1066
1108
|
} finally { hub.close(); }
|
|
1067
1109
|
},
|
|
1068
1110
|
|
|
@@ -1120,7 +1162,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1120
1162
|
const result = await hub.request(request); hub.close();
|
|
1121
1163
|
if (!result.ok) fail(result.error);
|
|
1122
1164
|
const data = result.budgets ?? result.budget ?? { disabled: result.disabled };
|
|
1123
|
-
if (
|
|
1165
|
+
if (options.json) return console.log(JSON.stringify(data, null, 2));
|
|
1166
|
+
if (op === "configure" || op === "disable") return printOutput(renderBudgetResult(op, op === "configure" ? result.budget : result, options.columns, Date.now()), options.color);
|
|
1124
1167
|
return printOutput(renderExecutionBudgetStatus(result.budgets, options.columns, Date.now()), options.color);
|
|
1125
1168
|
}
|
|
1126
1169
|
let set: Record<string, unknown> | undefined;
|
|
@@ -1148,7 +1191,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1148
1191
|
},
|
|
1149
1192
|
|
|
1150
1193
|
backlog: async () => {
|
|
1151
|
-
const
|
|
1194
|
+
const split = splitPresentation(args, { edges: true }), operands = split.operands;
|
|
1195
|
+
const [sub, id, revision, value, ...reason] = operands;
|
|
1196
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1197
|
+
const shown = (render: (data: any, columns?: number, now?: number) => Span[][]) => async (text: Promise<string>) => showToolText(render, await text, present);
|
|
1152
1198
|
if (sub === "list" && outputArguments().length === 1) {
|
|
1153
1199
|
// #399: a table by default; --json prints the hub's listing unchanged, and a refusal is printed as it came.
|
|
1154
1200
|
const options = outputOptions(), raw = await taskOp("hub_backlog_list", {});
|
|
@@ -1158,43 +1204,44 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1158
1204
|
if (!Array.isArray(cards)) return console.log(raw);
|
|
1159
1205
|
return printOutput(renderBacklog(cards, options.columns, Date.now(), options.full), options.color);
|
|
1160
1206
|
}
|
|
1161
|
-
if (sub === "groom" &&
|
|
1162
|
-
if (["return", "return-untouched"].includes(sub ?? "") && /^[1-9]\d*$/.test(id ?? "") && /^\d+$/.test(revision ?? "") &&
|
|
1163
|
-
return
|
|
1207
|
+
if (sub === "groom" && operands.length === 1) return shown(renderGroom)(taskOp("hub_planner_groom", {}));
|
|
1208
|
+
if (["return", "return-untouched"].includes(sub ?? "") && /^[1-9]\d*$/.test(id ?? "") && /^\d+$/.test(revision ?? "") && operands.length >= 4) {
|
|
1209
|
+
return shown((data, columns, now) => renderBacklogResult("return-untouched", data, columns, now))(taskOp("hub_backlog_return_untouched", { id: Number(id), revision: Number(revision), reason: operands.slice(3).join(" ") }));
|
|
1164
1210
|
}
|
|
1165
1211
|
if (sub === "add" && args.length >= 3) return console.log(await taskOp("hub_task_propose", { class: id, title: args.slice(2).join(" "), backlog: true }));
|
|
1166
1212
|
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>");
|
|
1167
|
-
if (["pull", "return"].includes(sub ?? "") &&
|
|
1213
|
+
if (["pull", "return"].includes(sub ?? "") && operands.length === 2) return console.log(await taskOp(sub === "pull" ? "backlog_pull" : "backlog_return", { id }));
|
|
1168
1214
|
if (!/^\d+$/.test(revision ?? "")) fail("planning revision is required");
|
|
1169
|
-
if (sub === "pull-ready" &&
|
|
1215
|
+
if (sub === "pull-ready" && operands.length >= 4) return shown((data, columns, now) => renderBacklogResult("pull-ready", data, columns, now))(taskOp("hub_backlog_pull", { id: Number(id), revision: Number(revision), reason: operands.slice(3).join(" ") }));
|
|
1170
1216
|
if (sub === "split" && value && reason.length) {
|
|
1171
1217
|
let parts: unknown;try { parts=JSON.parse(value); } catch { return fail("split parts must be JSON"); }
|
|
1172
|
-
return
|
|
1218
|
+
return shown((data, columns, now) => renderBacklogResult("split", data, columns, now))(taskOp("hub_backlog_split", { id: Number(id), revision: Number(revision), parts, reason: reason.join(" ") }));
|
|
1173
1219
|
}
|
|
1174
|
-
if (sub === "undo" &&
|
|
1220
|
+
if (sub === "undo" && operands.length === 3) return shown((data, columns, now) => renderBacklogResult("undo", data, columns, now))(taskOp("hub_backlog_undo", { id: Number(id), revision: Number(revision) }));
|
|
1175
1221
|
if (!["plan", "pin"].includes(sub ?? "") || !value || !reason.length) fail("planning needs fields and a reason");
|
|
1176
1222
|
let fields: unknown;
|
|
1177
1223
|
try { fields = sub === "pin" ? { pinned: value === "true" ? true : value === "false" ? false : undefined } : JSON.parse(value); }
|
|
1178
1224
|
catch { return fail("planning fields must be JSON"); }
|
|
1179
1225
|
if (sub === "pin" && value !== "true" && value !== "false") fail("pin must be true or false");
|
|
1180
|
-
|
|
1226
|
+
return shown((data, columns, now) => renderBacklogResult(sub as "plan" | "pin", data, columns, now))(taskOp("hub_backlog_plan", { id: Number(id), revision: Number(revision), fields, reason: reason.join(" ") }));
|
|
1181
1227
|
},
|
|
1182
1228
|
|
|
1183
1229
|
release: async () => {
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
if (sub === "
|
|
1230
|
+
const split = splitPresentation(args, { edges: true }), operands = split.operands;
|
|
1231
|
+
const [sub, name, revision, value] = operands;
|
|
1232
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1233
|
+
const shown = (render: (data: any, columns?: number, now?: number) => Span[][]) => async (text: Promise<string>) => showToolText(render, await text, present);
|
|
1234
|
+
if (sub === "list" && operands.length === 1) return shown(renderReleaseList)(taskOp("hub_release_list", {}));
|
|
1235
|
+
if (sub === "show" && name && operands.length === 2) return shown(renderReleaseShow)(taskOp("release_show", { name }));
|
|
1236
|
+
if (sub === "new" && name && revision && operands.length === 4 && /^\d+$/.test(value ?? "")) return shown(renderReleaseNew)(taskOp("release_new", { name, goal: revision, order: Number(value) }));
|
|
1190
1237
|
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>");
|
|
1191
1238
|
const states: Record<string, string> = { activate: "active", freeze: "frozen", ship: "shipped", drop: "dropped" };
|
|
1192
1239
|
let fields: unknown;
|
|
1193
|
-
if (sub === "plan" && value &&
|
|
1240
|
+
if (sub === "plan" && value && operands.length === 4) {
|
|
1194
1241
|
try { fields = JSON.parse(value); } catch { return fail("release dates must be JSON"); }
|
|
1195
|
-
} else if (sub && Object.hasOwn(states, sub) &&
|
|
1242
|
+
} else if (sub && Object.hasOwn(states, sub) && operands.length === 3) fields = { state: states[sub] };
|
|
1196
1243
|
else return fail("invalid release command");
|
|
1197
|
-
|
|
1244
|
+
return shown(renderReleaseChange)(taskOp("release_change", { name, revision: Number(revision), fields }));
|
|
1198
1245
|
},
|
|
1199
1246
|
|
|
1200
1247
|
deliverables: async () => {
|
|
@@ -1256,6 +1303,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1256
1303
|
},
|
|
1257
1304
|
|
|
1258
1305
|
sdlc: async () => {
|
|
1306
|
+
const args = sdlcOperands(), present = SDLC_READ.includes(args[0] ?? "") ? outputOptions() : undefined;
|
|
1307
|
+
/** The human form (#814). Each site keeps its own `if (present.json) console.log(JSON.stringify(...))`, so the JSON branch stays visible to the guard test. */
|
|
1308
|
+
const human = (render: (data: any, columns?: number, now?: number) => Span[][], data: unknown) => printOutput(render(data, present!.columns, Date.now()), present!.color);
|
|
1259
1309
|
const sub = args[0], id = args[1] === undefined ? undefined : Number(args[1]);
|
|
1260
1310
|
if (sub === "authorize-release") {
|
|
1261
1311
|
if (args.length !== 2 || !validReleaseVersion(args[1])) fail("usage: ahub sdlc authorize-release <version>");
|
|
@@ -1263,7 +1313,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1263
1313
|
const commit = result.stdout?.trim();
|
|
1264
1314
|
if (result.status !== 0 || !commit || !/^[a-f0-9]{40}$/.test(commit)) fail("candidate commit unavailable");
|
|
1265
1315
|
const hub = await connect();
|
|
1266
|
-
try { const reply = await hub.request({ t: "sdlc", op: sub, version: args[1], commit }, 30_000); if (!reply.ok) fail(reply.error); console.log(JSON.stringify(reply.authorization, null, 2)); }
|
|
1316
|
+
try { const reply = await hub.request({ t: "sdlc", op: sub, version: args[1], commit }, 30_000); if (!reply.ok) fail(reply.error); if (present!.json) console.log(JSON.stringify(reply.authorization, null, 2)); else human(renderAuthorization, reply.authorization); }
|
|
1267
1317
|
finally { hub.close(); }
|
|
1268
1318
|
return;
|
|
1269
1319
|
}
|
|
@@ -1305,7 +1355,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1305
1355
|
const repo = args[1], liveIssues = args.slice(2).map(Number);
|
|
1306
1356
|
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...]");
|
|
1307
1357
|
const hub = await connect();
|
|
1308
|
-
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)); }
|
|
1358
|
+
try { const reply = await hub.request({ t: "sdlc", op: sub, repo, liveIssues }, 30_000); if (!reply.ok) fail(reply.error); if (present!.json) console.log(JSON.stringify(reply, null, 2)); else human(sub === "queue" ? renderSdlcQueue : renderSdlcRelease, reply); }
|
|
1309
1359
|
finally { hub.close(); }
|
|
1310
1360
|
return;
|
|
1311
1361
|
}
|
|
@@ -1330,12 +1380,14 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1330
1380
|
}
|
|
1331
1381
|
if (sub === "costs") {
|
|
1332
1382
|
if (args.length > 2) fail("usage: ahub sdlc costs [owner/repository]");
|
|
1333
|
-
|
|
1383
|
+
const text = await taskOp("hub_sdlc_costs", { ...(args[1] ? { repo: args[1] } : {}) });
|
|
1384
|
+
return showToolText(renderSdlcCosts, text, present!);
|
|
1334
1385
|
}
|
|
1335
1386
|
if (sub === "metrics") {
|
|
1336
1387
|
const flags = takeFlags(args.slice(1), ["--since"], []);
|
|
1337
1388
|
if (flags.rest.length) fail("usage: ahub sdlc metrics [--since <YYYY-MM-DD|ISO time>]");
|
|
1338
|
-
|
|
1389
|
+
const text = await taskOp("hub_sdlc_metrics", { ...(flags.one["--since"] !== undefined ? { since: flags.one["--since"] } : {}) });
|
|
1390
|
+
return showToolText(renderSdlcMetrics, text, present!);
|
|
1339
1391
|
}
|
|
1340
1392
|
if (sub === "plan-review") {
|
|
1341
1393
|
const revision = Number(args[2]);
|
|
@@ -1356,6 +1408,20 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1356
1408
|
} finally { hub.close(); }
|
|
1357
1409
|
return;
|
|
1358
1410
|
}
|
|
1411
|
+
if (sub === "merge") {
|
|
1412
|
+
// #786 T1: the guarded merge of one approved task; a person or the configured conductor asks, the daemon re-reads every gate.
|
|
1413
|
+
const flags = takeFlags(args.slice(2), [], []);
|
|
1414
|
+
const dryRun = flags.rest.includes("--dry-run");
|
|
1415
|
+
if (flags.rest.some(arg => arg !== "--dry-run") || !Number.isSafeInteger(id) || id! <= 0) fail("usage: ahub sdlc merge <task> [--dry-run]");
|
|
1416
|
+
const hub = await connect();
|
|
1417
|
+
try {
|
|
1418
|
+
const reply = await hub.request({ t: "sdlc", op: "merge", id, ...(dryRun ? { dryRun: true } : {}) }, 60_000);
|
|
1419
|
+
if (!reply.ok) fail(reply.error);
|
|
1420
|
+
console.log(JSON.stringify({ task: reply.task, mode: reply.mode, ran: reply.ran, decision: reply.decision, ...(reply.argv ? { argv: reply.argv } : {}), ...(reply.refused ? { refused: reply.refused } : {}), ...(reply.result ? { result: reply.result } : {}) }, null, 2));
|
|
1421
|
+
if (reply.decision === "refused" || reply.ran && reply.result?.state !== "merged") process.exitCode = 1;
|
|
1422
|
+
} finally { hub.close(); }
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1359
1425
|
if (sub === "accept") {
|
|
1360
1426
|
// #481 S1: the person accepts the task's spec issue at its current body; agent shells are refused before this runs.
|
|
1361
1427
|
const flags = takeFlags(args.slice(2), ["--issue", "--repo"], []);
|
|
@@ -1379,12 +1445,19 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1379
1445
|
try {
|
|
1380
1446
|
const reply = await hub.request({ t: "sdlc", op: sub, ...(id === undefined ? {} : { id }) }, 30_000);
|
|
1381
1447
|
if (!reply.ok) fail(reply.error);
|
|
1382
|
-
|
|
1448
|
+
// `sdlc export` is a data format by purpose (#814): it stays JSON and never reaches a renderer.
|
|
1449
|
+
if (sub === "export") console.log(JSON.stringify(reply, null, 2));
|
|
1450
|
+
else if (present!.json) console.log(JSON.stringify(reply, null, 2));
|
|
1451
|
+
else human(sub === "explain" ? renderSdlcExplain : renderSdlcStatus, reply);
|
|
1383
1452
|
} finally { hub.close(); }
|
|
1384
1453
|
},
|
|
1385
1454
|
|
|
1386
1455
|
task: async () => {
|
|
1387
|
-
const [
|
|
1456
|
+
const presentation = splitPresentation(args, { valueFlags: ["--reason", "--superseded-by", "--revision"], edges: true });
|
|
1457
|
+
const shownTask = (render: (data: any, columns?: number, now?: number) => Span[][]) => async (text: Promise<string>) => showToolText(render, await text, outputOptions(presentation.flags));
|
|
1458
|
+
// task show and task cancel read their presentation options before the request goes out (a refusal after a cancel would invite a retry of work already done)
|
|
1459
|
+
if (["show", "cancel"].includes(args[0] ?? "")) outputOptions(presentation.flags);
|
|
1460
|
+
const [sub, ...rest] = ["show", "cancel"].includes(args[0] ?? "") ? presentation.operands : args;
|
|
1388
1461
|
if (sub === "role") {
|
|
1389
1462
|
if (rest.length !== 2 || !/^[1-9]\d*$/.test(rest[0]!)) fail("usage: ahub task role <id> <role|none>");
|
|
1390
1463
|
const hub = await connect();
|
|
@@ -1396,9 +1469,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1396
1469
|
const { one, rest: positional } = takeFlags(rest, ["--reason", "--superseded-by", "--revision"], []);
|
|
1397
1470
|
const revision = one["--revision"] === undefined ? undefined : Number(one["--revision"]);
|
|
1398
1471
|
if (positional.length !== 1 || !/^[1-9]\d*$/.test(positional[0]!) || !one["--reason"]?.trim() || (revision !== undefined && (!/^\d+$/.test(one["--revision"]!) || !Number.isSafeInteger(revision)))) fail("usage: ahub task cancel <id> --reason <text> [--superseded-by <ref>] [--revision N]");
|
|
1399
|
-
return
|
|
1472
|
+
return shownTask(renderTaskCancel)(taskOp("hub_task_cancel", { id: Number(positional[0]), reason: one["--reason"], ...(one["--superseded-by"] === undefined ? {} : { superseded_by: one["--superseded-by"] }), ...(revision === undefined ? {} : { revision }) }));
|
|
1473
|
+
}
|
|
1474
|
+
if (sub === "show") {
|
|
1475
|
+
// a stray operand (for example the value of a `--color always` that is not the `--color=` form) is a usage error, never silently ignored
|
|
1476
|
+
if (rest.length !== 1) fail("usage: ahub task show <id> [--json] [--full] [--color=auto|always|never]");
|
|
1477
|
+
return shownTask(renderTaskShow)(taskOp("task_show", { id: rest[0] }));
|
|
1400
1478
|
}
|
|
1401
|
-
if (sub === "show") return console.log(await taskOp("task_show", { id: rest[0] }));
|
|
1402
1479
|
if (sub === "escalate") return console.log(await taskOp("task_escalate", { id: rest[0] }));
|
|
1403
1480
|
if (sub === "assign") {
|
|
1404
1481
|
const { one, rest: positional } = takeFlags(rest, ["--role", "--revision"], []);
|
|
@@ -1489,7 +1566,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1489
1566
|
const reply = await hub.request({ t: "peer_stop", peer }, 35_000);
|
|
1490
1567
|
if (reply.ok !== true) {
|
|
1491
1568
|
const error = String(reply.error ?? "peer stop request refused");
|
|
1492
|
-
fail(/unknown (?:control )?(?:message|request|command)(?:\b|:)|this hub does not know "peer_stop"/i.test(error) ? `${error}; upgrade the running hub to use ahub stop` : error);
|
|
1569
|
+
fail(/unknown (?:control )?(?:message|request|command)(?:\b|:)|this hub does not know "peer_stop"/i.test(error) ? `${error}; upgrade the running hub to use ahub stop` : error,reply.refusalAudited===true);
|
|
1493
1570
|
}
|
|
1494
1571
|
console.log(`${peer}: ${reply.state}`);
|
|
1495
1572
|
} finally { hub.close(); }
|
|
@@ -1525,7 +1602,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1525
1602
|
const hub = await connect();
|
|
1526
1603
|
try {
|
|
1527
1604
|
requireNamedPeerFeature(hub, peer);
|
|
1528
|
-
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; };
|
|
1605
|
+
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"),reply.refusalAudited===true); return reply; };
|
|
1529
1606
|
if (sub === "list") {
|
|
1530
1607
|
const view = await request({ t: "settings_get" });
|
|
1531
1608
|
if (!Array.isArray(view.roster)) fail("upgrade the running hub to use ahub roster");
|
|
@@ -1591,7 +1668,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1591
1668
|
const reply = await hub.request(message, 35_000);
|
|
1592
1669
|
if (reply.ok !== true) {
|
|
1593
1670
|
const error = String(reply.error ?? "settings request refused");
|
|
1594
|
-
fail(/unknown (?:control )?(?:message|request|command)(?:\b|:)|this hub does not know "settings_/i.test(error) ? `${error}; upgrade the running hub to use ahub settings` : error);
|
|
1671
|
+
fail(/unknown (?:control )?(?:message|request|command)(?:\b|:)|this hub does not know "settings_/i.test(error) ? `${error}; upgrade the running hub to use ahub settings` : error,reply.refusalAudited===true);
|
|
1595
1672
|
}
|
|
1596
1673
|
return reply;
|
|
1597
1674
|
};
|
|
@@ -1719,8 +1796,29 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1719
1796
|
} finally { hub.close(); }
|
|
1720
1797
|
},
|
|
1721
1798
|
|
|
1722
|
-
logs: () =>
|
|
1723
|
-
|
|
1799
|
+
logs: async () => {
|
|
1800
|
+
if (identity.role !== "tools") return exec("tail", [args.includes("-f") ? "-f" : "-n100", join(stateDir, "hub.log")]);
|
|
1801
|
+
const hub=await connect();const pending:string[]=[];let ready=false;
|
|
1802
|
+
hub.onPush=message=>{if(message.t==="operational_log" && typeof message.line==="string"){if(ready)console.log(message.line);else if(pending.length<100)pending.push(message.line);}};
|
|
1803
|
+
try {
|
|
1804
|
+
const result=await hub.request({t:"conductor_operational",op:"logs",follow:args.includes("-f")});
|
|
1805
|
+
if(result.ok!==true)fail(String(result.error??"operational logs unavailable"),result.refusalAudited===true);
|
|
1806
|
+
for(const line of result.lines??[])console.log(line);
|
|
1807
|
+
ready=true;for(const line of pending)console.log(line);
|
|
1808
|
+
if(result.truncated)console.error("ahub: operational log history is truncated");
|
|
1809
|
+
if(args.includes("-f"))await new Promise<void>(resolve=>{
|
|
1810
|
+
const finish=()=>{process.off("SIGINT",finish);resolve();};
|
|
1811
|
+
hub.onClose=finish;process.once("SIGINT",finish);
|
|
1812
|
+
});
|
|
1813
|
+
} finally {hub.close();}
|
|
1814
|
+
},
|
|
1815
|
+
export: async () => {
|
|
1816
|
+
if(identity.role==="tools") {
|
|
1817
|
+
const hub=await connect();
|
|
1818
|
+
try {const result=await hub.request({t:"conductor_operational",op:"export",since:since()});if(result.ok!==true)fail(String(result.error??"operational export unavailable"),result.refusalAudited===true);for(const row of result.events??[])console.log(JSON.stringify(row));if(result.truncated)console.error("ahub: operational event history is truncated");}
|
|
1819
|
+
finally {hub.close();}
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1724
1822
|
for (const e of readEvents(join(stateDir, "events.jsonl"), since())) console.log(JSON.stringify(e));
|
|
1725
1823
|
},
|
|
1726
1824
|
bench: async () => {
|
|
@@ -2130,7 +2228,12 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2130
2228
|
console.log(options.json ? JSON.stringify(report, null, 2) : formatMemoryStaleAnchors(report));
|
|
2131
2229
|
return;
|
|
2132
2230
|
}
|
|
2133
|
-
if (args.includes("--orphans"))
|
|
2231
|
+
if (args.includes("--orphans")) {
|
|
2232
|
+
if(identity.role==="tools" && args.includes("--kill")) {
|
|
2233
|
+
const hub=await connect();try {const result=await hub.request({t:"conductor_operational",op:"orphans"},10_000);if(result.ok!==true)fail(String(result.error??"orphan inspection unavailable"),result.refusalAudited===true);console.log(JSON.stringify(result,null,2));}finally{hub.close();}return;
|
|
2234
|
+
}
|
|
2235
|
+
return orphanDoctor(args.includes("--kill"), options);
|
|
2236
|
+
}
|
|
2134
2237
|
const version = (bin: string) => {
|
|
2135
2238
|
const res = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
2136
2239
|
return res.status === 0 ? res.stdout.trim().split("\n")[0]! : undefined;
|
|
@@ -2287,8 +2390,26 @@ function inspectDoctorRouting(cwd: string): { routing?: Routing; errors: string[
|
|
|
2287
2390
|
}
|
|
2288
2391
|
|
|
2289
2392
|
async function runConductorCommand(): Promise<void> {
|
|
2393
|
+
const operation=conductorOperation(cmd,args);
|
|
2394
|
+
if(operation) {
|
|
2395
|
+
const denial=()=>peerCommandRefusal(identity.role==="tools"?identity.peer:"unknown",commandLabel);
|
|
2396
|
+
const descriptor=readControl(stateDir);
|
|
2397
|
+
if(!descriptor?.instanceId || !descriptor.projectId || !descriptor.pid || !descriptor.pidSignature || processLiveness(descriptor.pid,descriptor.pidSignature)!=="live")fail(denial());
|
|
2398
|
+
let hub:ControlClient;
|
|
2399
|
+
try {hub=await connect(750);} catch {fail(denial());}
|
|
2400
|
+
let release:(()=>void)|undefined;
|
|
2401
|
+
try {
|
|
2402
|
+
let result:any;
|
|
2403
|
+
try {result=await hub.request({t:"conductor_operational",op:"authorize",command:operation},750);} catch {fail(denial());}
|
|
2404
|
+
if(result.ok!==true || !hub.instanceId)fail(`${denial()}${result.error?`; ${String(result.error)}`:""}`,result.refusalAudited===true);
|
|
2405
|
+
release=ControlClient.pinInstance(stateDir,hub.instanceId);
|
|
2406
|
+
await commands[cmd]!();
|
|
2407
|
+
} finally {release?.();hub.close();}
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2290
2410
|
let op: string;
|
|
2291
2411
|
let input: Record<string, unknown>;
|
|
2412
|
+
if (cmd === "sdlc") return commands.sdlc!(); // #786: `sdlc merge` is the one conductor-class sdlc subcommand; the daemon checks the role.
|
|
2292
2413
|
if (cmd === "task") {
|
|
2293
2414
|
op = args[0] === "assign" ? "hub_task_assign" : "hub_task_escalate";
|
|
2294
2415
|
const { one, rest: positional } = takeFlags(args.slice(1), args[0] === "assign" ? ["--role"] : [], []);
|
package/src/cli/output.ts
CHANGED
|
@@ -19,7 +19,8 @@ export interface StatusOutput {
|
|
|
19
19
|
isolatedChecks?: { pending?: number; orphans?: string[]; unknownReceipts?: readonly string[]; maintenanceUnavailable?: boolean };
|
|
20
20
|
memoryRead?: { truncated: boolean; scanned: number; limit: number };
|
|
21
21
|
memoryProfile?: { profile?: string; generation?: string; state?: string; agentMemVersion?: string; skippedRows?: number; error?: string };
|
|
22
|
-
|
|
22
|
+
/** #784 AC3: `active`, `reviewWait` and `reserved` add up to `wip`; absent from an older hub. */
|
|
23
|
+
taskWip?: Record<string, { wip: number; wipLimit: number; active?: number; reviewWait?: number; reserved?: number }>;
|
|
23
24
|
pid?: number; controlPort?: number; cwd?: string; version?: string; peers: Record<string, any>; roleInfo?: Record<string, any>; actionPolicyInfo?: Record<string, { note: string; enforceable: boolean }>; budget?: Record<string, any>; planner?: { peers?: string[]; source?: string };
|
|
24
25
|
tasks?: Record<string, number>; models?: { backends?: any[] }; permissionDefaults?: any[]; deliveryError?: string; crash?: string[];
|
|
25
26
|
}
|
|
@@ -185,6 +186,10 @@ function rows(head: string[], data: OutputCell[][], columns?: number, details?:
|
|
|
185
186
|
}
|
|
186
187
|
return renderAtWidths(widths);
|
|
187
188
|
}
|
|
189
|
+
/** #784 AC3: ` (active 1, review-wait 2, reserved 0)`; nothing from an older hub that sends no breakdown. */
|
|
190
|
+
function wipSplit(value: { active?: number; reviewWait?: number; reserved?: number }): string {
|
|
191
|
+
return [value.active, value.reviewWait, value.reserved].every(n => Number.isSafeInteger(n)) ? ` (active ${value.active}, review-wait ${value.reviewWait}, reserved ${value.reserved})` : "";
|
|
192
|
+
}
|
|
188
193
|
function detail(label: string, text: unknown, columns?: number, tone?: Tone): Span[][] {
|
|
189
194
|
return lines(`${label} ${flat(text)}`, columns, tone, " ");
|
|
190
195
|
}
|
|
@@ -265,7 +270,7 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
265
270
|
if (peerRows.length) out.push([], ...rows(head, peerRows, columns, peerDetails));
|
|
266
271
|
else out.push(...lines("no peers attached yet (ahub claude | ahub codex | ahub kimi)", columns));
|
|
267
272
|
if (settling) out.push([], ...lines("settling: the adapter has not confirmed these deliveries yet (Claude: reply or hub_delivery_done). Task state is independent.", columns, "muted"));
|
|
268
|
-
if (data.taskWip) for (const [peer, value] of Object.entries(data.taskWip)) out.push(...detail(`${peer} planner WIP`, `${value.wip}/${value.wipLimit}`, columns));
|
|
273
|
+
if (data.taskWip) for (const [peer, value] of Object.entries(data.taskWip)) out.push(...detail(`${peer} planner WIP`, `${value.wip}/${value.wipLimit}${wipSplit(value)}`, columns));
|
|
269
274
|
if (data.models?.backends?.length) {
|
|
270
275
|
out.push([], ...rows(["MODEL", "STATE", "ACTIVE", "REQUESTED", "ACTUAL", "PROVIDER"], data.models.backends.map(b => [span(backendLabel(b)), span(b.state ?? "unknown", stateTone(b.state ?? "")), span(b.active || "-"), span(b.requestedModel ?? "-"), span(b.actualModel ?? "-"), span(b.provider ?? "-")]), columns));
|
|
271
276
|
for (const b of data.models.backends) for (const [key, label] of [["coolingUntil", "cooling"], ["failingUntil", "dispatch held"]]) if (b[key!]) { const at = Date.parse(b[key!]); out.push(...detail(`${backendLabel(b)} ${label}`, `${Number.isFinite(at) ? relative(at, now) : "unknown"}${key === "coolingUntil" && typeof b.failures === "number" ? ` after ${b.failures} failures` : key === "failingUntil" ? "; no load moves until then" : ""}`, columns, "attention")); }
|