@staix/agent-hub 0.12.32 → 0.12.34
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 +16 -0
- package/README.md +6 -5
- package/docs/agent-notes/adapters.md +7 -3
- package/docs/agent-notes/budget.md +5 -3
- package/docs/agent-notes/bus.md +2 -0
- package/docs/agent-notes/daemon.md +7 -5
- package/docs/agent-notes/tasks.md +3 -2
- package/docs/agent-notes/tests.md +2 -1
- package/docs/operations.md +48 -14
- package/docs/quickstart.md +5 -3
- package/docs/security.md +10 -2
- package/docs/smoke.md +1 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +246 -4
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +20 -0
- package/docs/verification/2026-10-18-0.12.33.md +42 -0
- package/docs/verification/2026-10-18-0.12.34.md +48 -0
- package/docs/verified.json +33 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +22882 -16496
- package/src/adapters/codex-appserver.ts +81 -11
- package/src/adapters/definitions/builtin-metadata.ts +101 -7
- package/src/adapters/definitions/native-cli-claude-launch.ts +90 -0
- package/src/adapters/definitions/native-cli-claude-setup.ts +80 -0
- package/src/adapters/definitions/native-cli-codex-launch.ts +14 -0
- package/src/adapters/definitions/native-cli-doctor.ts +46 -0
- package/src/adapters/definitions/native-cli-entry-claude.ts +52 -0
- package/src/adapters/definitions/native-cli-entry-codex.ts +26 -0
- package/src/adapters/definitions/native-cli-entry-jcode.ts +24 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +52 -0
- package/src/adapters/definitions/native-cli-entry-local.ts +14 -0
- package/src/adapters/definitions/native-cli-entry-pi.ts +51 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +20 -0
- package/src/adapters/definitions/native-cli-entry-types.ts +40 -0
- package/src/adapters/definitions/native-cli-entry.ts +19 -0
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +58 -0
- package/src/adapters/definitions/native-cli-launch-common.ts +17 -0
- package/src/adapters/definitions/native-cli-pi.ts +33 -0
- package/src/adapters/definitions/native-cli-preview-claude.ts +37 -0
- package/src/adapters/definitions/native-cli-preview-codex.ts +20 -0
- package/src/adapters/definitions/native-cli-preview-common.ts +57 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +20 -0
- package/src/adapters/definitions/native-cli-preview-kimi.ts +28 -0
- package/src/adapters/definitions/native-cli-preview-pi.ts +36 -0
- package/src/adapters/definitions/native-cli-preview-qwen.ts +22 -0
- package/src/adapters/definitions/native-cli-preview.ts +16 -0
- package/src/adapters/definitions/native-config-defaults.ts +19 -0
- package/src/adapters/definitions/native-config-policy.ts +35 -0
- package/src/adapters/definitions/native-context.ts +35 -0
- package/src/adapters/definitions/native-daemon-kimi.ts +5 -0
- package/src/adapters/definitions/native-daemon-policy.ts +18 -0
- package/src/adapters/definitions/native-display.ts +18 -0
- package/src/adapters/definitions/native-factories.ts +48 -0
- package/src/adapters/definitions/native-policy-aux.ts +130 -0
- package/src/adapters/definitions/native-quota.ts +29 -0
- package/src/adapters/definitions/native-recovery-continuity.ts +12 -0
- package/src/adapters/definitions/native-recovery.ts +88 -0
- package/src/adapters/definitions/qwen.ts +8 -4
- package/src/adapters/registry-types.ts +97 -10
- package/src/adapters/registry.ts +103 -3
- package/src/cli/console-state.ts +5 -5
- package/src/cli/identity.ts +6 -5
- package/src/cli/jcode-mcp.ts +2 -58
- package/src/cli/launch.ts +14 -100
- package/src/cli/main.ts +35 -272
- package/src/cli/output.ts +3 -2
- package/src/cli/preview.ts +14 -125
- package/src/cli/recovery-guidance.ts +17 -13
- package/src/cli/reset.ts +13 -1
- package/src/cli/setup.ts +2 -80
- package/src/cli/shell-quote.ts +3 -0
- package/src/cli/terminal-recovery.ts +17 -10
- package/src/cli/upgrade-interactive.ts +4 -2
- package/src/cli/upgrade-presentation.ts +11 -1
- package/src/cli/upgrade-restore-state.ts +2 -1
- package/src/cli/upgrade-runtime.ts +107 -146
- package/src/cli/upgrade.ts +10 -7
- package/src/hub/budget.ts +6 -30
- package/src/hub/bus.ts +133 -14
- package/src/hub/conductor.ts +8 -4
- package/src/hub/config-apply.ts +9 -13
- package/src/hub/config-reload.ts +7 -2
- package/src/hub/context-window.ts +3 -27
- package/src/hub/control-client.ts +1 -1
- package/src/hub/crash.ts +2 -35
- package/src/hub/daemon.ts +244 -241
- package/src/hub/deferred-hub.ts +60 -0
- package/src/hub/delivery-journal.ts +6 -0
- package/src/hub/execution-budget.ts +2 -1
- package/src/hub/hub-tools.ts +3 -3
- package/src/hub/peer-catalog.ts +4 -13
- package/src/hub/permission-mode.ts +9 -12
- package/src/hub/recovery-continuity.ts +6 -5
- package/src/hub/report.ts +9 -8
- package/src/hub/restart.ts +12 -1
- package/src/hub/roster.ts +6 -7
- package/src/hub/routing.ts +21 -10
- package/src/hub/settings.ts +6 -5
- package/src/hub/settled-roles.ts +1 -1
- package/src/hub/start-mode.ts +8 -6
- package/src/hub/tasks.ts +14 -11
- package/src/hub/tooling.ts +4 -60
- package/src/hub/ui.ts +3 -1
- package/src/memory/recall.ts +4 -5
- package/src/models/route/signals.ts +3 -1
- package/src/pi/launch.ts +2 -1
- package/src/ui/index.html +3 -2
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +16 -6
- package/templates/routing.toml +2 -2
package/src/hub/tasks.ts
CHANGED
|
@@ -18,12 +18,12 @@ import { CLASSES, OUTCOMES_KEPT_MS, ALL_PLAN_KEYS, TASK_MOVE_REASONS, taskHasSta
|
|
|
18
18
|
import type { Bus } from "./bus.ts";
|
|
19
19
|
import { HUB, newEnvelope, NOTE_KINDS, noteLine, sanitize, USER, type Envelope, type PeerId, type PeerState } from "./envelope.ts";
|
|
20
20
|
import type { PiiVerdict, ScreenItem, ScreenRecord } from "./inference.ts";
|
|
21
|
-
import { assign, detectSignals,
|
|
21
|
+
import { assign, detectSignals, peerEntry, predictSplit, relayCandidates, type Assignment, type PeerCapabilities, type Routing, type SplitObservation, type SplitPrediction } from "./routing.ts";
|
|
22
22
|
import { ExecutionBudget, type ExecutionBudgetConfig, type ExecutionBudgetDecision, type ExecutionBudgetStatus, type ExecutionUnit } from "./execution-budget.ts";
|
|
23
23
|
import { Cohorts, MAX_REQUESTS, type Cohort, type Completion } from "./cohorts.ts";
|
|
24
24
|
import { realPath } from "./project.ts";
|
|
25
25
|
import { PreEffectToolRefusal } from "./tool-refusal.ts";
|
|
26
|
-
import {
|
|
26
|
+
import { ADAPTERS, definitionOf } from "../adapters/registry.ts";
|
|
27
27
|
import { nextSweep, taskSweepConfig, type TaskSweepConfig, type SweepRecord } from "./task-sweep.ts";
|
|
28
28
|
import { readSdlcPr, sdlcConfig, sdlcReceipt, sdlcStatus, stageDefinition, lifecycleStage, type SdlcConfig, sdlcGh, type SdlcGh, type SdlcStatus } from "./sdlc.ts";
|
|
29
29
|
|
|
@@ -125,9 +125,12 @@ export interface TasksDeps {
|
|
|
125
125
|
sweep?: TaskSweepConfig;
|
|
126
126
|
/** Shutdown or coordinator work holds task sweeps too. */
|
|
127
127
|
sweepHeld?: () => boolean;
|
|
128
|
+
/** Consecutive changes_requested verdicts that move a task to another peer (`tasks.escalate_after`, #488). */
|
|
129
|
+
escalateAfter?: () => number;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
/** The `tasks.escalate_after` default (#488): the third consecutive changes_requested escalates. */
|
|
133
|
+
export const ESCALATE_AFTER = 3;
|
|
131
134
|
// Demotion (issue #36): a failure counts half after a day. A peer is demoted for a class once its decayed failures
|
|
132
135
|
// reach 1.5 (two within about half a day, three within two days) and outweigh its decayed successes there.
|
|
133
136
|
// ponytail: fixed constants; make them config when someone needs to tune them.
|
|
@@ -509,7 +512,7 @@ export class Tasks {
|
|
|
509
512
|
private health() {
|
|
510
513
|
const failing = this.d.failing?.();
|
|
511
514
|
const held = this.d.held?.();
|
|
512
|
-
return { ...(this.d.capabilities ? { capabilities: this.d.capabilities(), repository: this.d.repository } : {}), pullOnly: [...this.d.bus.peers.values()].filter(peer => peer.id
|
|
515
|
+
return { ...(this.d.capabilities ? { capabilities: this.d.capabilities(), repository: this.d.repository } : {}), pullOnly: [...this.d.bus.peers.values()].filter(peer => definitionOf(peer.id)?.attach?.pullOnlyForms?.length && peer.pullOnly).map(peer => peer.id), offRoster: ADAPTERS.map(adapter => adapter.kind).filter(peer => !this.d.bus.isRostered(peer)), ...(failing ? { failing } : {}), ...(held ? { held } : {}), ...(this.d.roles ? { roles: this.d.roles } : {}) };
|
|
513
516
|
}
|
|
514
517
|
|
|
515
518
|
/** Capability admission precedes explicit assignments/reservations, independent of readiness or transient peer health. */
|
|
@@ -799,7 +802,7 @@ export class Tasks {
|
|
|
799
802
|
if (by !== USER && Object.hasOwn(patch, "reserved") && this.hasPersonReservation(task)) throw new PreEffectToolRefusal("a person's reservation is retained");
|
|
800
803
|
if (Object.hasOwn(patch, "deps") || Object.hasOwn(patch, "reserved")) {
|
|
801
804
|
if (task.state !== "backlog" || taskHasStarted(task) || task.owner) throw new PreEffectToolRefusal("dependency and reservation changes require unstarted backlog work");
|
|
802
|
-
if (patch.reserved && !(
|
|
805
|
+
if (patch.reserved && !definitionOf(patch.reserved) && !this.d.bus.knownPeers().includes(patch.reserved) && !this.d.routing().classes[task.class]?.peers.includes(patch.reserved)) throw new PreEffectToolRefusal("reserved owner is not a supported or configured peer");
|
|
803
806
|
if (patch.reserved && !this.d.bus.isRostered(patch.reserved)) throw new PreEffectToolRefusal("reserved owner is off roster");
|
|
804
807
|
if (patch.reserved) this.requireCapabilities(task, patch.reserved);
|
|
805
808
|
const rows = new Map(this.d.board.list().map(row => [row.id, row]));
|
|
@@ -1596,7 +1599,7 @@ export class Tasks {
|
|
|
1596
1599
|
const pii = this.isPii(task), delivery = { ...a, reviewer: task.reviewer ?? undefined };
|
|
1597
1600
|
const rejected = task.history.filter((h) => h.event === "changes_requested").map((h) => `- ${h.by}: ${this.screen(task, h.note ?? "", "review note", delivery.owner, h.withheld)}`);
|
|
1598
1601
|
const handoff = context && !pii ? this.screen(task, context, "handoff", delivery.owner, contextWithheld).slice(0, 3000) : "";
|
|
1599
|
-
const facts = [`class ${profileTaskLabel(task)}`, delivery.owner
|
|
1602
|
+
const facts = [`class ${profileTaskLabel(task)}`, delivery.owner && definitionOf(delivery.owner)?.routing?.backendLabel ? `backend ${definitionOf(delivery.owner)!.routing!.backendLabel}/${delivery.piBackend ?? "dgx"}` : "", task.refs.paths?.length ? `paths ${task.refs.paths.join(", ")}` : "", task.refs.branch ? `branch ${task.refs.branch}` : "", delivery.reviewer ? `reviewer ${delivery.reviewer}` : task.class === "review" ? "no reviewer" : this.noReviewer(delivery)].filter(Boolean).join("; ");
|
|
1600
1603
|
const plan = planText(task.plan);
|
|
1601
1604
|
const body = [
|
|
1602
1605
|
`Task #${task.id} [${profileTaskLabel(task)}] ${task.title}`,
|
|
@@ -2338,7 +2341,7 @@ export class Tasks {
|
|
|
2338
2341
|
if (rejected.owner) this.d.board.recordOutcome(rejected.owner, rejected.class, false);
|
|
2339
2342
|
this.contradict(rejected);
|
|
2340
2343
|
this.note(rejected, by, "decision", `Task #${rejected.id} changes requested by ${by}: ${rejected.title}\n${note ?? ""}`, withheld);
|
|
2341
|
-
if (rejected.rejections >= ESCALATE_AFTER) {
|
|
2344
|
+
if (rejected.rejections >= (this.d.escalateAfter?.() ?? ESCALATE_AFTER)) {
|
|
2342
2345
|
let moved: Task;
|
|
2343
2346
|
try {
|
|
2344
2347
|
moved = await this.escalate(HUB, rejected.id, `${rejected.rejections} consecutive changes_requested`, "rejections");
|
|
@@ -2415,7 +2418,7 @@ export class Tasks {
|
|
|
2415
2418
|
const acceptance = previous.history.slice(assignment + 1).some(entry => entry.event === "accepted" && entry.by === peer);
|
|
2416
2419
|
const boardEvidence = role === "owner" && acceptance;
|
|
2417
2420
|
const mayHaveReceived = boardEvidence || this.d.bus.lastDeliveredTo(peer).some(matches) || this.d.bus.queueList(peer).some(row => ["dispatching", "accepted", "needs_review", "completed"].includes(row.state) && row.originals.some(matches));
|
|
2418
|
-
for (const env of this.d.bus.
|
|
2421
|
+
for (const env of this.d.bus.pendingEnvelopes(peer)) if (matches(env)) this.d.bus.withdraw(env.id);
|
|
2419
2422
|
if (!mayHaveReceived) return;
|
|
2420
2423
|
const body = role === "owner"
|
|
2421
2424
|
? `Task ${this.publicTitle(next)} is no longer assigned to you${next.owner ? `; its owner is now ${next.owner}` : "; it is unassigned"}. Do not accept or complete it; late calls under your former assignment are refused.`
|
|
@@ -2494,7 +2497,7 @@ export class Tasks {
|
|
|
2494
2497
|
const owner = task.owner === peer;
|
|
2495
2498
|
const weights = this.weights(task.class);
|
|
2496
2499
|
const pii = this.isPii(task);
|
|
2497
|
-
const candidates = owner ?
|
|
2500
|
+
const candidates = owner ? relayCandidates(routing.classes[task.class]?.peers ?? [], pii) : [];
|
|
2498
2501
|
const a = assign(task, this.states(), routing, { ...weights, offRoster: [...(weights.offRoster ?? []), peer], exclude: [peer], excludeOwners: owner && this.plannerPullRecord(task)?.pull ? this.plannerFull(task) : [], candidates, ...(owner ? { waitsFor: this.waitsFor(task) } : task.owner ? { notReviewer: task.owner } : {}) });
|
|
2499
2502
|
return { id: task.id, title: this.publicTitle(task), role: owner ? "owner" : "reviewer", to: (owner ? a.owner : a.reviewer) ?? null };
|
|
2500
2503
|
});
|
|
@@ -2515,7 +2518,7 @@ export class Tasks {
|
|
|
2515
2518
|
if (urgentOnly && !task.signals.includes("urgent")) continue;
|
|
2516
2519
|
if (task.owner === peer && (OPEN.includes(task.state) || roster && task.state === "in_review")) {
|
|
2517
2520
|
const pii = this.isPii(task);
|
|
2518
|
-
const candidates =
|
|
2521
|
+
const candidates = relayCandidates(routing.classes[task.class]?.peers ?? [], pii);
|
|
2519
2522
|
if (this.plannerPullRecord(task)?.pull && !assign(task, this.states(), routing, { candidates, exclude: [...this.excluded(task), peer], excludeOwners: this.plannerFull(task), waitsFor: this.waitsFor(task), ...this.weights(task.class) }).owner) continue;
|
|
2520
2523
|
// Budget keeps its original release/unassign behaviour for ordinary work. Roster changes the state in
|
|
2521
2524
|
// assignOwner's same owner write only after routing has found somebody to take it.
|
|
@@ -2735,4 +2738,4 @@ export class Tasks {
|
|
|
2735
2738
|
}
|
|
2736
2739
|
}
|
|
2737
2740
|
|
|
2738
|
-
export { LOCAL };
|
|
2741
|
+
export { LOCAL } from "./routing.ts";
|
package/src/hub/tooling.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { definitionOf } from "../adapters/registry.ts";
|
|
1
2
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
-
import { accessSync,
|
|
3
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { delimiter,
|
|
5
|
+
import { delimiter, relative, resolve, isAbsolute } from "node:path";
|
|
5
6
|
import { realPath } from "./project.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -15,11 +16,6 @@ export const ADDONS = ["claude-mem", "ponytail", "ripwire", "open-code-review",
|
|
|
15
16
|
export type AddonName = (typeof ADDONS)[number];
|
|
16
17
|
export interface Addon { name: AddonName; version: string | null }
|
|
17
18
|
|
|
18
|
-
/** Plugin names (before the `@marketplace`) that stand for an add-on. */
|
|
19
|
-
// Maps, not objects: a plugin named `constructor` or `toString` must find nothing, not an inherited property.
|
|
20
|
-
const PLUGINS = new Map<string, AddonName>([["claude-mem", "claude-mem"], ["ponytail", "ponytail"], ["open-code-review", "open-code-review"]]);
|
|
21
|
-
/** Claude Code only: the Codex plugin is a Claude plugin named `codex` from the `openai-codex` marketplace. */
|
|
22
|
-
const CLAUDE_ONLY = new Map<string, AddonName>([["codex@openai-codex", "codex-plugin"]]);
|
|
23
19
|
/**
|
|
24
20
|
* Executables any peer with a shell can run, and how each is told apart from another program of the same name: the
|
|
25
21
|
* first one on PATH is asked for its version (bounded, no stdin), so Graphviz's `dot` is never resource admission.
|
|
@@ -32,60 +28,9 @@ const PROBE_MS = 2000, PROBE_BYTES = 4096;
|
|
|
32
28
|
/** `env` supplies CLAUDE_CONFIG_DIR and CODEX_HOME, the runtimes' own config roots (the hub's environment by default). */
|
|
33
29
|
export interface ToolingSources { home?: string; cwd?: string; path?: string; env?: Record<string, string | undefined> }
|
|
34
30
|
|
|
35
|
-
/** A regular file of at most 1 MiB, opened without blocking: a FIFO or device under a project's `.claude/` reads as absent. */
|
|
36
|
-
const text = (file: string): string | undefined => {
|
|
37
|
-
let fd: number | undefined;
|
|
38
|
-
try {
|
|
39
|
-
fd = openSync(file, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
40
|
-
const st = fstatSync(fd);
|
|
41
|
-
if (!st.isFile() || st.size > 1024 * 1024) return undefined;
|
|
42
|
-
return readFileSync(fd, "utf8");
|
|
43
|
-
} catch { return undefined; } finally { if (fd !== undefined) closeSync(fd); }
|
|
44
|
-
};
|
|
45
|
-
const json = (file: string): Record<string, unknown> | undefined => {
|
|
46
|
-
try { const v = JSON.parse(text(file) ?? ""); return v && typeof v === "object" ? v as Record<string, unknown> : undefined; } catch { return undefined; }
|
|
47
|
-
};
|
|
48
31
|
/** A version string as data, never anything longer or stranger. */
|
|
49
32
|
const version = (v: unknown): string | null => typeof v === "string" && /^[0-9A-Za-z.+_-]{1,40}$/.test(v) ? v : null;
|
|
50
33
|
|
|
51
|
-
function claudePlugins(root: string, cwd: string | undefined): Addon[] {
|
|
52
|
-
const enabled = new Map<string, boolean>();
|
|
53
|
-
for (const file of [join(root, "settings.json"), ...(cwd ? [join(cwd, ".claude", "settings.json"), join(cwd, ".claude", "settings.local.json")] : [])]) {
|
|
54
|
-
const map = json(file)?.enabledPlugins;
|
|
55
|
-
if (map && typeof map === "object") for (const [key, on] of Object.entries(map)) enabled.set(key, on === true);
|
|
56
|
-
}
|
|
57
|
-
const installed = (json(join(root, "plugins", "installed_plugins.json"))?.plugins ?? {}) as Record<string, { scope?: string; projectPath?: string; version?: unknown }[]>;
|
|
58
|
-
const out: Addon[] = [];
|
|
59
|
-
for (const [key, on] of enabled) {
|
|
60
|
-
if (!on) continue;
|
|
61
|
-
const name = CLAUDE_ONLY.get(key) ?? PLUGINS.get(key.split("@")[0]!);
|
|
62
|
-
const entries = Array.isArray(installed[key]) ? installed[key]! : [];
|
|
63
|
-
// An install for another project is not this one's.
|
|
64
|
-
const entry = entries.find((e) => e?.scope === "user") ?? entries.find((e) => e?.projectPath === cwd);
|
|
65
|
-
if (name && entry) out.push({ name, version: version(entry.version) });
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function codexPlugins(root: string): Addon[] {
|
|
71
|
-
const config = text(join(root, "config.toml"));
|
|
72
|
-
if (config === undefined) return [];
|
|
73
|
-
const found = new Map<string, string>(), disabled = new Set<string>(); // plugin name -> marketplace
|
|
74
|
-
// `[plugins."name@market"]` with no `enabled = false` below it, and plugins whose hooks Codex was told to trust
|
|
75
|
-
// unless their plugin entry disables them: a trust entry outlives the plugin's own switch.
|
|
76
|
-
for (const m of config.matchAll(/^\[plugins\."([^"@]+)@([^"]+)"\]\s*\n((?:(?!\[)[^\n]*\n?)*)/gm)) if (!/^\s*enabled\s*=\s*false\b/m.test(m[3] ?? "")) found.set(m[1]!, m[2]!); else disabled.add(m[1]! + "@" + m[2]!);
|
|
77
|
-
for (const m of config.matchAll(/^\[hooks\.state\."([^"@]+)@([^":]+):/gm)) if (!disabled.has(m[1]! + "@" + m[2]!)) found.set(m[1]!, m[2]!);
|
|
78
|
-
const out: Addon[] = [];
|
|
79
|
-
for (const [plugin, market] of found) {
|
|
80
|
-
const name = PLUGINS.get(plugin);
|
|
81
|
-
if (!name) continue;
|
|
82
|
-
let versions: string[] = [];
|
|
83
|
-
try { versions = readdirSync(join(root, "plugins", "cache", market, plugin)); } catch { /* no cache: version unknown */ }
|
|
84
|
-
out.push({ name, version: versions.length === 1 ? version(versions[0]) : null });
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
34
|
/** Never throws: a path that cannot be resolved (a project directory that went away) is compared as given. */
|
|
90
35
|
const real = (path: string): string => { try { return realPath(path); } catch { return resolve(path); } };
|
|
91
36
|
const within = (file: string, dir: string): boolean => { const r = relative(dir, file); return r === "" || (!r.startsWith("..") && !isAbsolute(r)); };
|
|
@@ -145,8 +90,7 @@ export async function detectTooling(peer: string, src: ToolingSources = {}): Pro
|
|
|
145
90
|
const found = new Map<AddonName, Addon>();
|
|
146
91
|
const add = (a: Addon) => { if (!found.has(a.name) || (found.get(a.name)!.version === null && a.version !== null)) found.set(a.name, a); };
|
|
147
92
|
const env = src.env ?? process.env;
|
|
148
|
-
|
|
149
|
-
if (peer === "codex") codexPlugins(env.CODEX_HOME?.trim() || join(home, ".codex")).forEach(add);
|
|
93
|
+
definitionOf(peer)?.hooks?.pluginInventory?.({ env, home, cwd: src.cwd }).forEach(add);
|
|
150
94
|
const path = src.path ?? process.env.PATH ?? "";
|
|
151
95
|
for (const [bin, spec] of Object.entries(BINARIES)) {
|
|
152
96
|
const file = onPath(bin, path, src.cwd ?? process.cwd());
|
package/src/hub/ui.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { PEER_PALETTE, peerPaletteCss } from "../ui/peer-palette.ts";
|
|
|
5
5
|
import { PEER_START_RETRY_MS } from "./start-mode.ts";
|
|
6
6
|
import { taskProgress } from "../ui/task-progress.ts";
|
|
7
7
|
import { officeModel } from "../ui/office-model.ts";
|
|
8
|
+
import { OFFICE_PEER_ORDER, ADAPTER_DISPLAY } from "../adapters/registry.ts";
|
|
8
9
|
import { needs } from "../ui/needs.ts";
|
|
9
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
10
11
|
import { readFileSync } from "node:fs";
|
|
@@ -35,10 +36,11 @@ export function startDashboard(options: DashboardOptions) {
|
|
|
35
36
|
// Inject the same pure public-board model used by the console before computing CSP hashes.
|
|
36
37
|
const html = readFileSync(new URL("../ui/index.html", import.meta.url), "utf8")
|
|
37
38
|
.replace("/* BOARD_MODEL */", () => `const boardColumns = ${boardColumns.toString()}; const peerPalette = ${JSON.stringify(PEER_PALETTE)};`)
|
|
39
|
+
.replace("/* ADAPTER_DISPLAY */", () => `const adapterDisplay = ${JSON.stringify(ADAPTER_DISPLAY)};`)
|
|
38
40
|
.replace("/* PEER_PALETTE_CSS */", () => peerPaletteCss())
|
|
39
41
|
.replace("/* TASK_PROGRESS_MODEL */", () => `const taskProgress = ${taskProgress.toString()};`)
|
|
40
42
|
.replace("/* PEER_START_RETRY_MS */", () => `const PEER_START_RETRY_MS = ${PEER_START_RETRY_MS};`)
|
|
41
|
-
.replace("/* OFFICE_MODEL */", () => `const officeModel = ${officeModel.toString()};`)
|
|
43
|
+
.replace("/* OFFICE_MODEL */", () => `const officeModel = ((OFFICE_PEER_ORDER) => ${officeModel.toString()})(${JSON.stringify(OFFICE_PEER_ORDER)});`)
|
|
42
44
|
.replace("/* NEEDS_MODEL */", () => `const needs = ${needs.toString()};`);
|
|
43
45
|
const hashes = (tag: string) => [...html.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "g"))]
|
|
44
46
|
.map((m) => `'sha256-${createHash("sha256").update(m[1]!).digest("base64")}'`).join(" ");
|
package/src/memory/recall.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { ADAPTERS, definitionOf } from "../adapters/registry.ts";
|
|
1
2
|
import type { PeerId } from "../hub/envelope.ts";
|
|
2
3
|
import type { MemoryClient } from "./client.ts";
|
|
3
4
|
import type { View as MemoryView } from "@staix/agent-mem";
|
|
4
5
|
|
|
5
|
-
const PLATFORMS = ["claude", "codex", "kimi", "agent-hub"];
|
|
6
|
-
/** Peers whose own hooks already inject their own platform's context at session start. */
|
|
7
|
-
const OWN_PLATFORM: Record<PeerId, string> = { claude: "claude", codex: "codex" };
|
|
8
6
|
|
|
9
7
|
const HEADER =
|
|
10
8
|
"Shared project memory (claude-mem): recent work by the agents in this project. Reference only, not a request. " +
|
|
@@ -26,11 +24,12 @@ const hasContext = (text: string | undefined): text is string => !!text && text.
|
|
|
26
24
|
const entries = (text: string) => text.slice(Math.max(text.indexOf("\n### "), 0)).trim();
|
|
27
25
|
|
|
28
26
|
export async function recallFor(peer: PeerId, client: MemoryClient, chain: string[], tokens: number): Promise<string | undefined> {
|
|
29
|
-
const
|
|
27
|
+
const hooks = definitionOf(peer)?.hooks;
|
|
28
|
+
const own = hooks?.recallInjected ? hooks.recallPlatform : undefined;
|
|
30
29
|
// Everyone else gets all platforms in one call; claude and codex get the other platforms only.
|
|
31
30
|
const blocks = own
|
|
32
31
|
? await Promise.all(
|
|
33
|
-
|
|
32
|
+
[...ADAPTERS.flatMap(adapter => adapter.hooks?.recallSource ? [adapter.hooks.recallSource] : []), "agent-hub"].filter((p) => p !== own).map(async (p) => {
|
|
34
33
|
const text = await client.contextInject(chain, p);
|
|
35
34
|
return hasContext(text) ? `## from ${p} sessions\n${entries(text)}` : undefined;
|
|
36
35
|
}),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Ported to TypeScript from NVIDIA NeMo Switchyard crates/libsy/src/algorithms/util/tool_signals.rs at c8848511, modified.
|
|
4
4
|
|
|
5
5
|
import type { Conversation, NormalizedToolCall } from './normalize.ts';
|
|
6
|
+
import { definitionOf } from '../../adapters/registry.ts';
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_RECENT_WINDOW = 3;
|
|
8
9
|
export const SOFT = 0.3;
|
|
@@ -133,7 +134,8 @@ function shellClassification(command: string): Semantic {
|
|
|
133
134
|
function semantic(name: string, command?: string, source?: string): Semantic {
|
|
134
135
|
if (source?.toLowerCase() === 'filechange') return 'edit';
|
|
135
136
|
const lower = name.toLowerCase();
|
|
136
|
-
|
|
137
|
+
const native = source === undefined ? undefined : definitionOf(source)?.hooks?.classifyTool?.(name, command);
|
|
138
|
+
if (native !== undefined) return native;
|
|
137
139
|
if (WRITE.has(lower)) return 'write';
|
|
138
140
|
if (EDITOR.has(lower) && command === 'view') return 'read';
|
|
139
141
|
if (EDIT.has(lower)) return 'edit';
|
package/src/pi/launch.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ADAPTERS } from "../adapters/registry.ts";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { peerChildEnv } from "../hub/child-process.ts";
|
|
3
4
|
import type { PiOptions, PiModelDescriptor, PiTuiLaunch } from "../adapters/pi.ts";
|
|
@@ -16,6 +17,6 @@ export function buildPiLaunch(opts: Pick<PiOptions, "stateDir" | "cmd" | "mode"
|
|
|
16
17
|
const env: NodeJS.ProcessEnv = { ...peerChildEnv("pi", inherited), PI_CODING_AGENT_DIR: join(opts.stateDir, "pi"), AGENTHUB_PI_BRIDGE_URL: `http://127.0.0.1:${bridgePort}`, AGENTHUB_PI_BRIDGE_TOKEN: bridgeToken, AGENTHUB_PI_OWNER_TOKEN: bridgeToken, AGENTHUB_PI_RELAY_URL: opts.relay.url, AGENTHUB_PI_RELAY_TOKEN: opts.relay.token, AGENTHUB_PI_MODELS: JSON.stringify(opts.relay.models), AGENTHUB_PI_TOOLS: JSON.stringify(opts.tools), AGENTHUB_PI_MAX_STEPS: String(opts.maxSteps ?? 30) };
|
|
17
18
|
const args = [ ...(opts.mode === "headless" ? ["--mode", "rpc"] : []), "--provider", "agent-hub-local", "--model", opts.model ?? modelFor(opts.backend, opts.relay.models), "--models", opts.relay.models.map((model) => `agent-hub-local/${model.id}`).join(","), "--no-builtin-tools", "--no-skills", "--no-prompt-templates", "--no-extensions", "--extension", extension, ...(opts.preamble ? ["--append-system-prompt", opts.preamble] : []), "--session-dir", join(opts.stateDir, "pi-sessions")];
|
|
18
19
|
if (opts.sessionFile) args.push("--session", opts.sessionFile); else if (opts.sessionId) args.push("--session-id", opts.sessionId);
|
|
19
|
-
const command = opts.cmd ?? ["pi"];
|
|
20
|
+
const command = opts.cmd ?? [ADAPTERS.find(adapter => adapter.start.daemonPort === "pi-rpc")!.cli.command];
|
|
20
21
|
return { cmd: command[0]!, args: [...command.slice(1), ...args], env };
|
|
21
22
|
}
|
package/src/ui/index.html
CHANGED
|
@@ -72,6 +72,7 @@ const dashboardTheme = (() => {
|
|
|
72
72
|
/* TASK_PROGRESS_MODEL */
|
|
73
73
|
/* PEER_START_RETRY_MS */
|
|
74
74
|
/* BOARD_MODEL */
|
|
75
|
+
/* ADAPTER_DISPLAY */
|
|
75
76
|
/* NEEDS_MODEL */
|
|
76
77
|
/* OFFICE_MODEL */
|
|
77
78
|
const officeView = (() => {
|
|
@@ -284,7 +285,7 @@ function renderTaskStage(stage) {
|
|
|
284
285
|
row.append(el('span', ` / Stage ${stage.stage}/4: ${stage.label}`)); return row;
|
|
285
286
|
}
|
|
286
287
|
function renderApprovals(permissions) {
|
|
287
|
-
const terminalWaiting = permissions.filter((permission) => permission.peer
|
|
288
|
+
const terminalWaiting = permissions.filter((permission) => adapterDisplay[permission.peer]?.terminalApproval).length;
|
|
288
289
|
$('permission-count').textContent = permissions.length + ' waiting' + (terminalWaiting ? ', ' + terminalWaiting + ' Pi/local: answer in the console or ahub tail / ahub permit' : '');
|
|
289
290
|
update('permissions', permissions, (target) => {
|
|
290
291
|
if (!permissions.length) return empty(target, 'No approvals waiting.');
|
|
@@ -783,7 +784,7 @@ function render(snapshot) {
|
|
|
783
784
|
if (peer.attached === false) row.append(el('p', 'Disconnected recipient; work is retained.', 'meta'));
|
|
784
785
|
if (peer.toolsOnly) row.append(el('p', peer.toolsOnly, 'meta'));
|
|
785
786
|
if (peer.permissionNote) row.append(el('p', peer.permissionNote, 'meta'));
|
|
786
|
-
if (peer.pullOnly && id
|
|
787
|
+
if (peer.pullOnly && adapterDisplay[id]?.pullOnlyRead) row.append(el('p', 'Last hub_inbox read: ' + (typeof peer.lastInboxAt === 'number' ? Math.max(0, Math.floor((Date.now() - peer.lastInboxAt) / 1000)) + 's ago' : 'never'), 'meta'));
|
|
787
788
|
if (peer.paused) row.append(el('p', peer.paused, 'meta'));
|
|
788
789
|
if (peer.requestedModel) row.append(el('p', 'Requested: ' + peer.requestedModel, 'meta'));
|
|
789
790
|
for (const backend of peer.backends || []) {
|
package/src/ui/office-model.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { OFFICE_PEER_ORDER } from "../adapters/registry.ts";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Pure office scene model for the dashboard (#264, phase 1: the static Flat view).
|
|
3
5
|
* Self-contained so `src/hub/ui.ts` injects it into the page by toString(), exactly like
|
|
@@ -178,7 +180,7 @@ export interface OfficeScene {
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
export function officeModel(snapshot: OfficeSnapshotInput, previous: OfficeScene | null, options: { cols?: number; now?: number }): OfficeScene {
|
|
181
|
-
const FIXED =
|
|
183
|
+
const FIXED = OFFICE_PEER_ORDER;
|
|
182
184
|
const KNOWN = ["proposed", "in_progress", "in_review", "changes_requested", "approved", "cancelled"];
|
|
183
185
|
const GLYPHS: Record<string, string> = { idle: "○", busy: "●", offline: "×", paused: "‖", waiting: "⚑", unknown: "?" };
|
|
184
186
|
const LABELS: Record<string, string> = { idle: "idle", busy: "busy", offline: "offline", paused: "paused", waiting: "waiting on approval" };
|
package/src/ui/peer-palette.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
import { definitionOf, OFFICE_PEER_ORDER } from "../adapters/registry.ts";
|
|
2
|
+
import type { NATIVE_DISPLAYS } from "../adapters/definitions/native-display.ts";
|
|
3
|
+
|
|
4
|
+
type NativePalette = { [Kind in keyof typeof NATIVE_DISPLAYS]: {
|
|
5
|
+
tone: (typeof NATIVE_DISPLAYS)[Kind]["palette"]["tone"];
|
|
6
|
+
ansi: (typeof NATIVE_DISPLAYS)[Kind]["palette"]["ansi"];
|
|
7
|
+
css: (typeof NATIVE_DISPLAYS)[Kind]["palette"]["fg"];
|
|
8
|
+
} };
|
|
9
|
+
const other = { tone: "muted", ansi: "\x1b[90m", css: "#8a8f99" } as const;
|
|
10
|
+
const nativePalette = Object.fromEntries(OFFICE_PEER_ORDER.map(kind => {
|
|
11
|
+
const palette = definitionOf(kind)!.display.palette;
|
|
12
|
+
return [kind, { tone: palette.tone ?? other.tone, ansi: palette.ansi ?? other.ansi, css: palette.fg }];
|
|
13
|
+
})) as NativePalette;
|
|
14
|
+
|
|
1
15
|
/** Stable peer identity colors shared by terminal board/status and the dashboard (#263). */
|
|
2
16
|
export const PEER_PALETTE = Object.freeze({
|
|
3
|
-
|
|
4
|
-
codex: { tone: "peerCodex", ansi: "\x1b[96m", css: "#22b8cf" },
|
|
5
|
-
kimi: { tone: "taskKeyword", ansi: "\x1b[35m", css: "#bc73ca" },
|
|
6
|
-
pi: { tone: "attention", ansi: "\x1b[33m", css: "#c79936" },
|
|
7
|
-
local: { tone: "success", ansi: "\x1b[32m", css: "#4caa77" },
|
|
17
|
+
...nativePalette,
|
|
8
18
|
hub: { tone: "info", ansi: "\x1b[36m", css: "#489da8" },
|
|
9
|
-
other
|
|
19
|
+
other,
|
|
10
20
|
} as const);
|
|
11
21
|
export function peerStyle(peer: string): (typeof PEER_PALETTE)[keyof typeof PEER_PALETTE] {
|
|
12
22
|
return PEER_PALETTE[Object.hasOwn(PEER_PALETTE, peer) ? peer as keyof typeof PEER_PALETTE : "other"];
|
package/templates/routing.toml
CHANGED
|
@@ -46,8 +46,8 @@ target = "dsv4f"
|
|
|
46
46
|
# confirmations = 2
|
|
47
47
|
|
|
48
48
|
# L1: task assignment. `peers` is the preference order among attached, unpaused peers (idle before busy).
|
|
49
|
-
# `route` / `fixed_model` apply when the task lands on the local worker.
|
|
50
|
-
# task to the next attached peer in `escalate_to`.
|
|
49
|
+
# `route` / `fixed_model` apply when the task lands on the local worker. `tasks.escalate_after` consecutive
|
|
50
|
+
# changes_requested (default 3, in .agenthub/config.json) move the task to the next attached peer in `escalate_to`.
|
|
51
51
|
|
|
52
52
|
[signals]
|
|
53
53
|
pii_patterns = ["\\b\\d{6}-\\d{7}\\b"] # Korean resident registration numbers; add your own, e.g. "@your-org\\.example" for staff mail addresses
|