@staix/agent-hub 0.30.0 → 0.31.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 +39 -0
- package/README.md +13 -7
- package/docs/agent-notes/README.md +1 -0
- package/docs/agent-notes/adapters.md +5 -2
- package/docs/agent-notes/budget.md +1 -0
- package/docs/agent-notes/bus.md +9 -4
- package/docs/agent-notes/daemon.md +15 -8
- package/docs/agent-notes/local-worker.md +1 -0
- package/docs/agent-notes/tasks.md +7 -3
- package/docs/agent-notes/tests.md +6 -4
- package/docs/design/adapters.md +52 -0
- package/docs/design/agent-hub-architecture.md +46 -0
- package/docs/design/bus.md +139 -1
- package/docs/design/task-board.md +82 -0
- package/docs/dogfood.md +22 -4
- package/docs/events.md +3 -0
- package/docs/memory.md +17 -1
- package/docs/operations.md +307 -78
- package/docs/profiles.md +35 -0
- package/docs/quickstart.md +18 -5
- package/docs/security.md +154 -60
- package/docs/specs/2026-09-19-agent-hub-design.md +84 -14
- package/docs/specs/2026-09-19-multi-project-design.md +7 -2
- package/docs/verification/2026-11-04-1171-channel-client-evidence.md +31 -0
- package/docs/verification/2026-11-05-0.31.0.md +182 -0
- package/docs/verification/2026-11-05-claude-implementer-2.1.318.md +70 -0
- package/docs/verification/2026-11-05-hardcoded-values-classification-rules.md +257 -0
- package/docs/verification/2026-11-05-hardcoded-values-ratchet.md +55 -0
- package/docs/verification/2026-11-06-rooms-presence-cost.md +152 -0
- package/docs/verification/claude-shell-wrapper-native.py +1 -1
- package/docs/verified.json +14 -14
- package/package.json +3 -4
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +4393 -3609
- package/src/adapters/acp.ts +5 -4
- package/src/adapters/channel-peer.ts +72 -3
- package/src/adapters/claude-channel-evidence.ts +52 -0
- package/src/adapters/claude-channel.ts +61 -18
- package/src/adapters/claude-implementer.ts +29 -7
- package/src/adapters/claude-print-child.ts +12 -2
- package/src/adapters/codex-appserver.ts +13 -12
- package/src/adapters/compat.ts +3 -3
- package/src/adapters/definitions/builtin-metadata.ts +13 -13
- package/src/adapters/definitions/qwen.ts +1 -0
- package/src/adapters/local-worker.ts +61 -18
- package/src/adapters/pi.ts +48 -6
- package/src/adapters/registry-types.ts +12 -0
- package/src/adapters/registry.ts +2 -0
- package/src/adapters/reviewer-print.ts +50 -11
- package/src/broker/server.ts +44 -13
- package/src/broker/store.ts +38 -8
- package/src/cli/answer-args.ts +50 -0
- package/src/cli/attention.ts +17 -2
- package/src/cli/backlog-return-args.ts +37 -0
- package/src/cli/console-question.ts +110 -0
- package/src/cli/console-state.ts +77 -16
- package/src/cli/console.ts +54 -1
- package/src/cli/handoff-args.ts +15 -5
- package/src/cli/handoff-output.ts +15 -2
- package/src/cli/help.ts +8 -5
- package/src/cli/identity.ts +13 -3
- package/src/cli/main.ts +128 -16
- package/src/cli/memory.ts +107 -3
- package/src/cli/output.ts +29 -6
- package/src/cli/pet.ts +4 -4
- package/src/cli/results-args.ts +30 -0
- package/src/cli/runtime-version-warning.ts +50 -0
- package/src/cli/status-lines.ts +41 -1
- package/src/cli/tail-render.ts +1 -0
- package/src/cli/upgrade-interactive.ts +14 -2
- package/src/hub/action-class.ts +9 -1
- package/src/hub/board.ts +369 -18
- package/src/hub/bus.ts +493 -53
- package/src/hub/completion-detail.ts +50 -0
- package/src/hub/completions-store.ts +168 -0
- package/src/hub/completions.ts +120 -0
- package/src/hub/config-apply.ts +7 -5
- package/src/hub/config-reload.ts +10 -1
- package/src/hub/control-client.ts +40 -4
- package/src/hub/daemon.ts +1569 -134
- package/src/hub/delivery-failure.ts +14 -0
- package/src/hub/delivery-hints.ts +14 -4
- package/src/hub/delivery-journal.ts +751 -42
- package/src/hub/delivery-superseded.ts +4 -1
- package/src/hub/envelope.ts +3 -3
- package/src/hub/events.ts +6 -0
- package/src/hub/handoff.ts +145 -6
- package/src/hub/hub-tools.ts +56 -7
- package/src/hub/manager.ts +59 -3
- package/src/hub/memory-policy.ts +44 -1
- package/src/hub/memory-sync-policy.ts +109 -0
- package/src/hub/memory-sync-state.ts +145 -0
- package/src/hub/no-writes.ts +1 -1
- package/src/hub/operational-settings.ts +11 -2
- package/src/hub/operational-view.ts +12 -1
- package/src/hub/peers.ts +25 -2
- package/src/hub/planning.ts +6 -1
- package/src/hub/question-desk.ts +201 -0
- package/src/hub/question-detach.ts +6 -0
- package/src/hub/questions.ts +260 -0
- package/src/hub/research-evidence.ts +106 -0
- package/src/hub/restart.ts +14 -0
- package/src/hub/review-anchor.ts +181 -0
- package/src/hub/review-brief.ts +13 -0
- package/src/hub/review-comments.ts +115 -0
- package/src/hub/review-findings.ts +7 -2
- package/src/hub/review-material.ts +76 -0
- package/src/hub/routing.ts +3 -1
- package/src/hub/task-sweep.ts +2 -0
- package/src/hub/tasks.ts +577 -63
- package/src/memory/capture.ts +5 -2
- package/src/pet/config.ts +25 -13
- package/src/rooms/link.ts +209 -21
- package/src/rooms/remote-peer.ts +186 -19
- package/src/rooms/service.ts +338 -22
- package/src/rooms/store.ts +437 -65
- package/src/rooms/wire.ts +24 -6
- package/src/ui/index.html +138 -7
- package/src/ui/needs.ts +10 -4
- package/src/ui/plugin-questions.ts +56 -0
- package/src/ui/plugin-results.ts +81 -0
- package/src/ui/plugin.ts +207 -27
- package/src/pet/window.html +0 -114
- package/src/pet/window.jxa.js +0 -122
- package/src/pet/window.ts +0 -154
package/src/cli/main.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { assertConductorOperationLocal, authorizeConductorOperation, authorizeCo
|
|
|
3
3
|
import { runtimePruneHealth } from "../hub/runtime-prune-health.ts";
|
|
4
4
|
import { conductorLocalOperation, conductorOperation, OPERATIONAL_PERSON_GUIDANCE } from "../hub/operational-authority.ts";
|
|
5
5
|
import { sayRecipients } from "./say-input.ts";
|
|
6
|
+
import { parseAnswerArgs } from "./answer-args.ts";
|
|
6
7
|
import type { DeliverableRow } from "../hub/tasks.ts";
|
|
7
8
|
import { kimiSourceRow } from "../hub/budget-notes.ts";
|
|
8
9
|
import { hubGitSync, hubGitEnv } from "../hub/git.ts";
|
|
@@ -30,6 +31,8 @@ import { renderIntake, renderIntakeChange } from "./intake-output.ts";
|
|
|
30
31
|
import { renderLearnChange, renderLearnList } from "./learn-output.ts";
|
|
31
32
|
import { renderCapabilities } from "./capabilities-output.ts";
|
|
32
33
|
import { parseStudyArgs } from "./study.ts";
|
|
34
|
+
import { parseBacklogReturn } from "./backlog-return-args.ts";
|
|
35
|
+
import { parseResultsArgs } from "./results-args.ts";
|
|
33
36
|
import { renderExperiments, renderStudyList, renderStudyReceipt, renderStudyShow } from "./study-output.ts";
|
|
34
37
|
import { renderHandoffReceipt, renderHandoffs } from "./handoff-output.ts";
|
|
35
38
|
import { parseHandoffArgs } from "./handoff-args.ts";
|
|
@@ -44,8 +47,8 @@ import { discoverClaudeHomes } from "./claude-homes.ts";
|
|
|
44
47
|
import { setupClaudeHomes } from "./setup-homes.ts";
|
|
45
48
|
import { registerAconPlugin } from "./acon-register.ts";
|
|
46
49
|
import { petView, readPet, renderPet } from "./pet.ts";
|
|
47
|
-
import {
|
|
48
|
-
import { loadPetConfig
|
|
50
|
+
import { PET_WINDOW_RETIRED_NOTICE, petPalette } from "../pet/config.ts";
|
|
51
|
+
import { loadPetConfig } from "./pet-config.ts";
|
|
49
52
|
import { projectProfile, resolveProfile } from "../hub/profiles.ts";
|
|
50
53
|
import { catalogAssignments, loadRoleCatalog, selectedRoles } from "../hub/roles.ts";
|
|
51
54
|
import { attachWatch } from "./watch.ts";
|
|
@@ -88,7 +91,7 @@ import { childEnv, personEnv } from "../hub/child-process.ts";
|
|
|
88
91
|
import { abortRecovery, createOperation, disposeRecovery, recordRecoveryDelegation, repairedFreshSession, reattachRecovery, liveProjects, nextChoices, nextActionsText, publicOperation, recoveryCommand, registeredProjects, runRecovery, type RecoveryOperation, type Inspection } from "./upgrade.ts";
|
|
89
92
|
import { endPlannedPeer, makeRecoveryDriver, makeUpgradePlan, preserveSource } from "./upgrade-runtime.ts";
|
|
90
93
|
import { exactVersion, latestRelease, newerVersion } from "./recovery-package.ts";
|
|
91
|
-
import { runtimeVersionWarnings } from "./runtime-version-warning.ts";
|
|
94
|
+
import { globalUpdateAdvice, runtimeVersionWarnings } from "./runtime-version-warning.ts";
|
|
92
95
|
import { operationScreen, planScreen, type ScreenIO, type UpgradeHost } from "./upgrade-interactive.ts";
|
|
93
96
|
import { readRecoveryPresentationFlags } from "./upgrade-presentation-flags.ts";
|
|
94
97
|
import { renderOperation, renderBlocked, renderSummary, type RecoveryPresentationOptions } from "./upgrade-presentation.ts";
|
|
@@ -717,6 +720,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
717
720
|
runner: recoveryRunner,
|
|
718
721
|
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect),
|
|
719
722
|
endInspection: recoveryEndInspection,
|
|
723
|
+
afterCompleted: (op) => globalUpdateLines(op.plan.version),
|
|
720
724
|
endPeer: (planned, peer) => endPlannedPeer(planned, peer),
|
|
721
725
|
runnerStoppable: (id) => signedRunner(id) !== undefined,
|
|
722
726
|
stopRunner: (id) => stopSignedRunner(id),
|
|
@@ -725,6 +729,16 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
725
729
|
};
|
|
726
730
|
}
|
|
727
731
|
|
|
732
|
+
/**
|
|
733
|
+
* #718 (#1179): the step for the global CLI and the shared Claude plugin, when either is older than the daemon `target` installs. The plugin version comes from the same `claude plugin list`
|
|
734
|
+
* probe `ahub doctor` reads (10 s, no shell); a missing `claude`, a failure or an unreadable list is an unknown plugin, which is never a claim. The CLI is this process, or, when an older CLI handed the
|
|
735
|
+
* upgrade to this release's own coordinator (`AGENTHUB_UPGRADE_HANDOVER=1`), the older one that started it. Advice only: nothing here can fail an upgrade.
|
|
736
|
+
*/
|
|
737
|
+
function globalUpdateLines(target: string): string[] {
|
|
738
|
+
return globalUpdateAdvice({ target, cli: VERSION, handedOver: process.env.AGENTHUB_UPGRADE_HANDOVER === "1", packageRoot: join(import.meta.dir, "..", ".."),
|
|
739
|
+
probe: () => spawnSync(DOCTOR_PLUGIN_PROBE.command, [...DOCTOR_PLUGIN_PROBE.argv], { encoding: "utf8", timeout: 10_000 }) });
|
|
740
|
+
}
|
|
741
|
+
|
|
728
742
|
async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
729
743
|
const display = recoveryDisplay();
|
|
730
744
|
const { one, rest } = takeFlags(display.args, ["--to"], []);
|
|
@@ -799,7 +813,11 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
799
813
|
writeOperation(id, operation);
|
|
800
814
|
} else operation = createOperation(plan, sourceRoot, undefined, delegatedPeer ? { peer: delegatedPeer, command: kind } : undefined);
|
|
801
815
|
if (display.json) { spawnRecovery(operation, true); console.log(JSON.stringify(publicOperation(operation), null, 2)); }
|
|
802
|
-
else
|
|
816
|
+
else {
|
|
817
|
+
spawnRecovery(operation, false, humanRecovery(false) ? display.options : undefined);
|
|
818
|
+
// #718: the upgrade is only scheduled here, but the step is already known: the CLI and the plugin are compared with the daemon this upgrade installs. Never in --json, which stays the raw document.
|
|
819
|
+
if (kind === "upgrade") printRecovery(globalUpdateLines(plan.version));
|
|
820
|
+
}
|
|
803
821
|
}
|
|
804
822
|
|
|
805
823
|
/** Strict person command: recorded identity only, no session or terminal overrides. */
|
|
@@ -1162,8 +1180,6 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1162
1180
|
const status = await startProject(registeredProject(), { unattended, liveTest, ...(recoveryLauncherCurrent ? { current: recoveryLauncherCurrent, env: { ...personEnv(process.env), AGENTHUB_RECOVERY_OPERATION: continuationId, AGENTHUB_PEER_ID: continuationPeer, AGENTHUB_RECOVERY_CONDUCTOR: continuationPeer, AGENTHUB_RECOVERY_CONTINUATION: continuationId } } : {}) });
|
|
1163
1181
|
console.log(`ahub up (ws://127.0.0.1:${status.controlPort}), state in ${stateDir}`);
|
|
1164
1182
|
if (recoveryLauncherCurrent) { recoveryLauncherCurrent(); return; }
|
|
1165
|
-
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
1166
|
-
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
1167
1183
|
// #608: the default agent set, only for a person's own `ahub up` (never an agent's, a live test's, or an upgrade or recovery relaunch, which set AGENTHUB_RECOVERY_OPERATION).
|
|
1168
1184
|
if (!args.includes("--no-agents") && !liveTest && caller.role === "console" && identity.role === "console" && !process.env.AGENTHUB_RECOVERY_OPERATION) {
|
|
1169
1185
|
try {
|
|
@@ -1410,7 +1426,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1410
1426
|
}
|
|
1411
1427
|
if (sub === "groom" && operands.length === 1) return shown(renderGroom)(taskOp("hub_planner_groom", {}));
|
|
1412
1428
|
if (["return", "return-untouched"].includes(sub ?? "") && /^[1-9]\d*$/.test(id ?? "") && /^\d+$/.test(revision ?? "") && operands.length >= 4) {
|
|
1413
|
-
|
|
1429
|
+
// #1177: `--reserved <peer>` / `--unreserve` edit the reservation atomically with the return (fields.reserved); with neither, the arguments are what they always were.
|
|
1430
|
+
const returned = parseBacklogReturn(operands.slice(3));
|
|
1431
|
+
if (!returned.ok) return fail(returned.error);
|
|
1432
|
+
return shown((data, columns, now) => renderBacklogResult("return-untouched", data, columns, now))(taskOp("hub_backlog_return_untouched", { id: Number(id), revision: Number(revision), reason: returned.value.reason, ...(returned.value.fields ? { fields: returned.value.fields } : {}) }));
|
|
1414
1433
|
}
|
|
1415
1434
|
if (sub === "add" && args.length >= 3) return console.log(await taskOp("hub_task_propose", { class: id, title: args.slice(2).join(" "), backlog: true }));
|
|
1416
1435
|
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>");
|
|
@@ -1422,12 +1441,12 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1422
1441
|
return shown((data, columns, now) => renderBacklogResult("split", data, columns, now))(taskOp("hub_backlog_split", { id: Number(id), revision: Number(revision), parts, reason: reason.join(" ") }));
|
|
1423
1442
|
}
|
|
1424
1443
|
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) }));
|
|
1425
|
-
if (!["plan", "pin"].includes(sub ?? "") || !value || !reason.length) fail("planning needs fields and a reason");
|
|
1444
|
+
if (!["plan", "pin", "person"].includes(sub ?? "") || !value || !reason.length) fail("planning needs fields and a reason");
|
|
1426
1445
|
let fields: unknown;
|
|
1427
|
-
try { fields = sub === "pin" ? { pinned: value === "true" ? true : value === "false" ? false : undefined } : JSON.parse(value); }
|
|
1446
|
+
try { fields = sub === "pin" ? { pinned: value === "true" ? true : value === "false" ? false : undefined } : sub === "person" ? { needsPerson: value === "true" ? true : value === "false" ? false : undefined } : JSON.parse(value); }
|
|
1428
1447
|
catch { return fail("planning fields must be JSON"); }
|
|
1429
|
-
if (sub === "pin" && value !== "true" && value !== "false") fail(
|
|
1430
|
-
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(" ") }));
|
|
1448
|
+
if ((sub === "pin" || sub === "person") && value !== "true" && value !== "false") fail(`${sub} must be true or false`);
|
|
1449
|
+
return shown((data, columns, now) => renderBacklogResult(sub === "person" ? "plan" : sub as "plan" | "pin", data, columns, now))(taskOp("hub_backlog_plan", { id: Number(id), revision: Number(revision), fields, reason: reason.join(" ") }));
|
|
1431
1450
|
},
|
|
1432
1451
|
|
|
1433
1452
|
release: async () => {
|
|
@@ -1927,9 +1946,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1927
1946
|
if (sub === "handoff") {
|
|
1928
1947
|
// #604: work that belongs to ANOTHER project that runs agent-hub goes onto that project's board. The person's form (a console command: the classifier gives an agent `console` for it, and an
|
|
1929
1948
|
// agent uses hub_task_handoff); the hub resolves the target, asks its daemon and records the answer. Nothing is queued: a target that is not running is named with the action.
|
|
1930
|
-
const present = outputOptions(splitPresentation(rest, { valueFlags: ["--project", "--path", "--issue", "--task", "--detail"], edges: true }).flags);
|
|
1931
|
-
const parsed = parseHandoffArgs(splitPresentation(rest, { valueFlags: ["--project", "--path", "--issue", "--task", "--detail"], edges: true }).operands);
|
|
1949
|
+
const present = outputOptions(splitPresentation(rest, { valueFlags: ["--project", "--path", "--issue", "--task", "--detail", "--duplicate-ok", "--key"], edges: true }).flags);
|
|
1950
|
+
const parsed = parseHandoffArgs(splitPresentation(rest, { valueFlags: ["--project", "--path", "--issue", "--task", "--detail", "--duplicate-ok", "--key"], edges: true }).operands);
|
|
1932
1951
|
if ("usage" in parsed) return fail(parsed.usage);
|
|
1952
|
+
// #734: the person's list (age and reachability of each target) and sync (ask the targets now); both read the same rows `ahub board --handoffs` shows.
|
|
1953
|
+
if ("view" in parsed) return showToolText(data => renderHandoffs(data, present.columns), await taskOp(parsed.view === "sync" ? "hub_task_handoff_sync" : "hub_task_handoffs", {}), present);
|
|
1933
1954
|
const text = await taskOp("hub_task_handoff", { ...parsed });
|
|
1934
1955
|
return showToolText(data => renderHandoffReceipt(data, present.columns), text, present);
|
|
1935
1956
|
}
|
|
@@ -2005,6 +2026,35 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2005
2026
|
} finally { hub.close(); }
|
|
2006
2027
|
},
|
|
2007
2028
|
|
|
2029
|
+
peer: async () => {
|
|
2030
|
+
const options = outputOptions();
|
|
2031
|
+
const { operands, flags } = splitPresentation(args);
|
|
2032
|
+
const usage = "usage: ahub peer takeover <peer> [standby-id] [--json] [--color=auto|always|never]";
|
|
2033
|
+
const [sub, peer, standbyId, ...extra] = operands;
|
|
2034
|
+
const uuid = (value: string | undefined): value is string => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
2035
|
+
if (sub !== "takeover" || !peer || !/^[a-z][a-z0-9-]{0,31}$/.test(peer) || extra.length || standbyId !== undefined && !uuid(standbyId)
|
|
2036
|
+
|| flags.some(flag => flag !== "--json" && !flag.startsWith("--color="))) fail(usage);
|
|
2037
|
+
const hub = await connect();
|
|
2038
|
+
try {
|
|
2039
|
+
if (!hub.features.includes("peer_takeover")) fail("running hub lacks peer_takeover; upgrade and restart the running hub first");
|
|
2040
|
+
let reply: any;
|
|
2041
|
+
try { reply = await hub.request({ t: "peer_takeover", peer, ...(standbyId ? { standbyId } : {}) }, 5000); }
|
|
2042
|
+
catch (error) {
|
|
2043
|
+
const message = String((error as Error).message ?? "");
|
|
2044
|
+
if (/unknown (?:control )?(?:message|request|command)(?:\b|:)|this hub does not know \"peer_takeover\"/i.test(message)) fail(`${message}; upgrade and restart the running hub to use ahub peer takeover`);
|
|
2045
|
+
fail(`${message || "peer takeover result is unconfirmed"}; inspect ahub status before retrying; takeover may already have started`);
|
|
2046
|
+
}
|
|
2047
|
+
const transportError = String(reply.error ?? "");
|
|
2048
|
+
if (reply.ok === false && (transportError === "hub connection closed" || transportError === "hub request could not be sent" || /^no answer from the hub within \d+ s$/.test(transportError))) {
|
|
2049
|
+
fail(`${transportError}; inspect ahub status before retrying; takeover may already have started`);
|
|
2050
|
+
}
|
|
2051
|
+
if (reply.ok !== true) fail(String(reply.error ?? "peer takeover was refused"), reply.refusalAudited === true, reply.authorityRefused);
|
|
2052
|
+
if (reply.outcome !== "requested" || !uuid(reply.standbyId) || standbyId !== undefined && reply.standbyId !== standbyId) fail("peer takeover result was not confirmed; refresh ahub status");
|
|
2053
|
+
if (options.json) console.log(JSON.stringify(reply, null, 2));
|
|
2054
|
+
else console.log(`${peer}: old session detached; standby attachment requested`);
|
|
2055
|
+
} finally { hub.close(); }
|
|
2056
|
+
},
|
|
2057
|
+
|
|
2008
2058
|
title: async () => {
|
|
2009
2059
|
const [peer, ...words] = args;
|
|
2010
2060
|
if (!peer || !words.length) fail("usage: ahub title <peer> <text...>");
|
|
@@ -2274,15 +2324,77 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2274
2324
|
hub.close();
|
|
2275
2325
|
},
|
|
2276
2326
|
|
|
2327
|
+
// #1053 T4: the person's answer to a question a peer asked with hub_ask. No operands lists the open questions; `<set> <question>=<option>[,<option>] [--other <text>]` answers one.
|
|
2328
|
+
// The hub validates every answer against the stored row and names a refusal by position, never by the offered text. Console role only (classifyPeerCommand's default).
|
|
2329
|
+
answer: async () => {
|
|
2330
|
+
const options = outputOptions();
|
|
2331
|
+
const operands = args.filter(arg => arg !== "--json" && !arg.startsWith("--color=") && arg !== "--full");
|
|
2332
|
+
const hub = await connect();
|
|
2333
|
+
try {
|
|
2334
|
+
if (!operands.length) {
|
|
2335
|
+
const reply = await hub.request({ t: "questions" });
|
|
2336
|
+
if (reply.ok === false) return fail(String(reply.error ?? "questions are unavailable"));
|
|
2337
|
+
const rows = Array.isArray(reply.questions) ? reply.questions : [];
|
|
2338
|
+
if (options.json) return console.log(JSON.stringify(rows, null, 2));
|
|
2339
|
+
if (!rows.length) return console.log("no open questions");
|
|
2340
|
+
for (const row of rows) {
|
|
2341
|
+
console.log(`${row.set} ${row.peer} asks${row.private ? " (private)" : ""}`);
|
|
2342
|
+
for (const question of row.questions) {
|
|
2343
|
+
console.log(` ${question.id}: ${terminalText(String(question.prompt))}${question.multi ? " (choose any)" : ""}${question.allowOther ? " (or --other <text>)" : ""}`);
|
|
2344
|
+
for (const option of question.options) console.log(` ${option.id} ${terminalText(String(option.label))}${option.description ? ` - ${terminalText(String(option.description))}` : ""}`);
|
|
2345
|
+
}
|
|
2346
|
+
console.log(` answer with: ahub answer ${row.set} ${row.questions.map((question: { id: string; options: { id: string }[] }) => `${question.id}=${question.options[0]?.id ?? "<option>"}`).join(" ")}`);
|
|
2347
|
+
}
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
const parsed = parseAnswerArgs(operands);
|
|
2351
|
+
if (!parsed.ok) return fail(parsed.error);
|
|
2352
|
+
const reply = await hub.request({ t: "answer", surface: "terminal", questionSet: parsed.value.set, answers: parsed.value.answers });
|
|
2353
|
+
if (reply.ok === false) return fail(String(reply.error ?? "the answer was refused"));
|
|
2354
|
+
if (options.json) return console.log(JSON.stringify({ ok: true, questionSet: reply.questionSet, state: reply.state }));
|
|
2355
|
+
console.log(`answered ${reply.questionSet ?? parsed.value.set}`);
|
|
2356
|
+
} finally { hub.close(); }
|
|
2357
|
+
},
|
|
2358
|
+
|
|
2359
|
+
// #1054 T4: the results a run a person started left behind. A person's command (an agent shell is refused before the hub); reads change nothing, and only an explicit flag writes a receipt.
|
|
2360
|
+
results: async () => {
|
|
2361
|
+
const options = outputOptions();
|
|
2362
|
+
const parsed = parseResultsArgs(args.filter(arg => arg !== "--json" && !arg.startsWith("--color=") && arg !== "--full"));
|
|
2363
|
+
if (!parsed.ok) return fail(parsed.error);
|
|
2364
|
+
const request = parsed.value;
|
|
2365
|
+
const hub = await connect();
|
|
2366
|
+
try {
|
|
2367
|
+
if (request.action === "read") {
|
|
2368
|
+
const reply = await hub.request({ t: "result_seen", id: request.id, surface: "terminal" });
|
|
2369
|
+
if (reply.ok === false) return fail(String(reply.error ?? "the result could not be marked read"));
|
|
2370
|
+
return options.json ? console.log(JSON.stringify({ ok: true, id: request.id, read: true })) : console.log(`marked ${request.id} read`);
|
|
2371
|
+
}
|
|
2372
|
+
if (request.action === "read-all" || request.action === "unread-last") {
|
|
2373
|
+
const reply = await hub.request({ t: "results", action: request.action, surface: "terminal" });
|
|
2374
|
+
if (reply.ok === false) return fail(String(reply.error ?? "the receipt could not be written"));
|
|
2375
|
+
if (options.json) return console.log(JSON.stringify(request.action === "read-all" ? { ok: true, marked: reply.marked } : { ok: true, unread: reply.unread }));
|
|
2376
|
+
return console.log(request.action === "read-all" ? `marked ${reply.marked} ${reply.marked === 1 ? "result" : "results"} read` : `marked ${reply.unread} unread again`);
|
|
2377
|
+
}
|
|
2378
|
+
const reply = await hub.request({ t: "results", unread: request.unread });
|
|
2379
|
+
if (reply.ok === false) return fail(String(reply.error ?? "results are unavailable"));
|
|
2380
|
+
const rows = Array.isArray(reply.results) ? reply.results : [];
|
|
2381
|
+
if (options.json) return console.log(JSON.stringify(rows, null, 2));
|
|
2382
|
+
if (!rows.length) return console.log(request.unread ? "no unread results" : "no results");
|
|
2383
|
+
for (const row of rows) console.log(`${row.read ? " " : "*"} ${row.id} ${terminalText(String(row.title))} ${new Date(Number(row.settledAt)).toISOString().replace("T", " ").slice(0, 16)} ${Math.round(Number(row.ms) / 1000)}s${row.read ? "" : ` ahub results --read ${row.id}`}`);
|
|
2384
|
+
// The listing holds the newest 100. Unread results are never pruned, so when more exist than shown, say how many and how to reach them (status still counts every one).
|
|
2385
|
+
if (Number(reply.more) > 0) console.log(`... and ${Number(reply.more)} older unread; ahub results --read-all marks them all, or read the newest ones first`);
|
|
2386
|
+
} finally { hub.close(); }
|
|
2387
|
+
},
|
|
2388
|
+
|
|
2277
2389
|
pet: async () => {
|
|
2278
2390
|
if (args.includes("--window")) {
|
|
2279
2391
|
if (args.some(arg => !["--window", "--close"].includes(arg))) return fail("usage: ahub pet --window [--close]");
|
|
2280
|
-
return console.log(
|
|
2392
|
+
return console.log(PET_WINDOW_RETIRED_NOTICE);
|
|
2281
2393
|
}
|
|
2282
2394
|
if (args.includes("--palette")) {
|
|
2283
2395
|
if (args.length !== 2 || args[0] !== "--palette") return fail("usage: ahub pet [--short] | --window [--close] | --palette <blue|teal|violet|ochre>");
|
|
2284
|
-
|
|
2285
|
-
|
|
2396
|
+
if (!petPalette(args[1])) return fail("Unknown pet palette; use blue, teal, violet or ochre (a/b/c/d).");
|
|
2397
|
+
return console.log(PET_WINDOW_RETIRED_NOTICE);
|
|
2286
2398
|
}
|
|
2287
2399
|
if (args.some(arg => arg !== "--short")) return fail("usage: ahub pet [--short] | --window [--close] | --palette <blue|teal|violet|ochre>");
|
|
2288
2400
|
const short = args.includes("--short");
|
package/src/cli/memory.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { memoryAnchorTaskIdentity, memoryStaleAnchors, unavailableMemoryStaleAnc
|
|
|
14
14
|
import { MEMORY_SYMBOL_START_BUDGET_MS } from "../hub/memory-symbols.ts";
|
|
15
15
|
import { detectCliIdentity } from "./identity.ts";
|
|
16
16
|
import { createStalenessProbe } from "../hub/memory-staleness.ts";
|
|
17
|
-
import { looksSecret, memoryProjectKey, pathCandidates, readProfileJSON, resolveShardProfile, type MemoryStore } from "@staix/agent-mem";
|
|
17
|
+
import { looksSecret, memoryHome, memoryProjectKey, pathCandidates, readProfileJSON, resolveShardProfile, type MemoryStore } from "@staix/agent-mem";
|
|
18
18
|
import type { SyncOptions } from "@staix/agent-mem";
|
|
19
19
|
import { memoryUsage, goalUsage, rawMemoryCommand, rawGoalCommand } from "@staix/agent-mem";
|
|
20
20
|
|
|
@@ -24,9 +24,10 @@ import { mentionsPrivatePath, promotionScreen, withoutPii } from "../hub/memory-
|
|
|
24
24
|
import { currentRouting, detectSignals } from "../hub/routing.ts";
|
|
25
25
|
import { loadConfig } from "../hub/daemon.ts";
|
|
26
26
|
import { bindProfileMemory, memoryProjectFor, memoryProjectForStore, openProfileMemory, MemoryProfileBinding, MEMORY_PROFILE_UNAVAILABLE, MEMORY_SPLIT_INCOMPLETE, type MemoryProfileReceipt, type ProfileMemoryOptions } from "../hub/memory-profile.ts";
|
|
27
|
+
import { createMemorySyncPolicy, inspectDeclinedMemoryRows, projectMemorySyncText } from "../hub/memory-sync-policy.ts";
|
|
27
28
|
|
|
28
29
|
export const GOAL_USAGE = goalUsage(AHUB_NAMES.goal);
|
|
29
|
-
export const MEMORY_USAGE = `${memoryUsage(AHUB_NAMES.memory)} | decay [--days N] [--json] | profile-check [--json]`;
|
|
30
|
+
export const MEMORY_USAGE = `${memoryUsage(AHUB_NAMES.memory)} | decay [--days N] [--json] | declined [--json] | profile-check [--json]`;
|
|
30
31
|
|
|
31
32
|
const fail = (message: string): never => { throw new Error(message); };
|
|
32
33
|
/** The project's config, or none when there is no valid one (reading memory needs none). */
|
|
@@ -36,6 +37,75 @@ const loadConfigSafe = (cwd: string) => { try { return loadConfig(cwd); } catch
|
|
|
36
37
|
/** The project key: an injected store resolves the alias in its own home, the default open in the ambient one (#240). */
|
|
37
38
|
const projectOf = (cwd: string, provided: MemoryStore | undefined): string => provided ? memoryProjectForStore(provided, cwd) : memoryProjectFor(cwd);
|
|
38
39
|
|
|
40
|
+
/** Bounded, read-only lookup of current task paths that the daemon's promotion screen also considers. */
|
|
41
|
+
function memoryTaskPaths(cwd: string, boundProject: string, rowProject: string, key: string): string[] | undefined {
|
|
42
|
+
if (rowProject !== boundProject) return [];
|
|
43
|
+
const identity = memoryAnchorTaskIdentity(key, boundProject);
|
|
44
|
+
if (!identity) return [];
|
|
45
|
+
let db: Database | undefined;
|
|
46
|
+
try {
|
|
47
|
+
const file = join(stateDirFor(cwd), "hub.db");
|
|
48
|
+
let stat;
|
|
49
|
+
try { stat = lstatSync(file); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; throw error; }
|
|
50
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
51
|
+
db = new Database(file, { readonly: true, create: false });
|
|
52
|
+
db.run(`PRAGMA busy_timeout = ${HUB_DB_BUSY_TIMEOUT_MS}`);
|
|
53
|
+
const row = db.query(`SELECT id,created,length(refs) AS refsLength,length(plan) AS planLength,
|
|
54
|
+
CASE WHEN length(refs)<=65536 THEN refs END AS refs,CASE WHEN length(plan)<=65536 THEN plan END AS plan
|
|
55
|
+
FROM tasks WHERE id=? AND created=?`).get(identity.id, identity.created) as { id: number; created: number; refsLength: number; planLength: number; refs: string | null; plan: string | null } | null;
|
|
56
|
+
if (!row || row.id !== identity.id || row.created !== identity.created) return [];
|
|
57
|
+
if (row.refsLength > 65536 || row.planLength > 65536 || typeof row.refs !== "string" || typeof row.plan !== "string") return undefined;
|
|
58
|
+
const refs: unknown = JSON.parse(row.refs), plan: unknown = JSON.parse(row.plan);
|
|
59
|
+
if (!refs || typeof refs !== "object" || Array.isArray(refs) || !plan || typeof plan !== "object" || Array.isArray(plan)) return undefined;
|
|
60
|
+
const refPaths = (refs as TaskRefs).paths ?? [], planPaths = (plan as TaskPlan).paths ?? [];
|
|
61
|
+
if (!Array.isArray(refPaths) || !refPaths.every(path => typeof path === "string") || !Array.isArray(planPaths) || !planPaths.every(path => typeof path === "string")) return undefined;
|
|
62
|
+
return [...refPaths, ...planPaths];
|
|
63
|
+
} catch { return undefined; }
|
|
64
|
+
finally { db?.close(); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hostSyncPolicyInputs(cwd: string, home: string, boundProject: string, profile?: MemoryProfileReceipt): { version: string; denyPaths: string[]; piiPaths: string[]; piiPatterns: string[]; routing: ReturnType<typeof currentRouting> } {
|
|
68
|
+
const config = loadConfig(cwd), routing = currentRouting(cwd);
|
|
69
|
+
const denyPaths = [...config.local.deny], piiPaths = [...(config.pii?.paths ?? [])], piiPatterns = [...routing.signals.pii_patterns];
|
|
70
|
+
return { version: JSON.stringify({ home, boundProject, profile: profile ? { profile: profile.profile, generation: profile.generation, selectorRevision: profile.selectorRevision } : null, denyPaths, piiPaths, piiPatterns }), denyPaths, piiPaths, piiPatterns, routing };
|
|
71
|
+
}
|
|
72
|
+
function createHubMemorySyncPolicy(cwd: string, home: string, boundProject: string, profile?: MemoryProfileReceipt) {
|
|
73
|
+
const inputs = hostSyncPolicyInputs(cwd, home, boundProject, profile);
|
|
74
|
+
const policy = createMemorySyncPolicy({
|
|
75
|
+
home, cwd, denyPaths: inputs.denyPaths, piiPatterns: inputs.piiPatterns,
|
|
76
|
+
hostScreen: row => {
|
|
77
|
+
const task = typeof row.task === "string" ? row.task : "";
|
|
78
|
+
const project = typeof row.project === "string" ? row.project : "";
|
|
79
|
+
const about = Array.isArray(row.about) ? row.about.filter((node): node is string => typeof node === "string") : [];
|
|
80
|
+
const taskPaths = memoryTaskPaths(cwd, boundProject, project, task);
|
|
81
|
+
if (task && taskPaths === undefined) return { ok: false, rule: "hub privacy policy unavailable" };
|
|
82
|
+
const enrichedAbout = [...about, ...(taskPaths ?? []).map(path => `path:${path}`)];
|
|
83
|
+
const visible = withoutPii([{
|
|
84
|
+
title: typeof row.title === "string" ? row.title : null,
|
|
85
|
+
text: JSON.stringify(row), about: enrichedAbout,
|
|
86
|
+
...(task ? { task } : {}), ...(project ? { project } : {}),
|
|
87
|
+
}], inputs.routing, cwd, inputs.piiPaths).length > 0;
|
|
88
|
+
return visible ? { ok: true } : { ok: false, rule: "hub privacy policy: PII pattern or private path" };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return { policy, version: inputs.version };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Preserve any trusted caller's stricter sync screen and bind its epoch into the host policy stamp. */
|
|
95
|
+
export function composeMemorySyncOptions(host: Pick<SyncOptions, "screen" | "policyVersion">, caller?: Omit<SyncOptions, "remote">, canonical = false): Omit<SyncOptions, "remote"> {
|
|
96
|
+
return {
|
|
97
|
+
...caller,
|
|
98
|
+
screen: row => {
|
|
99
|
+
try {
|
|
100
|
+
const content = projectMemorySyncText(row, canonical);
|
|
101
|
+
return host.screen?.(content) === true && (caller?.screen ? caller.screen(content) === true : true);
|
|
102
|
+
}
|
|
103
|
+
catch { return false; }
|
|
104
|
+
},
|
|
105
|
+
policyVersion: () => JSON.stringify([host.policyVersion?.() ?? null, caller?.policyVersion?.() ?? null]),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
39
109
|
/** Bind each command at entry; the CLI closes default opens and injected stores remain caller-owned. */
|
|
40
110
|
function commandStores(provided: MemoryStore | undefined, options: ProfileMemoryOptions, cwd: string) {
|
|
41
111
|
try { return { ...(provided ? bindProfileMemory(provided, undefined, memoryProjectKey(cwd)) : openProfileMemory(options)), owned: !provided }; }
|
|
@@ -86,13 +156,47 @@ export async function memoryCommand(args: string[], cwd: string, opts: { store?:
|
|
|
86
156
|
return profileOutput(text, false, profile, person);
|
|
87
157
|
} finally { if (context.owned) store.close(); }
|
|
88
158
|
}
|
|
159
|
+
if (rest[0] === "declined") {
|
|
160
|
+
if (!person) fail("memory declined requires person authority");
|
|
161
|
+
if (rest.length !== 1) fail("usage: ahub memory declined [--json]");
|
|
162
|
+
const project = projectOf(cwd, opts.store);
|
|
163
|
+
const context = commandStores(opts.store, { project, deferPromotions: true, now: opts.now }, cwd);
|
|
164
|
+
try {
|
|
165
|
+
context.binding.assertCurrent();
|
|
166
|
+
const boundPolicy = createHubMemorySyncPolicy(cwd, context.binding.home, project, context.binding.status(context.store));
|
|
167
|
+
const result = inspectDeclinedMemoryRows(context.store, { screen: (row, canonical) => {
|
|
168
|
+
const content = projectMemorySyncText(row, canonical);
|
|
169
|
+
const host = boundPolicy.policy.screen(content, true);
|
|
170
|
+
if (!host.ok) return host;
|
|
171
|
+
try { return opts.sync?.screen ? (opts.sync.screen(content) === true ? { ok: true } : { ok: false, rule: "caller sync policy" }) : { ok: true }; }
|
|
172
|
+
catch { return { ok: false, rule: "caller sync policy" }; }
|
|
173
|
+
} }, () => JSON.stringify([boundPolicy.version, opts.sync?.policyVersion?.() ?? null]));
|
|
174
|
+
context.binding.assertCurrent();
|
|
175
|
+
const profile = context.binding.status(context.store);
|
|
176
|
+
if (hostSyncPolicyInputs(cwd, context.binding.home, project, profile).version !== boundPolicy.version)
|
|
177
|
+
fail("memory declined policy changed during inspection; retry with stable privacy policy");
|
|
178
|
+
if (args.includes("--json")) return JSON.stringify({ rows: result.rows, complete: result.complete, ...(result.reason ? { reason: result.reason } : {}), memoryProfile: profile });
|
|
179
|
+
const text = result.complete
|
|
180
|
+
? result.rows.length
|
|
181
|
+
? [`${result.rows.length} declined physical outbound row${result.rows.length === 1 ? "" : "s"}:`, ...result.rows.map(row => `mem:${row.id} ${row.rule}`), "Review the source or approved policy, then run `ahub memory sync`. `forget` does not remove append-only physical rows."].join("\n")
|
|
182
|
+
: "no declined physical outbound rows; inspection complete"
|
|
183
|
+
: [`declined-row inspection incomplete: ${result.reason ?? "physical memory rows could not be inspected"}`, ...(result.rows.length ? [`${result.rows.length} declined row${result.rows.length === 1 ? "" : "s"} found before inspection stopped:`, ...result.rows.map(row => `mem:${row.id} ${row.rule}`)] : []), "No complete result is available; resolve the inspection issue before treating the list as empty."].join("\n");
|
|
184
|
+
return text;
|
|
185
|
+
} finally { if (context.owned) context.store.close(); }
|
|
186
|
+
}
|
|
89
187
|
let showStale = false;
|
|
90
188
|
const project = projectOf(cwd, opts.store);
|
|
189
|
+
let policyHome = opts.store?.profile.home ?? memoryHome();
|
|
91
190
|
const context = commandStores(opts.store, { project, ttlMs: (loadConfigSafe(cwd)?.memory.short_ttl_days ?? 14) * 24 * 3600_000,
|
|
92
|
-
screen: promotionScreen(project, cwd, () => currentRouting(cwd), privatePaths) }, cwd);
|
|
191
|
+
screen: promotionScreen(project, cwd, () => currentRouting(cwd), privatePaths, task => memoryTaskPaths(cwd, project, project, task) ?? [], () => createHubMemorySyncPolicy(cwd, policyHome, project).policy) }, cwd);
|
|
192
|
+
policyHome = context.binding.home;
|
|
93
193
|
try {
|
|
94
194
|
context.binding.assertCurrent();
|
|
195
|
+
let boundSyncPolicy: ReturnType<typeof createHubMemorySyncPolicy> | undefined;
|
|
196
|
+
const syncPolicy = () => boundSyncPolicy ??= createHubMemorySyncPolicy(cwd, context.binding.home, project, context.binding.status(context.store));
|
|
95
197
|
const text = await rawMemoryCommand(args, cwd, { ...opts, names: AHUB_NAMES, project, store: context.store, open: () => context.store,
|
|
198
|
+
sync: composeMemorySyncOptions({ screen: row => syncPolicy().policy.screen(row, true).ok,
|
|
199
|
+
policyVersion: () => { context.binding.assertCurrent(); return hostSyncPolicyInputs(cwd, context.binding.home, project, context.binding.status(context.store)).version; } }, opts.sync, true),
|
|
96
200
|
readable: notes => {
|
|
97
201
|
const paths = privatePaths(), visible = withoutPii(notes, currentRouting(cwd), cwd, paths);
|
|
98
202
|
if (args[0] !== "show" || args.includes("--json")) return visible;
|
package/src/cli/output.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { boardColumns, type BoardPeerInput } from "../ui/task-board.ts";
|
|
|
5
5
|
import { peerStyle } from "../ui/peer-palette.ts";
|
|
6
6
|
import { cells, TABLES, table, fitLine, wrap, duration, relative, plural, terminalText, initialConsoleState, stateTone, type Span, type Tone } from "./console-state.ts";
|
|
7
7
|
import { taskProgress } from "../ui/task-progress.ts";
|
|
8
|
-
import { backendLabel } from "./status-lines.ts";
|
|
8
|
+
import { backendLabel, memorySyncLine, reviewInputHealthLine } from "./status-lines.ts";
|
|
9
9
|
import { ownerIsolationText } from "./role-holders.ts";
|
|
10
10
|
import { schedulePeriodText, scheduleWhen } from "../hub/schedules.ts";
|
|
11
11
|
import { formatReport, formatTaskReport, type Report, type TaskReport } from "../hub/report.ts";
|
|
@@ -17,6 +17,8 @@ export interface StatusOutput {
|
|
|
17
17
|
controlWriteTiming?: { active: number; slowRequests: number; blockedLoopSamples: number; droppedActive: number; droppedRecent: number; droppedLogs: number; droppedSpans?: number; droppedMetadata?: number; expiredRequests?: number };
|
|
18
18
|
specs?: SpecsStatus & { missingBlockHint?: boolean };
|
|
19
19
|
queueBackpressure?: QueueBackpressureStatus;
|
|
20
|
+
/** Unresolved failed delivery counts, absent when an older hub or storage cannot answer. */
|
|
21
|
+
deliveryFailures?: Record<string, number>;
|
|
20
22
|
pendingRestart?: readonly { key: string; reason: string }[];
|
|
21
23
|
retainedConfig?: readonly { key: string; reason: string }[];
|
|
22
24
|
/** #572 AC3: advisory, present while the project root checkout is on a branch other than the default. */
|
|
@@ -29,12 +31,12 @@ export interface StatusOutput {
|
|
|
29
31
|
pendingTaskOffers?: readonly { task: number; peer: string }[];
|
|
30
32
|
isolatedChecks?: { pending?: number; orphans?: string[]; unknownReceipts?: readonly string[]; maintenanceUnavailable?: boolean };
|
|
31
33
|
memoryRead?: { truncated: boolean; scanned: number; limit: number };
|
|
32
|
-
memoryProfile?: { profile?: string; generation?: string; state?: string; agentMemVersion?: string; skippedRows?: number; error?: string };
|
|
34
|
+
memoryProfile?: { profile?: string; generation?: string; state?: string; agentMemVersion?: string; skippedRows?: number; error?: string; sync?: { state: "disabled" | "unavailable" | "notrun" | "not-configured" | "stopped" | "ok" | "failed" | "unknown"; at?: number; cause?: string; declinedCount?: number; declinedIdsKnown?: false; withheldCount?: number; exportedCount?: number; placementActive?: false; nextStep?: string } };
|
|
33
35
|
/** #784 AC3: `active`, `reviewWait` and `reserved` add up to `wip`; absent from an older hub. */
|
|
34
36
|
taskWip?: Record<string, { wip: number; wipLimit: number; active?: number; reviewWait?: number; reserved?: number }>;
|
|
35
37
|
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 };
|
|
36
38
|
ownerIsolation?: Record<string, boolean>; ownerIsolationNotes?: Record<string, string>;
|
|
37
|
-
tasks?: Record<string, number>; models?: { backends?: any[] }; permissionDefaults?: any[]; deliveryError?: string; crash?: string[];
|
|
39
|
+
tasks?: Record<string, number>; personRun?: number[]; models?: { backends?: any[] }; permissionDefaults?: any[]; deliveryError?: string; reviewInputHealth?: { state?: string; cause?: string | null }; crash?: string[];
|
|
38
40
|
}
|
|
39
41
|
/** `peers`: one note per peer that shows no number, and why (no quota, nothing read yet); absent from an older hub. */
|
|
40
42
|
export interface BudgetOutput { budget: Record<string, any>; gate?: number; peers?: { peer: string; kind?: string; note: string }[] }
|
|
@@ -252,6 +254,8 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
252
254
|
const memory = data.memoryProfile;
|
|
253
255
|
const profile = memory.profile === "generation" ? `generation ${memory.generation?.slice(0, 8) ?? "unknown"}` : memory.profile ?? "unbound";
|
|
254
256
|
out.push(...detail("memory", `${profile}; ${memory.state ?? "unknown"}; agent-mem ${memory.agentMemVersion ?? "unknown"}${memory.skippedRows ? `; malformed legacy rows skipped: ${memory.skippedRows}` : ""}${memory.error ? `; ${memory.error}` : ""}`, columns, memory.state === "unavailable" ? "attention" : undefined));
|
|
257
|
+
const sync = memory.sync;
|
|
258
|
+
out.push(...detail("memory sync", memorySyncLine(sync ?? { state: "unknown" }), columns, sync?.state === "failed" ? "attention" : undefined));
|
|
255
259
|
}
|
|
256
260
|
if (data.memoryRead?.truncated) out.push(...detail("memory view", `truncated: newest ${data.memoryRead.limit} project candidates considered`, columns, "attention"));
|
|
257
261
|
if (data.planner) out.push(...detail("planner", `${data.planner.peers?.join(", ") || "nobody"} (${data.planner.source ?? "unknown"})${data.planner.source === "automatic" || data.planner.source === "none" ? "; pin it with ahub settings set planner <peer>" : ""}`, columns, data.planner.source === "none" ? "attention" : undefined));
|
|
@@ -260,6 +264,13 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
260
264
|
if (text) out.push(...detail(`${id} owner isolation`, text, columns, isolated ? undefined : "attention"));
|
|
261
265
|
}
|
|
262
266
|
if (data.deliveryError) out.push(...detail("delivery storage", `${data.deliveryError}; dispatch is stopped`, columns, "failure"));
|
|
267
|
+
for (const [peer, count] of Object.entries(data.deliveryFailures ?? {})) {
|
|
268
|
+
if (Number.isSafeInteger(count) && count > 0) out.push(...detail(`${peer} failed deliveries`, `${count}; next: ahub queue discard --peer ${peer} --state failed`, columns, "attention"));
|
|
269
|
+
}
|
|
270
|
+
if (data.reviewInputHealth) {
|
|
271
|
+
const health = reviewInputHealthLine(data.reviewInputHealth);
|
|
272
|
+
if (health) out.push(...detail("review input provenance", health, columns, data.reviewInputHealth.state === "corrupt" ? "failure" : "attention"));
|
|
273
|
+
}
|
|
263
274
|
for (const pending of data.permissionDefaults ?? []) out.push(...detail("permission default", `${pending.peer}: never-ask from ${pending.source}, stays ask until a person confirms in ahub console`, columns, "attention"));
|
|
264
275
|
for (const crash of data.crash ?? []) out.push(...detail("crash recovery", crash, columns, "attention"));
|
|
265
276
|
for (const warning of data.isolatedChecks?.orphans ?? []) out.push(...detail("isolated check warning", warning, columns, "attention"));
|
|
@@ -275,11 +286,18 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
275
286
|
let settling = false;
|
|
276
287
|
const peerDetails = Object.entries(data.peers).map(([id, p]) => {
|
|
277
288
|
const out: Span[][] = [];
|
|
289
|
+
for (const session of Array.isArray(p.standbySessions) ? p.standbySessions : []) {
|
|
290
|
+
if (!session || typeof session.id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(session.id)) continue;
|
|
291
|
+
const pid = Number.isSafeInteger(session.pid) && session.pid > 0 ? String(session.pid) : "unknown";
|
|
292
|
+
const since = Number.isFinite(session.since) ? `; waiting ${relative(session.since, now)}` : "";
|
|
293
|
+
out.push(...detail(`${id} standby`, `pid ${pid}; session ${session.id}${since}; ahub peer takeover ${id} ${session.id}`, columns, "attention"));
|
|
294
|
+
}
|
|
278
295
|
if (typeof p.form === "string") out.push(...detail(`${id} form`, p.form, columns));
|
|
279
296
|
if (Array.isArray(p.capableRoles)) out.push(...detail(`${id} capable roles`, p.capableRoles.join(", ") || "none", columns));
|
|
280
297
|
if (p.flowRoles) out.push(...detail(`${id} flow roles`, p.flowRoles.join(", ") || "none", columns));
|
|
281
298
|
if (p.catalogRoles) { out.push(...detail(`${id} catalog roles`, Array.isArray(p.catalogRoles.value) ? p.catalogRoles.value.join(", ") || "none" : "unconfirmed", columns)); if (p.catalogRoles.pending !== undefined) out.push(...detail(`${id} pending roles`, p.catalogRoles.pending.join(", ") || "none", columns, "attention")); }
|
|
282
299
|
if (p.liveAccepted?.length) { settling = true; out.push(...detail("settling", p.liveAccepted.map((v: string) => shortId(v, columns, full)).join(", "), columns, "attention")); }
|
|
300
|
+
if (typeof p.reviewObserverHoldNote === "string" && p.reviewObserverHoldNote) out.push(...detail(`${id} review input`, p.reviewObserverHoldNote, columns, "attention"));
|
|
283
301
|
if (p.heldRows?.length) {
|
|
284
302
|
for (const row of p.heldRows) out.push(...detail("held", `${id}: needs_review ${shortId(row.id, columns, full)}; from ${row.senders.join(",")}; kind ${row.kinds.join(",")}; age ${duration(Math.max(0, now - row.createdAt))}${row.context ? `; ${row.context}` : ""}; ahub queue show ${row.id}`, columns, "failure"));
|
|
285
303
|
} else if (p.heldBy) {
|
|
@@ -297,6 +315,7 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
297
315
|
: flat(p.paused).replace(/^(budget: .*), resets .+$/, "$1; reset unknown");
|
|
298
316
|
out.push(...detail("paused", pauseText, columns, "attention"));
|
|
299
317
|
}
|
|
318
|
+
if (p.attachMode === "channel" || p.attachMode === "tools-only") out.push(...detail("attach", `${p.attachMode}; evidence: ${flat(p.attachEvidence ?? "unknown")}`, columns, p.attachMode === "channel" ? "muted" : "attention"));
|
|
300
319
|
if (p.toolsOnly) out.push(...detail(definitionOf(id)?.attach?.pullOnlyForms?.includes("tui") && p.pullOnly ? "pull-only" : "tools-only", flat(p.toolsOnly).replace(/^(?:tools-only|pull-only):\s*/, ""), columns, "attention"));
|
|
301
320
|
if (p.pullOnly && definitionOf(id)?.attach?.pullOnlyForms?.includes("tui")) out.push(...detail(`${id} last read`, typeof p.lastInboxAt === "number" ? relative(p.lastInboxAt, now) : "never", columns, "muted"));
|
|
302
321
|
const actionPolicy = data.actionPolicyInfo?.[id];
|
|
@@ -322,6 +341,7 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
322
341
|
}
|
|
323
342
|
const counts = Object.entries(data.tasks ?? {}).map(([state, count]) => `${count} ${state}`).join(", ");
|
|
324
343
|
if (counts) out.push([], ...lines(`TASKS ${counts} (ahub board)`, columns));
|
|
344
|
+
if (data.personRun?.length) out.push(...lines(`WAITING ON YOU ${data.personRun.map(id => `#${id}`).join(", ")} (person-run: no agent is offered it; ahub backlog list)`, columns, "attention"));
|
|
325
345
|
return out;
|
|
326
346
|
}
|
|
327
347
|
// #724: short next-step codes for the compact board, keyed on the blocker texts of src/hub/sdlc.ts; the full advice stays in --full and ahub task show.
|
|
@@ -345,7 +365,7 @@ export function renderBacklog(cards: any[], columns?: number, now = Date.now(),
|
|
|
345
365
|
let table: OutputCell[][] = sorted.map(card => {
|
|
346
366
|
const deps: number[] = Array.isArray(card.deps) ? card.deps : [];
|
|
347
367
|
return [span(typeof card.rank === "number" ? card.rank : "-", "muted", true), span(`#${card.id}`, "info", true), span(typeof card.priority === "number" ? `P${card.priority}` : "-", undefined, true),
|
|
348
|
-
span(card.reserved ?? "-", peerTone(card.reserved ?? ""), true), span(deps.length ? (full || columns === undefined ? deps.map(id => `#${id}`).join(",") : `#${deps[0]}${deps.length > 1 ? `,+${deps.length - 1}` : ""}`) : "-", undefined, true), span(card.class ?? "-", undefined, true),
|
|
368
|
+
span(card.needsPerson === true ? `${card.reserved ?? ""}${card.reserved ? " " : ""}(person)` : card.reserved ?? "-", card.needsPerson === true ? "attention" : peerTone(card.reserved ?? ""), true), span(deps.length ? (full || columns === undefined ? deps.map(id => `#${id}`).join(",") : `#${deps[0]}${deps.length > 1 ? `,+${deps.length - 1}` : ""}`) : "-", undefined, true), span(card.class ?? "-", undefined, true),
|
|
349
369
|
span(typeof card.created === "number" ? duration(now - card.created) : "-", "muted", true), span(flat(card.title))];
|
|
350
370
|
});
|
|
351
371
|
if (!full && columns !== undefined) {
|
|
@@ -357,8 +377,10 @@ export function renderBacklog(cards: any[], columns?: number, now = Date.now(),
|
|
|
357
377
|
table = table.map(row => [...row.slice(0, -1), { ...row[row.length - 1]!, text: ellipsis(row[row.length - 1]!.text, titleRoom) }]);
|
|
358
378
|
}
|
|
359
379
|
const owners = new Map<string, number>();
|
|
360
|
-
|
|
361
|
-
const
|
|
380
|
+
const person = sorted.filter(card => card.needsPerson === true);
|
|
381
|
+
for (const card of sorted.filter(card => card.needsPerson !== true)) owners.set(card.reserved ?? "unreserved", (owners.get(card.reserved ?? "unreserved") ?? 0) + 1);
|
|
382
|
+
const summary = `${plural(sorted.length, "backlog card")}; reserved: ${[...owners].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([owner, n]) => `${owner} ${n}`).join(", ") || "none"}`
|
|
383
|
+
+ (person.length ? `; waiting on you: ${person.map(card => `#${card.id}`).join(", ")} (a person-run leg; no agent is offered it)` : "");
|
|
362
384
|
return [...rows(names, table, columns), [], ...lines(summary, columns)];
|
|
363
385
|
}
|
|
364
386
|
/** #724: one line per task. TITLE goes last and is cut to the room left; at a narrow width REVIEWER, CLASS and AGE give way first. */
|
|
@@ -545,6 +567,7 @@ function fields(value: unknown, columns: number | undefined, now: number, prefix
|
|
|
545
567
|
}
|
|
546
568
|
export function renderQueueShow(delivery: any, columns?: number, now = Date.now()): Span[][] {
|
|
547
569
|
const out = [...lines("Delivery", columns, "strong"), ...fields(delivery, columns, now)];
|
|
570
|
+
if (delivery?.state === "failed" && delivery.failureCause === undefined) out.push(...detail("failure cause", "unknown (not recorded by this hub version)", columns, "muted"));
|
|
548
571
|
if (delivery?.state === "needs_review" && delivery.id) out.push([], ...detail("resolve", `ahub queue resolve ${delivery.id} --action completed|retry|discard --reason <text>`, columns, "failure"));
|
|
549
572
|
// #1096: a failed row that no one has settled can be cleared by the person or the conductor; one a resolution already settled says what is left (nothing).
|
|
550
573
|
else if (delivery?.state === "failed" && delivery.id) {
|
package/src/cli/pet.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function parseNeedsSummary(raw: unknown): NeedsSummary | undefined {
|
|
|
18
18
|
if (row.first !== undefined) {
|
|
19
19
|
if (!row.first || typeof row.first !== "object" || Array.isArray(row.first)) return;
|
|
20
20
|
const item = row.first as Record<string, unknown>;
|
|
21
|
-
if (typeof item.kind !== "string" || typeof item.level !== "string" || !["approval", "default", "storage", "recovery", "delivery", "review", "budget", "offline"].includes(item.kind) || !["act", "watch"].includes(item.level)) return;
|
|
21
|
+
if (typeof item.kind !== "string" || typeof item.level !== "string" || !["approval", "default", "question", "storage", "recovery", "delivery", "review", "result", "budget", "offline"].includes(item.kind) || !["act", "watch"].includes(item.level)) return;
|
|
22
22
|
if (item.peer !== undefined && !id(item.peer)) return;
|
|
23
23
|
if (item.id !== undefined && !id(item.id) && !(count(item.id) && item.id > 0)) return;
|
|
24
24
|
if (item.until !== undefined && !count(item.until) || item.since !== undefined && !count(item.since)) return;
|
|
@@ -29,8 +29,8 @@ export function parseNeedsSummary(raw: unknown): NeedsSummary | undefined {
|
|
|
29
29
|
if ((row.act + row.watch > 0) !== !!first) return;
|
|
30
30
|
if (row.act > 0 && first?.level !== "act" || row.act === 0 && first?.level === "act") return;
|
|
31
31
|
if (row.act === 0 && ["waiting", "blocked", "review"].includes(row.state) || row.act > 0 && ["working", "idle", "sleeping"].includes(row.state)) return;
|
|
32
|
-
if (first?.level === "watch" && !["recovery", "budget", "offline"].includes(first.kind) || first?.level === "act" && ["budget", "offline"].includes(first.kind)) return;
|
|
33
|
-
if (row.state === "waiting" && !["approval", "default"].includes(first?.kind ?? "")) return;
|
|
32
|
+
if (first?.level === "watch" && !["recovery", "result", "budget", "offline"].includes(first.kind) || first?.level === "act" && ["budget", "offline"].includes(first.kind)) return;
|
|
33
|
+
if (row.state === "waiting" && !["approval", "default", "question"].includes(first?.kind ?? "")) return;
|
|
34
34
|
if (row.state === "blocked" && !["storage", "recovery", "delivery"].includes(first?.kind ?? "")) return;
|
|
35
35
|
if (row.state === "review" && first?.kind !== "review") return;
|
|
36
36
|
return { act: row.act, watch: row.watch, state: row.state as NeedsSummary["state"], ...(first ? { first } : {}) };
|
|
@@ -57,7 +57,7 @@ export function petView(status: unknown, reader: PetRecoveryReader = recoveryRea
|
|
|
57
57
|
const included = base && reported && typeof reported === "object" && (reported as Record<string, unknown>).operationId === operation && base.watch > 0 ? 1 : 0;
|
|
58
58
|
const act = (base?.act ?? 0) + (active ? 0 : 1), watch = (base?.watch ?? 0) - included + (active ? 1 : 0);
|
|
59
59
|
const first = base?.first;
|
|
60
|
-
const precedes = !active && (!first || first.level === "watch" || !["approval", "default", "storage"].includes(first.kind));
|
|
60
|
+
const precedes = !active && (!first || first.level === "watch" || !["approval", "default", "question", "storage"].includes(first.kind));
|
|
61
61
|
const selected = precedes || first?.kind === "recovery" && first.id === operation ? item : first ?? item;
|
|
62
62
|
const state = !active && base?.state !== "waiting" ? "blocked" : base?.state ?? "sleeping";
|
|
63
63
|
return { summary: { act, watch, state, first: selected }, unavailable: !base, ...(runner === "unknown" ? { runnerUnknown: true } : {}) };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1054 T4: `ahub results [--unread] [--read <id> | --read-all | --unread-last]`. Pure: argv to one closed request, never a decision about whether it applies (the hub checks
|
|
3
|
+
* that a result exists and is unread). At most one action flag; `--unread` only narrows the list and takes no action.
|
|
4
|
+
*/
|
|
5
|
+
export const RESULT_ID = /^cmp-[a-f0-9]{12}$/;
|
|
6
|
+
export type ResultsRequest = { action: "list"; unread: boolean } | { action: "read"; id: string } | { action: "read-all" } | { action: "unread-last" };
|
|
7
|
+
export type ResultsArgs = { ok: true; value: ResultsRequest } | { ok: false; error: string };
|
|
8
|
+
export const RESULTS_USAGE = "usage: ahub results [--unread] [--read <id> | --read-all | --unread-last] [--json] [--color=auto|always|never]";
|
|
9
|
+
|
|
10
|
+
export function parseResultsArgs(args: readonly string[]): ResultsArgs {
|
|
11
|
+
const fail = (error: string): { ok: false; error: string } => ({ ok: false, error: `${RESULTS_USAGE}; ${error}` });
|
|
12
|
+
let unread = false;
|
|
13
|
+
const actions: ResultsRequest[] = [];
|
|
14
|
+
for (let index = 0; index < args.length; index++) {
|
|
15
|
+
const word = args[index]!;
|
|
16
|
+
if (word === "--unread") { unread = true; continue; }
|
|
17
|
+
if (word === "--read-all") { actions.push({ action: "read-all" }); continue; }
|
|
18
|
+
if (word === "--unread-last") { actions.push({ action: "unread-last" }); continue; }
|
|
19
|
+
if (word === "--read" || word.startsWith("--read=")) {
|
|
20
|
+
const id = word === "--read" ? args[++index] : word.slice("--read=".length);
|
|
21
|
+
if (id === undefined || !RESULT_ID.test(id)) return fail("--read needs a result id like cmp-0123456789ab (ahub results lists them)");
|
|
22
|
+
actions.push({ action: "read", id });
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
return fail(word.startsWith("--") ? `unknown flag ${word.split("=")[0]}` : `unexpected operand ${word.slice(0, 40)}`);
|
|
26
|
+
}
|
|
27
|
+
if (actions.length > 1) return fail("give one of --read, --read-all and --unread-last");
|
|
28
|
+
if (actions.length === 1) { if (unread) return fail("--unread only narrows the list; it does not combine with an action"); return { ok: true, value: actions[0]! }; }
|
|
29
|
+
return { ok: true, value: { action: "list", unread } };
|
|
30
|
+
}
|