@staix/agent-hub 0.29.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 +51 -0
- package/README.md +15 -7
- package/docs/agent-notes/README.md +13 -0
- package/docs/agent-notes/adapters.md +7 -3
- package/docs/agent-notes/budget.md +1 -0
- package/docs/agent-notes/bus.md +10 -4
- package/docs/agent-notes/daemon.md +16 -8
- package/docs/agent-notes/local-worker.md +1 -0
- package/docs/agent-notes/tasks.md +10 -4
- package/docs/agent-notes/tests.md +8 -4
- package/docs/design/adapters.md +108 -0
- package/docs/design/agent-hub-architecture.md +58 -0
- package/docs/design/bus.md +147 -1
- package/docs/design/task-board.md +100 -0
- package/docs/dogfood.md +29 -8
- package/docs/events.md +3 -0
- package/docs/memory.md +17 -1
- package/docs/operations.md +410 -86
- package/docs/profiles.md +35 -0
- package/docs/quickstart.md +25 -5
- package/docs/sdlc.md +1 -1
- package/docs/security.md +182 -62
- package/docs/smoke.md +13 -2
- 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/specs/2026-10-12-office-view-design.md +86 -3
- package/docs/verification/2026-11-03-0.30.0.md +54 -0
- package/docs/verification/2026-11-03-agy-1.4.12-launcher-nonce.md +42 -0
- package/docs/verification/2026-11-03-agy-1.4.12.md +90 -0
- package/docs/verification/2026-11-03-agy-tui-close.md +49 -0
- package/docs/verification/2026-11-03-grok-1.0.62-t0.md +150 -0
- package/docs/verification/2026-11-03-pi-150-compat.md +40 -0
- package/docs/verification/2026-11-03-pi-title-api.md +26 -0
- package/docs/verification/2026-11-03-qwen-0265-acp.md +55 -0
- package/docs/verification/2026-11-03-qwen-0265-dual-output.md +40 -0
- 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/verification/dogfood/2026-11-03.md +3095 -0
- package/docs/verification/pi-150-model-free.ts +57 -0
- package/docs/verification/pi-150-native.mjs +21 -0
- package/docs/verification/pi-150-title-api.mjs +31 -0
- 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 +4565 -3617
- 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 +13 -13
- package/src/adapters/definitions/builtin-metadata.ts +13 -13
- package/src/adapters/definitions/qwen.ts +1 -0
- package/src/adapters/local-worker.ts +82 -23
- package/src/adapters/pi.ts +140 -16
- package/src/adapters/registry-types.ts +16 -0
- package/src/adapters/registry.ts +4 -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/autonomy.ts +66 -1
- package/src/cli/backlog-return-args.ts +37 -0
- package/src/cli/console-question.ts +110 -0
- package/src/cli/console-state.ts +82 -17
- 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 +10 -5
- package/src/cli/identity.ts +21 -4
- package/src/cli/main.ts +167 -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/cli/worktree-tidy.ts +158 -0
- package/src/cli/worktrees-output.ts +43 -0
- package/src/hub/action-class.ts +9 -1
- package/src/hub/board.ts +396 -20
- package/src/hub/bus.ts +497 -57
- 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/conductor.ts +4 -3
- package/src/hub/config-apply.ts +8 -5
- package/src/hub/config-reload.ts +10 -1
- package/src/hub/control-client.ts +40 -4
- package/src/hub/daemon.ts +1795 -142
- 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/dogfood-read.ts +62 -9
- 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 +66 -11
- package/src/hub/manager.ts +67 -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/peer-doing.ts +33 -3
- package/src/hub/peers.ts +32 -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/settings.ts +3 -0
- package/src/hub/task-sweep.ts +2 -0
- package/src/hub/tasks.ts +607 -71
- package/src/hub/ui.ts +2 -0
- package/src/hub/worktree-prs.ts +100 -0
- package/src/hub/worktree-shelf.ts +246 -0
- package/src/memory/capture.ts +5 -2
- package/src/pet/config.ts +25 -13
- package/src/pi/extension.ts +43 -3
- 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 +271 -17
- package/src/ui/needs.ts +10 -4
- package/src/ui/office-model.ts +64 -8
- package/src/ui/office-space.ts +1 -0
- package/src/ui/plugin-questions.ts +56 -0
- package/src/ui/plugin-results.ts +81 -0
- package/src/ui/plugin.ts +219 -28
- 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";
|
|
@@ -105,6 +108,8 @@ import { benchPreflight, defaultIo, runBench } from "./bench.ts";
|
|
|
105
108
|
import { ADDONS, isAddon, type AddonName } from "../hub/tooling.ts";
|
|
106
109
|
import { goalCommand, memoryCommand, memoryStaleAnchorsCommand } from "./memory.ts";
|
|
107
110
|
import { runSpecs, runSpecsStamp } from "./specs.ts";
|
|
111
|
+
import { confirmTidy, planTidy, realTidyGit, rowsFromReply, type TidyGit } from "./worktree-tidy.ts";
|
|
112
|
+
import { renderTidyPreview, renderTidyResult, renderWorktrees } from "./worktrees-output.ts";
|
|
108
113
|
import { formatMemoryStaleAnchors } from "../hub/memory-stale-anchors.ts";
|
|
109
114
|
import { AGENT_MEM_VERSION, assertAgentMemVersion, selectedMemoryProfile } from "../hub/memory-profile.ts";
|
|
110
115
|
import { checkMemoryProfiles, formatMemoryProfileCheck } from "../hub/memory-profile-check.ts";
|
|
@@ -715,6 +720,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
715
720
|
runner: recoveryRunner,
|
|
716
721
|
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer, undefined, conductorPeer).inspect),
|
|
717
722
|
endInspection: recoveryEndInspection,
|
|
723
|
+
afterCompleted: (op) => globalUpdateLines(op.plan.version),
|
|
718
724
|
endPeer: (planned, peer) => endPlannedPeer(planned, peer),
|
|
719
725
|
runnerStoppable: (id) => signedRunner(id) !== undefined,
|
|
720
726
|
stopRunner: (id) => stopSignedRunner(id),
|
|
@@ -723,6 +729,16 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
723
729
|
};
|
|
724
730
|
}
|
|
725
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
|
+
|
|
726
742
|
async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
727
743
|
const display = recoveryDisplay();
|
|
728
744
|
const { one, rest } = takeFlags(display.args, ["--to"], []);
|
|
@@ -797,7 +813,11 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
797
813
|
writeOperation(id, operation);
|
|
798
814
|
} else operation = createOperation(plan, sourceRoot, undefined, delegatedPeer ? { peer: delegatedPeer, command: kind } : undefined);
|
|
799
815
|
if (display.json) { spawnRecovery(operation, true); console.log(JSON.stringify(publicOperation(operation), null, 2)); }
|
|
800
|
-
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
|
+
}
|
|
801
821
|
}
|
|
802
822
|
|
|
803
823
|
/** Strict person command: recorded identity only, no session or terminal overrides. */
|
|
@@ -1160,8 +1180,6 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1160
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 } } : {}) });
|
|
1161
1181
|
console.log(`ahub up (ws://127.0.0.1:${status.controlPort}), state in ${stateDir}`);
|
|
1162
1182
|
if (recoveryLauncherCurrent) { recoveryLauncherCurrent(); return; }
|
|
1163
|
-
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
1164
|
-
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
1165
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).
|
|
1166
1184
|
if (!args.includes("--no-agents") && !liveTest && caller.role === "console" && identity.role === "console" && !process.env.AGENTHUB_RECOVERY_OPERATION) {
|
|
1167
1185
|
try {
|
|
@@ -1408,7 +1426,10 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1408
1426
|
}
|
|
1409
1427
|
if (sub === "groom" && operands.length === 1) return shown(renderGroom)(taskOp("hub_planner_groom", {}));
|
|
1410
1428
|
if (["return", "return-untouched"].includes(sub ?? "") && /^[1-9]\d*$/.test(id ?? "") && /^\d+$/.test(revision ?? "") && operands.length >= 4) {
|
|
1411
|
-
|
|
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 } : {}) }));
|
|
1412
1433
|
}
|
|
1413
1434
|
if (sub === "add" && args.length >= 3) return console.log(await taskOp("hub_task_propose", { class: id, title: args.slice(2).join(" "), backlog: true }));
|
|
1414
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>");
|
|
@@ -1420,12 +1441,12 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1420
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(" ") }));
|
|
1421
1442
|
}
|
|
1422
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) }));
|
|
1423
|
-
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");
|
|
1424
1445
|
let fields: unknown;
|
|
1425
|
-
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); }
|
|
1426
1447
|
catch { return fail("planning fields must be JSON"); }
|
|
1427
|
-
if (sub === "pin" && value !== "true" && value !== "false") fail(
|
|
1428
|
-
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(" ") }));
|
|
1429
1450
|
},
|
|
1430
1451
|
|
|
1431
1452
|
release: async () => {
|
|
@@ -1925,9 +1946,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1925
1946
|
if (sub === "handoff") {
|
|
1926
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
|
|
1927
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.
|
|
1928
|
-
const present = outputOptions(splitPresentation(rest, { valueFlags: ["--project", "--path", "--issue", "--task", "--detail"], edges: true }).flags);
|
|
1929
|
-
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);
|
|
1930
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);
|
|
1931
1954
|
const text = await taskOp("hub_task_handoff", { ...parsed });
|
|
1932
1955
|
return showToolText(data => renderHandoffReceipt(data, present.columns), text, present);
|
|
1933
1956
|
}
|
|
@@ -2003,6 +2026,47 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2003
2026
|
} finally { hub.close(); }
|
|
2004
2027
|
},
|
|
2005
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
|
+
|
|
2058
|
+
title: async () => {
|
|
2059
|
+
const [peer, ...words] = args;
|
|
2060
|
+
if (!peer || !words.length) fail("usage: ahub title <peer> <text...>");
|
|
2061
|
+
const hub = await connect();
|
|
2062
|
+
try {
|
|
2063
|
+
const reply = await hub.request({ t: "title", peer, title: words.join(" ") }, 35_000).catch(() => ({ ok: false, error: "session title result is unconfirmed; the title may have been applied; check ahub status before retrying" }));
|
|
2064
|
+
if (reply.ok === false && reply.error === "no answer from the hub within 35 s") fail("session title request timed out; the title may have been applied; check ahub status before retrying");
|
|
2065
|
+
if (reply.ok !== true) fail(String(reply.error ?? `session title refused: ${String(reply.reason ?? "invalid")}; refresh ahub status, then retry`));
|
|
2066
|
+
console.log(`${peer}: session title updated`);
|
|
2067
|
+
} finally { hub.close(); }
|
|
2068
|
+
},
|
|
2069
|
+
|
|
2006
2070
|
permission: async () => {
|
|
2007
2071
|
const positional = args.filter(arg => arg !== "--yes");
|
|
2008
2072
|
const [peer, mode] = positional;
|
|
@@ -2260,15 +2324,77 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2260
2324
|
hub.close();
|
|
2261
2325
|
},
|
|
2262
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
|
+
|
|
2263
2389
|
pet: async () => {
|
|
2264
2390
|
if (args.includes("--window")) {
|
|
2265
2391
|
if (args.some(arg => !["--window", "--close"].includes(arg))) return fail("usage: ahub pet --window [--close]");
|
|
2266
|
-
return console.log(
|
|
2392
|
+
return console.log(PET_WINDOW_RETIRED_NOTICE);
|
|
2267
2393
|
}
|
|
2268
2394
|
if (args.includes("--palette")) {
|
|
2269
2395
|
if (args.length !== 2 || args[0] !== "--palette") return fail("usage: ahub pet [--short] | --window [--close] | --palette <blue|teal|violet|ochre>");
|
|
2270
|
-
|
|
2271
|
-
|
|
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);
|
|
2272
2398
|
}
|
|
2273
2399
|
if (args.some(arg => arg !== "--short")) return fail("usage: ahub pet [--short] | --window [--close] | --palette <blue|teal|violet|ochre>");
|
|
2274
2400
|
const short = args.includes("--short");
|
|
@@ -2444,6 +2570,31 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
2444
2570
|
process.exitCode = await runSpecsStamp(args.slice(1), cwd, loadConfig(cwd).specs, { isTTY: !!rl, ask: async question => /^y(es)?$/i.test((await rl!.question(`${question} [y/N] `)).trim()) });
|
|
2445
2571
|
} finally { rl?.close(); }
|
|
2446
2572
|
},
|
|
2573
|
+
// #1057: `ahub worktrees [--json]` lists live and settled worktrees; `tidy` previews what is safe to remove; `tidy --confirm <fingerprint>` removes it, in THIS process,
|
|
2574
|
+
// never the daemon's. `--confirm` is console class (classifyPeerCommand) and refused under --live-test, so an agent can only list and preview.
|
|
2575
|
+
worktrees: async () => {
|
|
2576
|
+
const options = outputOptions();
|
|
2577
|
+
const rest = outputArguments();
|
|
2578
|
+
const sub = rest[0] === "tidy" ? "tidy" : undefined;
|
|
2579
|
+
const flags = sub ? rest.slice(1) : rest;
|
|
2580
|
+
const at = flags.indexOf("--confirm");
|
|
2581
|
+
const fingerprint = at >= 0 ? flags[at + 1] : undefined;
|
|
2582
|
+
const known = flags.every((arg, i) => arg === "--confirm" || i === at + 1 && at >= 0);
|
|
2583
|
+
if ((rest.length && !sub && rest.some(arg => !arg.startsWith("--"))) || !known || (at >= 0 && (!sub || fingerprint === undefined || fingerprint.startsWith("--"))) || (sub === undefined && at >= 0)) fail("usage: ahub worktrees [--json] | tidy [--json] | tidy --confirm <fingerprint>");
|
|
2584
|
+
const hub = await connect();
|
|
2585
|
+
const reply = await hub.request({ t: "worktrees" }, 20_000);
|
|
2586
|
+
hub.close();
|
|
2587
|
+
if (!reply.ok) return fail(String(reply.error ?? "the worktree shelf is unavailable"));
|
|
2588
|
+
if (!sub) return options.json ? console.log(JSON.stringify({ live: reply.live, settled: reply.settled, complete: reply.complete }, null, 2)) : printOutput(renderWorktrees(reply, options.columns, Date.now()), options.color);
|
|
2589
|
+
// tidy: read-only git in this process to decide what is safe; the daemon only said which rows are settled.
|
|
2590
|
+
const git: TidyGit = realTidyGit((argv, dir) => { const run = hubGitSync(argv, { cwd: dir, encoding: "utf8", timeout: 20_000, maxBuffer: 4 * 1024 * 1024, env: hubGitEnv(process.env, { repositoryIsolation: true }) }); return run.error ? undefined : { status: run.status, stdout: String(run.stdout ?? "") }; });
|
|
2591
|
+
const plan = planTidy(rowsFromReply(reply.settled ?? [], cwd, git), cwd, git);
|
|
2592
|
+
if (at < 0) return options.json ? console.log(JSON.stringify({ entries: plan.entries, removable: plan.removable, fingerprint: plan.fingerprint }, null, 2)) : printOutput(renderTidyPreview(plan, options.columns), options.color);
|
|
2593
|
+
const result = confirmTidy(plan, fingerprint!, cwd, git);
|
|
2594
|
+
if (!result.ok) return fail(result.error);
|
|
2595
|
+
if (options.json) return console.log(JSON.stringify({ outcomes: result.outcomes }, null, 2));
|
|
2596
|
+
printOutput(renderTidyResult(result.outcomes, options.columns), options.color);
|
|
2597
|
+
},
|
|
2447
2598
|
usage: async () => {
|
|
2448
2599
|
const options = outputOptions();
|
|
2449
2600
|
const rest = outputArguments();
|
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 } : {}) };
|