@staix/agent-hub 0.17.0 → 0.19.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 +35 -0
- package/README.md +4 -4
- package/docs/agent-notes/README.md +11 -0
- package/docs/agent-notes/adapters.md +10 -6
- package/docs/agent-notes/benchmarks.md +1 -0
- package/docs/agent-notes/daemon.md +6 -2
- package/docs/agent-notes/local-worker.md +3 -1
- package/docs/agent-notes/tasks.md +11 -2
- package/docs/agent-notes/tests.md +7 -3
- package/docs/dogfood.md +22 -2
- package/docs/memory.md +1 -1
- package/docs/operations.md +162 -35
- package/docs/profiles.md +36 -0
- package/docs/quickstart.md +15 -5
- package/docs/sdlc.md +29 -3
- package/docs/security.md +44 -13
- package/docs/smoke.md +3 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +31 -10
- package/docs/verification/2026-10-25-0.18.0.md +76 -0
- package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
- package/docs/verification/2026-10-25-remote-docker-transport.md +56 -0
- package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
- package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
- package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
- package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
- package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
- package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
- package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
- package/docs/verification/2026-10-27-0.19.0.md +86 -0
- package/docs/verification/claude-permission-transport-native.py +187 -0
- package/docs/verification/claude-shell-wrapper-host.ts +20 -0
- package/docs/verification/claude-shell-wrapper-native.py +145 -0
- package/docs/verification/pi-140-model-free.ts +57 -0
- package/docs/verification/pi-140-native.mjs +21 -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 +24 -18
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +364 -98
- package/src/adapters/agy/entry.ts +1 -1
- package/src/adapters/claude-channel.ts +7 -3
- package/src/adapters/claude-implementer.ts +5 -3
- package/src/adapters/compat.ts +32 -18
- package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
- package/src/adapters/definitions/builtin-metadata.ts +3 -3
- package/src/adapters/definitions/native-factories.ts +7 -2
- package/src/adapters/definitions/reviewer-variants.ts +2 -2
- package/src/adapters/local-worker.ts +5 -3
- package/src/adapters/pi.ts +39 -3
- package/src/adapters/qwen-dual.ts +25 -0
- package/src/adapters/registry-types.ts +6 -2
- package/src/adapters/registry.ts +1 -0
- package/src/cli/agents.ts +69 -7
- package/src/cli/claude-shell-wrapper.ts +54 -0
- 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 +198 -71
- package/src/cli/output.ts +11 -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/hub/agent-drift.ts +61 -0
- package/src/hub/board.ts +31 -2
- package/src/hub/check-runner.ts +385 -25
- package/src/hub/claude-shell-wrapper.ts +349 -0
- 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-edit.ts +2 -2
- package/src/hub/config-tier.ts +3 -3
- package/src/hub/daemon.ts +530 -87
- 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 +21 -1
- package/src/hub/hub-tools.ts +2 -2
- package/src/hub/instances.ts +18 -2
- package/src/hub/lifecycle.ts +1 -1
- package/src/hub/operational-authority.ts +67 -0
- package/src/hub/operational-view.ts +137 -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 +81 -0
- package/src/hub/read-only-shell.ts +162 -0
- package/src/hub/review-scope.ts +13 -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-metrics.ts +6 -4
- 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/task-shell-command.ts +85 -0
- package/src/hub/task-shell-default.ts +78 -0
- package/src/hub/task-shell-proof.ts +171 -0
- package/src/hub/task-shell-sandbox.ts +61 -0
- package/src/hub/tasks.ts +378 -27
- package/src/local/deny.ts +91 -11
- package/src/local/sandbox.ts +67 -28
- package/src/local/shell-environment.ts +10 -0
- package/src/local/tools.ts +17 -4
- package/src/ui/index.html +3 -1
package/src/cli/main.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { conductorOperation, OPERATIONAL_PERSON_GUIDANCE } 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,11 +487,17 @@ 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, authorityRefused?:boolean): never {
|
|
491
|
+
if(operationalAuditReady && authorityRefused!==false){if(refusalAudited)operationalRefusalRecorded=true;if(!operationalRefusalRecorded)audit("refused");}
|
|
451
492
|
console.error(`ahub: ${message}`);
|
|
452
493
|
process.exit(1);
|
|
453
494
|
}
|
|
454
495
|
|
|
496
|
+
function operationalIncomplete(result: {complete?:boolean;incompleteReason?:unknown;truncated?:boolean}, history: "log"|"event"): void {
|
|
497
|
+
if(result.complete===false)console.error(`ahub: operational output is incomplete: ${typeof result.incompleteReason==="string"?terminalText(result.incompleteReason).replace(/[\r\n]+/g," ").slice(0,200):"public privacy or history coverage unavailable"}`);
|
|
498
|
+
else if(result.truncated)console.error(`ahub: operational ${history} history is truncated`);
|
|
499
|
+
}
|
|
500
|
+
|
|
455
501
|
async function taskOp(op: string, a: Record<string, unknown>, throwOnError = false, connection?: ControlClient, timeoutMs?: number): Promise<string> {
|
|
456
502
|
const hub = connection ?? await connect();
|
|
457
503
|
try {
|
|
@@ -459,11 +505,12 @@ async function taskOp(op: string, a: Record<string, unknown>, throwOnError = fal
|
|
|
459
505
|
const res = await hub.request({ t: "task", op, args: a }, timeoutMs ?? (op === "github_import" ? 60_000 : 30_000));
|
|
460
506
|
if (!res.ok) {
|
|
461
507
|
if (throwOnError) throw new Error(res.error);
|
|
462
|
-
|
|
508
|
+
if(res.refusalAudited===true)operationalRefusalRecorded=true;
|
|
509
|
+
if(!operationalAuditReady || res.authorityRefused!==false)audit("refused");
|
|
463
510
|
// #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
511
|
const cause = agentShellCause(identity, String(res.error));
|
|
465
512
|
if (cause) console.error(`ahub: ${res.error}\n${cause}`), process.exit(1);
|
|
466
|
-
fail(res.error);
|
|
513
|
+
fail(res.error,res.refusalAudited===true,res.authorityRefused);
|
|
467
514
|
}
|
|
468
515
|
return res.text;
|
|
469
516
|
} finally { if (!connection) hub.close(); }
|
|
@@ -509,7 +556,9 @@ async function hold(t: "pause" | "resume"): Promise<void> {
|
|
|
509
556
|
}
|
|
510
557
|
|
|
511
558
|
function recoveryDisplay() {
|
|
512
|
-
|
|
559
|
+
// A terminal gives its own width; a pipe gives COLUMNS when set, else 80 (#814: a person reads a pipe too).
|
|
560
|
+
const width = process.stdout.columns ?? outputWidth({ isTTY: false, COLUMNS: process.env.COLUMNS }) ?? 80;
|
|
561
|
+
return readRecoveryPresentationFlags(args, { isTTY: !!process.stdout.isTTY, columns: width, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR });
|
|
513
562
|
}
|
|
514
563
|
const humanRecovery = (json: boolean) => !json && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
515
564
|
const printRecovery = (lines: readonly string[]) => { for (const line of lines) console.log(line); };
|
|
@@ -656,12 +705,15 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
656
705
|
if (interactive) return onTerminal((io) => planScreen(upgradeHost(kind, to ?? VERSION), io), display.options);
|
|
657
706
|
const plan = await makeUpgradePlan(kind, to ?? VERSION, kind === "restart" ? cwd : undefined);
|
|
658
707
|
const jsonApply = display.json && yes && !dry;
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
708
|
+
// #814: a person reading a pipe or a dry run gets the plan screen's rows, with every blocker, reconnect-only session and
|
|
709
|
+
// fresh start in them. `--json` keeps the document and the stderr lines (#206) exactly as they were.
|
|
710
|
+
if (display.json) {
|
|
711
|
+
if (!jsonApply) console.log(JSON.stringify(plan, null, 2));
|
|
712
|
+
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`);
|
|
713
|
+
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`);
|
|
714
|
+
} else printRecovery(renderUpgradePlan(plan, VERSION, display.options));
|
|
663
715
|
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}`);
|
|
716
|
+
if (display.json) for (const blocker of blockers) console.error(`ahub: blocker: ${blocker}`);
|
|
665
717
|
if (dry) return;
|
|
666
718
|
if (blockers.length) { if (jsonApply) console.log(JSON.stringify(plan, null, 2)); fail("plan has blockers; no runtime was changed"); }
|
|
667
719
|
assertLifecycleAvailable();
|
|
@@ -724,14 +776,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
724
776
|
return;
|
|
725
777
|
}
|
|
726
778
|
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
|
-
}
|
|
779
|
+
if (display.json) { console.log(JSON.stringify(publicOperation(result), null, 2)); return; }
|
|
780
|
+
printRecovery(renderOperation(result, undefined, display.options));
|
|
781
|
+
const live = await liveProjects(result, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
782
|
+
printRecovery(renderBlocked(result, nextChoices(result, undefined, live), display.options));
|
|
735
783
|
return;
|
|
736
784
|
}
|
|
737
785
|
// #272: without an id, the operation that holds the machine's lock; bare on a terminal, its screen.
|
|
@@ -761,7 +809,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
761
809
|
if (action === "status") {
|
|
762
810
|
// #215: abort and resume are offered by what the open sources show, read as the runner's errors read them.
|
|
763
811
|
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
764
|
-
console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
812
|
+
if (display.json) console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
813
|
+
else printRecovery(renderRecoveryStatus(operation, runner, nextChoices(operation, runner, live), display.options));
|
|
765
814
|
}
|
|
766
815
|
else if (action === "abort") {
|
|
767
816
|
await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer));
|
|
@@ -780,11 +829,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
780
829
|
console.log(`${one["--fresh-session"]}: a fresh session is accepted and the lost one is recorded; resuming`);
|
|
781
830
|
return spawnRecovery(result, false, humanRecovery(false) ? display.options : undefined);
|
|
782
831
|
}
|
|
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
|
-
}
|
|
832
|
+
if (display.json) console.log(JSON.stringify(publicOperation(result), null, 2));
|
|
833
|
+
else printRecovery(renderSummary(result, await recoveryEndInspection(result), display.options));
|
|
788
834
|
}
|
|
789
835
|
else if (["completed", "cancelled"].includes(operation.phase)) console.log(display.json ? JSON.stringify(publicOperation(operation, runner), null, 2) : `recovery is already ${operation.phase}`);
|
|
790
836
|
// #215: an operation being abandoned is never resumed, whichever coordinator started it (an older one would).
|
|
@@ -973,7 +1019,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
973
1019
|
|
|
974
1020
|
models: async () => {
|
|
975
1021
|
const action = outputArguments()[0] ?? "status";
|
|
976
|
-
const options =
|
|
1022
|
+
const options = ["status", "start", "setup"].includes(action) ? outputOptions() : undefined;
|
|
977
1023
|
const showStatus = (data: unknown) => options!.json ? console.log(JSON.stringify(data, null, 2)) : printOutput(renderModelsStatus(data, options!.columns, Date.now()), options!.color);
|
|
978
1024
|
const configured = projectConfig().mlx;
|
|
979
1025
|
if (configured.enabled === false) {
|
|
@@ -985,13 +1031,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
985
1031
|
const modelPath = configured.modelPath ? resolve(cwd, configured.modelPath) : join(homedir(), ".agenthub", "models", "qwen3-8b-mlx");
|
|
986
1032
|
const mlxOptions = configured.provider === "ollama" ? configured : { ...configured, runtimeDir, modelPath };
|
|
987
1033
|
if (action === "status") return showStatus(await inspectMlx(mlxOptions));
|
|
988
|
-
if (action === "start") { const handle = await ensureMlx(mlxOptions); return
|
|
1034
|
+
if (action === "start") { const handle = await ensureMlx(mlxOptions); return showStatus(handle.status()); }
|
|
989
1035
|
if (action === "stop") { await stopMlx(mlxOptions); return console.log("MLX stopped"); }
|
|
990
1036
|
if (action !== "setup") fail("usage: ahub models setup|status|start|stop");
|
|
991
1037
|
if (configured.provider === "ollama") {
|
|
992
1038
|
await setupOllamaModel(configured);
|
|
993
1039
|
const handle = await ensureMlx(configured);
|
|
994
|
-
return
|
|
1040
|
+
return showStatus(handle.status());
|
|
995
1041
|
}
|
|
996
1042
|
const python = join(runtimeDir, "bin", "python");
|
|
997
1043
|
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 +1070,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1024
1070
|
const { one, rest: flags } = takeFlags(rest, ["--peer"], []);
|
|
1025
1071
|
if (flags.length) fail("usage: ahub queue list [--peer <id>] [--json] [--full] [--color=auto|always|never]");
|
|
1026
1072
|
const result = await hub.request({ t: "queue", op: "list", ...(one["--peer"] ? { peer: one["--peer"] } : {}) });
|
|
1027
|
-
if (!result.ok) fail(result.error);
|
|
1073
|
+
if (!result.ok) fail(result.error,result.refusalAudited===true,result.authorityRefused);
|
|
1028
1074
|
if (options.json) return console.log(JSON.stringify(result.deliveries, null, 2));
|
|
1029
1075
|
printOutput(renderQueue(result.deliveries, options.columns, Date.now()), options.color);
|
|
1030
1076
|
return;
|
|
@@ -1036,7 +1082,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1036
1082
|
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
1083
|
if (yes && !one["--reason"]?.trim()) fail("ahub queue discard needs --reason <text> with --yes: it is recorded on every discarded delivery");
|
|
1038
1084
|
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);
|
|
1085
|
+
if (!result.ok) fail(result.error,result.refusalAudited===true,result.authorityRefused);
|
|
1040
1086
|
if (options.json) return console.log(JSON.stringify(result, null, 2));
|
|
1041
1087
|
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
1088
|
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 +1093,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1047
1093
|
if (operation === "show") {
|
|
1048
1094
|
if (flags.length) fail("usage: ahub queue show <id>");
|
|
1049
1095
|
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
1050
|
-
if (!shown.ok) fail(shown.error);
|
|
1096
|
+
if (!shown.ok) fail(shown.error,shown.refusalAudited===true,shown.authorityRefused);
|
|
1051
1097
|
if (options.json) return console.log(JSON.stringify(shown.delivery, null, 2));
|
|
1052
1098
|
return printOutput(renderQueueShow(shown.delivery, options.columns, Date.now()), options.color);
|
|
1053
1099
|
}
|
|
@@ -1057,12 +1103,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1057
1103
|
let revision = parsedRevision;
|
|
1058
1104
|
if (revision === undefined) {
|
|
1059
1105
|
const shown = await hub.request({ t: "queue", op: "show", id });
|
|
1060
|
-
if (!shown.ok) fail(shown.error);
|
|
1106
|
+
if (!shown.ok) fail(shown.error,shown.refusalAudited===true,shown.authorityRefused);
|
|
1061
1107
|
revision = shown.delivery.revision;
|
|
1062
1108
|
}
|
|
1063
1109
|
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));
|
|
1110
|
+
if (!result.ok) fail(result.error,result.refusalAudited===true,result.authorityRefused);
|
|
1111
|
+
if (options.json) console.log(JSON.stringify(result.delivery, null, 2));
|
|
1112
|
+
else printOutput(renderQueueShow(result.delivery, options.columns, Date.now()), options.color);
|
|
1066
1113
|
} finally { hub.close(); }
|
|
1067
1114
|
},
|
|
1068
1115
|
|
|
@@ -1120,7 +1167,8 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1120
1167
|
const result = await hub.request(request); hub.close();
|
|
1121
1168
|
if (!result.ok) fail(result.error);
|
|
1122
1169
|
const data = result.budgets ?? result.budget ?? { disabled: result.disabled };
|
|
1123
|
-
if (
|
|
1170
|
+
if (options.json) return console.log(JSON.stringify(data, null, 2));
|
|
1171
|
+
if (op === "configure" || op === "disable") return printOutput(renderBudgetResult(op, op === "configure" ? result.budget : result, options.columns, Date.now()), options.color);
|
|
1124
1172
|
return printOutput(renderExecutionBudgetStatus(result.budgets, options.columns, Date.now()), options.color);
|
|
1125
1173
|
}
|
|
1126
1174
|
let set: Record<string, unknown> | undefined;
|
|
@@ -1148,7 +1196,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1148
1196
|
},
|
|
1149
1197
|
|
|
1150
1198
|
backlog: async () => {
|
|
1151
|
-
const
|
|
1199
|
+
const split = splitPresentation(args, { edges: true }), operands = split.operands;
|
|
1200
|
+
const [sub, id, revision, value, ...reason] = operands;
|
|
1201
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1202
|
+
const shown = (render: (data: any, columns?: number, now?: number) => Span[][]) => async (text: Promise<string>) => showToolText(render, await text, present);
|
|
1152
1203
|
if (sub === "list" && outputArguments().length === 1) {
|
|
1153
1204
|
// #399: a table by default; --json prints the hub's listing unchanged, and a refusal is printed as it came.
|
|
1154
1205
|
const options = outputOptions(), raw = await taskOp("hub_backlog_list", {});
|
|
@@ -1158,43 +1209,44 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1158
1209
|
if (!Array.isArray(cards)) return console.log(raw);
|
|
1159
1210
|
return printOutput(renderBacklog(cards, options.columns, Date.now(), options.full), options.color);
|
|
1160
1211
|
}
|
|
1161
|
-
if (sub === "groom" &&
|
|
1162
|
-
if (["return", "return-untouched"].includes(sub ?? "") && /^[1-9]\d*$/.test(id ?? "") && /^\d+$/.test(revision ?? "") &&
|
|
1163
|
-
return
|
|
1212
|
+
if (sub === "groom" && operands.length === 1) return shown(renderGroom)(taskOp("hub_planner_groom", {}));
|
|
1213
|
+
if (["return", "return-untouched"].includes(sub ?? "") && /^[1-9]\d*$/.test(id ?? "") && /^\d+$/.test(revision ?? "") && operands.length >= 4) {
|
|
1214
|
+
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
1215
|
}
|
|
1165
1216
|
if (sub === "add" && args.length >= 3) return console.log(await taskOp("hub_task_propose", { class: id, title: args.slice(2).join(" "), backlog: true }));
|
|
1166
1217
|
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 ?? "") &&
|
|
1218
|
+
if (["pull", "return"].includes(sub ?? "") && operands.length === 2) return console.log(await taskOp(sub === "pull" ? "backlog_pull" : "backlog_return", { id }));
|
|
1168
1219
|
if (!/^\d+$/.test(revision ?? "")) fail("planning revision is required");
|
|
1169
|
-
if (sub === "pull-ready" &&
|
|
1220
|
+
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
1221
|
if (sub === "split" && value && reason.length) {
|
|
1171
1222
|
let parts: unknown;try { parts=JSON.parse(value); } catch { return fail("split parts must be JSON"); }
|
|
1172
|
-
return
|
|
1223
|
+
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
1224
|
}
|
|
1174
|
-
if (sub === "undo" &&
|
|
1225
|
+
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
1226
|
if (!["plan", "pin"].includes(sub ?? "") || !value || !reason.length) fail("planning needs fields and a reason");
|
|
1176
1227
|
let fields: unknown;
|
|
1177
1228
|
try { fields = sub === "pin" ? { pinned: value === "true" ? true : value === "false" ? false : undefined } : JSON.parse(value); }
|
|
1178
1229
|
catch { return fail("planning fields must be JSON"); }
|
|
1179
1230
|
if (sub === "pin" && value !== "true" && value !== "false") fail("pin must be true or false");
|
|
1180
|
-
|
|
1231
|
+
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
1232
|
},
|
|
1182
1233
|
|
|
1183
1234
|
release: async () => {
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
if (sub === "
|
|
1235
|
+
const split = splitPresentation(args, { edges: true }), operands = split.operands;
|
|
1236
|
+
const [sub, name, revision, value] = operands;
|
|
1237
|
+
const present = outputOptions(split.flags); // before any request: an invalid --color must refuse before a mutation is sent
|
|
1238
|
+
const shown = (render: (data: any, columns?: number, now?: number) => Span[][]) => async (text: Promise<string>) => showToolText(render, await text, present);
|
|
1239
|
+
if (sub === "list" && operands.length === 1) return shown(renderReleaseList)(taskOp("hub_release_list", {}));
|
|
1240
|
+
if (sub === "show" && name && operands.length === 2) return shown(renderReleaseShow)(taskOp("release_show", { name }));
|
|
1241
|
+
if (sub === "new" && name && revision && operands.length === 4 && /^\d+$/.test(value ?? "")) return shown(renderReleaseNew)(taskOp("release_new", { name, goal: revision, order: Number(value) }));
|
|
1190
1242
|
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
1243
|
const states: Record<string, string> = { activate: "active", freeze: "frozen", ship: "shipped", drop: "dropped" };
|
|
1192
1244
|
let fields: unknown;
|
|
1193
|
-
if (sub === "plan" && value &&
|
|
1245
|
+
if (sub === "plan" && value && operands.length === 4) {
|
|
1194
1246
|
try { fields = JSON.parse(value); } catch { return fail("release dates must be JSON"); }
|
|
1195
|
-
} else if (sub && Object.hasOwn(states, sub) &&
|
|
1247
|
+
} else if (sub && Object.hasOwn(states, sub) && operands.length === 3) fields = { state: states[sub] };
|
|
1196
1248
|
else return fail("invalid release command");
|
|
1197
|
-
|
|
1249
|
+
return shown(renderReleaseChange)(taskOp("release_change", { name, revision: Number(revision), fields }));
|
|
1198
1250
|
},
|
|
1199
1251
|
|
|
1200
1252
|
deliverables: async () => {
|
|
@@ -1256,6 +1308,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1256
1308
|
},
|
|
1257
1309
|
|
|
1258
1310
|
sdlc: async () => {
|
|
1311
|
+
const args = sdlcOperands(), present = SDLC_READ.includes(args[0] ?? "") ? outputOptions() : undefined;
|
|
1312
|
+
/** 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. */
|
|
1313
|
+
const human = (render: (data: any, columns?: number, now?: number) => Span[][], data: unknown) => printOutput(render(data, present!.columns, Date.now()), present!.color);
|
|
1259
1314
|
const sub = args[0], id = args[1] === undefined ? undefined : Number(args[1]);
|
|
1260
1315
|
if (sub === "authorize-release") {
|
|
1261
1316
|
if (args.length !== 2 || !validReleaseVersion(args[1])) fail("usage: ahub sdlc authorize-release <version>");
|
|
@@ -1263,7 +1318,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1263
1318
|
const commit = result.stdout?.trim();
|
|
1264
1319
|
if (result.status !== 0 || !commit || !/^[a-f0-9]{40}$/.test(commit)) fail("candidate commit unavailable");
|
|
1265
1320
|
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)); }
|
|
1321
|
+
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
1322
|
finally { hub.close(); }
|
|
1268
1323
|
return;
|
|
1269
1324
|
}
|
|
@@ -1305,7 +1360,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1305
1360
|
const repo = args[1], liveIssues = args.slice(2).map(Number);
|
|
1306
1361
|
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
1362
|
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)); }
|
|
1363
|
+
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
1364
|
finally { hub.close(); }
|
|
1310
1365
|
return;
|
|
1311
1366
|
}
|
|
@@ -1330,12 +1385,14 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1330
1385
|
}
|
|
1331
1386
|
if (sub === "costs") {
|
|
1332
1387
|
if (args.length > 2) fail("usage: ahub sdlc costs [owner/repository]");
|
|
1333
|
-
|
|
1388
|
+
const text = await taskOp("hub_sdlc_costs", { ...(args[1] ? { repo: args[1] } : {}) });
|
|
1389
|
+
return showToolText(renderSdlcCosts, text, present!);
|
|
1334
1390
|
}
|
|
1335
1391
|
if (sub === "metrics") {
|
|
1336
1392
|
const flags = takeFlags(args.slice(1), ["--since"], []);
|
|
1337
1393
|
if (flags.rest.length) fail("usage: ahub sdlc metrics [--since <YYYY-MM-DD|ISO time>]");
|
|
1338
|
-
|
|
1394
|
+
const text = await taskOp("hub_sdlc_metrics", { ...(flags.one["--since"] !== undefined ? { since: flags.one["--since"] } : {}) });
|
|
1395
|
+
return showToolText(renderSdlcMetrics, text, present!);
|
|
1339
1396
|
}
|
|
1340
1397
|
if (sub === "plan-review") {
|
|
1341
1398
|
const revision = Number(args[2]);
|
|
@@ -1356,6 +1413,20 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1356
1413
|
} finally { hub.close(); }
|
|
1357
1414
|
return;
|
|
1358
1415
|
}
|
|
1416
|
+
if (sub === "merge") {
|
|
1417
|
+
// #786 T1: the guarded merge of one approved task; a person or the configured conductor asks, the daemon re-reads every gate.
|
|
1418
|
+
const flags = takeFlags(args.slice(2), [], []);
|
|
1419
|
+
const dryRun = flags.rest.includes("--dry-run");
|
|
1420
|
+
if (flags.rest.some(arg => arg !== "--dry-run") || !Number.isSafeInteger(id) || id! <= 0) fail("usage: ahub sdlc merge <task> [--dry-run]");
|
|
1421
|
+
const hub = await connect();
|
|
1422
|
+
try {
|
|
1423
|
+
const reply = await hub.request({ t: "sdlc", op: "merge", id, ...(dryRun ? { dryRun: true } : {}) }, 60_000);
|
|
1424
|
+
if (!reply.ok) fail(reply.error);
|
|
1425
|
+
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));
|
|
1426
|
+
if (reply.decision === "refused" || reply.ran && reply.result?.state !== "merged") process.exitCode = 1;
|
|
1427
|
+
} finally { hub.close(); }
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1359
1430
|
if (sub === "accept") {
|
|
1360
1431
|
// #481 S1: the person accepts the task's spec issue at its current body; agent shells are refused before this runs.
|
|
1361
1432
|
const flags = takeFlags(args.slice(2), ["--issue", "--repo"], []);
|
|
@@ -1379,12 +1450,19 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1379
1450
|
try {
|
|
1380
1451
|
const reply = await hub.request({ t: "sdlc", op: sub, ...(id === undefined ? {} : { id }) }, 30_000);
|
|
1381
1452
|
if (!reply.ok) fail(reply.error);
|
|
1382
|
-
|
|
1453
|
+
// `sdlc export` is a data format by purpose (#814): it stays JSON and never reaches a renderer.
|
|
1454
|
+
if (sub === "export") console.log(JSON.stringify(reply, null, 2));
|
|
1455
|
+
else if (present!.json) console.log(JSON.stringify(reply, null, 2));
|
|
1456
|
+
else human(sub === "explain" ? renderSdlcExplain : renderSdlcStatus, reply);
|
|
1383
1457
|
} finally { hub.close(); }
|
|
1384
1458
|
},
|
|
1385
1459
|
|
|
1386
1460
|
task: async () => {
|
|
1387
|
-
const [
|
|
1461
|
+
const presentation = splitPresentation(args, { valueFlags: ["--reason", "--superseded-by", "--revision"], edges: true });
|
|
1462
|
+
const shownTask = (render: (data: any, columns?: number, now?: number) => Span[][]) => async (text: Promise<string>) => showToolText(render, await text, outputOptions(presentation.flags));
|
|
1463
|
+
// 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)
|
|
1464
|
+
if (["show", "cancel"].includes(args[0] ?? "")) outputOptions(presentation.flags);
|
|
1465
|
+
const [sub, ...rest] = ["show", "cancel"].includes(args[0] ?? "") ? presentation.operands : args;
|
|
1388
1466
|
if (sub === "role") {
|
|
1389
1467
|
if (rest.length !== 2 || !/^[1-9]\d*$/.test(rest[0]!)) fail("usage: ahub task role <id> <role|none>");
|
|
1390
1468
|
const hub = await connect();
|
|
@@ -1396,9 +1474,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1396
1474
|
const { one, rest: positional } = takeFlags(rest, ["--reason", "--superseded-by", "--revision"], []);
|
|
1397
1475
|
const revision = one["--revision"] === undefined ? undefined : Number(one["--revision"]);
|
|
1398
1476
|
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
|
|
1477
|
+
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 }) }));
|
|
1478
|
+
}
|
|
1479
|
+
if (sub === "show") {
|
|
1480
|
+
// a stray operand (for example the value of a `--color always` that is not the `--color=` form) is a usage error, never silently ignored
|
|
1481
|
+
if (rest.length !== 1) fail("usage: ahub task show <id> [--json] [--full] [--color=auto|always|never]");
|
|
1482
|
+
return shownTask(renderTaskShow)(taskOp("task_show", { id: rest[0] }));
|
|
1400
1483
|
}
|
|
1401
|
-
if (sub === "show") return console.log(await taskOp("task_show", { id: rest[0] }));
|
|
1402
1484
|
if (sub === "escalate") return console.log(await taskOp("task_escalate", { id: rest[0] }));
|
|
1403
1485
|
if (sub === "assign") {
|
|
1404
1486
|
const { one, rest: positional } = takeFlags(rest, ["--role", "--revision"], []);
|
|
@@ -1489,7 +1571,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1489
1571
|
const reply = await hub.request({ t: "peer_stop", peer }, 35_000);
|
|
1490
1572
|
if (reply.ok !== true) {
|
|
1491
1573
|
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);
|
|
1574
|
+
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,reply.authorityRefused);
|
|
1493
1575
|
}
|
|
1494
1576
|
console.log(`${peer}: ${reply.state}`);
|
|
1495
1577
|
} finally { hub.close(); }
|
|
@@ -1525,7 +1607,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1525
1607
|
const hub = await connect();
|
|
1526
1608
|
try {
|
|
1527
1609
|
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; };
|
|
1610
|
+
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,reply.authorityRefused); return reply; };
|
|
1529
1611
|
if (sub === "list") {
|
|
1530
1612
|
const view = await request({ t: "settings_get" });
|
|
1531
1613
|
if (!Array.isArray(view.roster)) fail("upgrade the running hub to use ahub roster");
|
|
@@ -1591,7 +1673,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1591
1673
|
const reply = await hub.request(message, 35_000);
|
|
1592
1674
|
if (reply.ok !== true) {
|
|
1593
1675
|
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);
|
|
1676
|
+
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,reply.authorityRefused);
|
|
1595
1677
|
}
|
|
1596
1678
|
return reply;
|
|
1597
1679
|
};
|
|
@@ -1719,8 +1801,29 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1719
1801
|
} finally { hub.close(); }
|
|
1720
1802
|
},
|
|
1721
1803
|
|
|
1722
|
-
logs: () =>
|
|
1723
|
-
|
|
1804
|
+
logs: async () => {
|
|
1805
|
+
if (identity.role !== "tools") return exec("tail", [args.includes("-f") ? "-f" : "-n100", join(stateDir, "hub.log")]);
|
|
1806
|
+
const hub=await connect();const pending:string[]=[];let ready=false,incompleteShown=false;
|
|
1807
|
+
hub.onPush=message=>{if(message.t==="operational_log" && typeof message.line==="string"){if(message.complete===false && !incompleteShown){operationalIncomplete(message,"log");incompleteShown=true;}if(ready)console.log(message.line);else if(pending.length<100)pending.push(message.line);}};
|
|
1808
|
+
try {
|
|
1809
|
+
const result=await hub.request({t:"conductor_operational",op:"logs",follow:args.includes("-f")});
|
|
1810
|
+
if(result.ok!==true)fail(String(result.error??"operational logs unavailable"),result.refusalAudited===true,result.authorityRefused);
|
|
1811
|
+
for(const line of result.lines??[])console.log(line);
|
|
1812
|
+
ready=true;for(const line of pending)console.log(line);
|
|
1813
|
+
if(!incompleteShown){operationalIncomplete(result,"log");if(result.complete===false)incompleteShown=true;}
|
|
1814
|
+
if(args.includes("-f"))await new Promise<void>(resolve=>{
|
|
1815
|
+
const finish=()=>{process.off("SIGINT",finish);resolve();};
|
|
1816
|
+
hub.onClose=finish;process.once("SIGINT",finish);
|
|
1817
|
+
});
|
|
1818
|
+
} finally {hub.close();}
|
|
1819
|
+
},
|
|
1820
|
+
export: async () => {
|
|
1821
|
+
if(identity.role==="tools") {
|
|
1822
|
+
const hub=await connect();
|
|
1823
|
+
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,result.authorityRefused);for(const row of result.events??[])console.log(JSON.stringify(row));operationalIncomplete(result,"event");}
|
|
1824
|
+
finally {hub.close();}
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1724
1827
|
for (const e of readEvents(join(stateDir, "events.jsonl"), since())) console.log(JSON.stringify(e));
|
|
1725
1828
|
},
|
|
1726
1829
|
bench: async () => {
|
|
@@ -2130,7 +2233,12 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2130
2233
|
console.log(options.json ? JSON.stringify(report, null, 2) : formatMemoryStaleAnchors(report));
|
|
2131
2234
|
return;
|
|
2132
2235
|
}
|
|
2133
|
-
if (args.includes("--orphans"))
|
|
2236
|
+
if (args.includes("--orphans")) {
|
|
2237
|
+
if(identity.role==="tools" && args.includes("--kill")) {
|
|
2238
|
+
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,result.authorityRefused);console.log(JSON.stringify(result,null,2));}finally{hub.close();}return;
|
|
2239
|
+
}
|
|
2240
|
+
return orphanDoctor(args.includes("--kill"), options);
|
|
2241
|
+
}
|
|
2134
2242
|
const version = (bin: string) => {
|
|
2135
2243
|
const res = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
2136
2244
|
return res.status === 0 ? res.stdout.trim().split("\n")[0]! : undefined;
|
|
@@ -2287,8 +2395,27 @@ function inspectDoctorRouting(cwd: string): { routing?: Routing; errors: string[
|
|
|
2287
2395
|
}
|
|
2288
2396
|
|
|
2289
2397
|
async function runConductorCommand(): Promise<void> {
|
|
2398
|
+
const operation=conductorOperation(cmd,args);
|
|
2399
|
+
if(operation) {
|
|
2400
|
+
const unavailable=`conductor authorization unavailable; nothing was executed; ${OPERATIONAL_PERSON_GUIDANCE}`;
|
|
2401
|
+
const descriptor=readControl(stateDir);
|
|
2402
|
+
if(!descriptor?.instanceId || !descriptor.projectId || !descriptor.pid || !descriptor.pidSignature || processLiveness(descriptor.pid,descriptor.pidSignature)!=="live")fail(unavailable);
|
|
2403
|
+
let hub:ControlClient;
|
|
2404
|
+
try {hub=await connect(3000);} catch {fail(unavailable);}
|
|
2405
|
+
let release:(()=>void)|undefined;
|
|
2406
|
+
try {
|
|
2407
|
+
let result:any;
|
|
2408
|
+
try {result=await hub.request({t:"conductor_operational",op:"authorize",command:operation},3000);} catch {fail(unavailable);}
|
|
2409
|
+
if(result.ok!==true)fail(result.authorityRefused===true || result.authorityRefused===false?String(result.error??unavailable):unavailable,result.refusalAudited===true,result.authorityRefused);
|
|
2410
|
+
if(!hub.instanceId)fail(unavailable);
|
|
2411
|
+
release=ControlClient.pinInstance(stateDir,hub.instanceId);
|
|
2412
|
+
await commands[cmd]!();
|
|
2413
|
+
} finally {release?.();hub.close();}
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2290
2416
|
let op: string;
|
|
2291
2417
|
let input: Record<string, unknown>;
|
|
2418
|
+
if (cmd === "sdlc") return commands.sdlc!(); // #786: `sdlc merge` is the one conductor-class sdlc subcommand; the daemon checks the role.
|
|
2292
2419
|
if (cmd === "task") {
|
|
2293
2420
|
op = args[0] === "assign" ? "hub_task_assign" : "hub_task_escalate";
|
|
2294
2421
|
const { one, rest: positional } = takeFlags(args.slice(1), args[0] === "assign" ? ["--role"] : [], []);
|