@staix/agent-hub 0.12.36 → 0.12.38
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 +22 -0
- package/README.md +18 -11
- package/docs/agent-notes/adapters.md +19 -6
- package/docs/agent-notes/budget.md +2 -1
- package/docs/agent-notes/bus.md +3 -2
- package/docs/agent-notes/daemon.md +9 -3
- package/docs/agent-notes/tasks.md +8 -4
- package/docs/agent-notes/tests.md +2 -2
- package/docs/memory.md +3 -1
- package/docs/operations.md +57 -10
- package/docs/quickstart.md +15 -7
- package/docs/sdlc.md +29 -3
- package/docs/security.md +10 -2
- package/docs/smoke.md +38 -2
- package/docs/specs/2026-09-19-agent-hub-design.md +58 -4
- package/docs/verification/2026-10-19-0.12.37.md +60 -0
- package/docs/verification/2026-10-19-agy-p1-t0.md +55 -0
- package/docs/verification/2026-10-19-bus-defer-reset.md +9 -0
- package/docs/verification/2026-10-19-jcode-0.98.1.md +30 -0
- package/docs/verification/2026-10-19-kimi-agy-reviewer-isolation.md +220 -0
- package/docs/verification/2026-10-19-reviewer-isolation.md +119 -0
- package/docs/verification/2026-10-20-0.12.38.md +56 -0
- package/docs/verified.json +27 -15
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +5627 -3670
- package/src/adapters/acp.ts +3 -2
- package/src/adapters/agy/definition.ts +61 -0
- package/src/adapters/agy/entry.ts +76 -0
- package/src/adapters/agy/hooks.ts +81 -0
- package/src/adapters/claude-channel.ts +1 -0
- package/src/adapters/claude-print.ts +18 -156
- package/src/adapters/codex-review-isolation.ts +290 -0
- package/src/adapters/definitions/builtin-metadata.ts +9 -6
- package/src/adapters/definitions/native-cli-entry-claude.ts +3 -0
- package/src/adapters/definitions/native-cli-entry-codex.ts +5 -1
- package/src/adapters/definitions/native-cli-entry-jcode.ts +2 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +12 -4
- package/src/adapters/definitions/native-cli-entry-local.ts +3 -0
- package/src/adapters/definitions/native-cli-entry-pi.ts +7 -1
- package/src/adapters/definitions/native-cli-entry-qwen.ts +3 -0
- package/src/adapters/definitions/native-cli-entry.ts +2 -0
- package/src/adapters/definitions/native-cli-retained.ts +15 -0
- package/src/adapters/definitions/native-factories.ts +17 -0
- package/src/adapters/definitions/native-policy-aux.ts +1 -1
- package/src/adapters/definitions/qwen.ts +1 -1
- package/src/adapters/definitions/reviewer-variants.ts +33 -0
- package/src/adapters/jcode.ts +20 -5
- package/src/adapters/kimi-review.ts +161 -0
- package/src/adapters/registry-types.ts +36 -4
- package/src/adapters/registry.ts +28 -7
- package/src/adapters/reviewer-mcp.ts +85 -0
- package/src/adapters/reviewer-print.ts +382 -0
- package/src/cli/agy-mcp.ts +370 -0
- package/src/cli/console-state.ts +6 -0
- package/src/cli/console.ts +2 -0
- package/src/cli/facts-hook.ts +15 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/identity.ts +1 -1
- package/src/cli/init.ts +7 -0
- package/src/cli/launch.ts +3 -1
- package/src/cli/main.ts +20 -0
- package/src/cli/memory.ts +13 -12
- package/src/cli/output.ts +5 -0
- package/src/hub/board.ts +34 -6
- package/src/hub/bus.ts +17 -6
- package/src/hub/config-apply.ts +3 -2
- package/src/hub/control-client.ts +7 -1
- package/src/hub/daemon.ts +266 -46
- package/src/hub/envelope.ts +1 -1
- package/src/hub/host-executable.ts +1 -1
- package/src/hub/hub-tools.ts +5 -0
- package/src/hub/instances.ts +13 -3
- package/src/hub/memory-profile.ts +67 -11
- package/src/hub/native-hook-launch.ts +37 -0
- package/src/hub/peer-catalog.ts +2 -3
- package/src/hub/planning.ts +2 -0
- package/src/hub/release-admission.ts +64 -0
- package/src/hub/reviewer-pool-config.ts +11 -0
- package/src/hub/reviewer-pool.ts +76 -0
- package/src/hub/routing.ts +45 -3
- package/src/hub/sdlc-release.ts +113 -0
- package/src/hub/tasks.ts +261 -36
- package/src/ui/index.html +6 -1
- package/templates/AGENTS.block.md +1 -1
package/src/cli/output.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { formatReport, formatTaskReport, type Report, type TaskReport } from "..
|
|
|
9
9
|
export interface DoctorCheck { section: "Tools" | "Hub" | "Config" | "Models" | "Memory"; level: "ok" | "warn" | "fail" | "unknown"; name: string; detail: string }
|
|
10
10
|
export interface StatusOutput {
|
|
11
11
|
pendingRestart?: readonly { key: string; reason: string }[];
|
|
12
|
+
retainedConfig?: readonly { key: string; reason: string }[];
|
|
12
13
|
taskStalls?: readonly { task: number; cause: { kind: string; peer?: string; dependency?: number; code: string } }[];
|
|
13
14
|
pendingTaskOffers?: readonly { task: number; peer: string }[];
|
|
14
15
|
isolatedChecks?: { pending?: number; orphans?: string[]; unknownReceipts?: readonly string[]; maintenanceUnavailable?: boolean };
|
|
@@ -191,6 +192,10 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
191
192
|
const out = lines(`agent-hub${data.version ? ` ${data.version}` : ""} pid ${data.pid ?? "unknown"} control 127.0.0.1:${data.controlPort ?? "unknown"}`, columns, "info");
|
|
192
193
|
if (data.cwd) out.push(...lines(data.cwd, columns));
|
|
193
194
|
for (const pending of data.pendingRestart ?? []) out.push(...detail("pending restart", `${pending.key}: ${pending.reason}`, columns, "attention"));
|
|
195
|
+
if (data.retainedConfig?.length) {
|
|
196
|
+
out.push(...detail("config retained", `${data.retainedConfig.map(row => row.key).join(", ")}; run ahub settings reload`, columns, "attention"));
|
|
197
|
+
for (const pending of data.retainedConfig) out.push(...detail("retained reason", `${pending.key}: ${pending.reason}`, columns, "attention"));
|
|
198
|
+
}
|
|
194
199
|
for (const stall of data.taskStalls ?? []) out.push(...detail(`task #${stall.task} stalled`, `${stall.cause.kind}${stall.cause.peer ? ` ${stall.cause.peer}` : ""}${stall.cause.dependency ? ` #${stall.cause.dependency}` : ""}: ${stall.cause.code}; configure the peer, ahub task assign ${stall.task} <peer>, or ahub task cancel ${stall.task} --reason <text>`, columns, "attention"));
|
|
195
200
|
for (const offer of data.pendingTaskOffers ?? []) out.push(...detail("task offer", `open ${offer.peer}: task #${offer.task} is waiting for it`, columns, "attention"));
|
|
196
201
|
if (data.memoryProfile) {
|
package/src/hub/board.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReviewerPoolReason } from "./reviewer-pool.ts";
|
|
1
2
|
import type { GitHubSource } from "./github-import.ts";
|
|
2
3
|
import { Database } from "bun:sqlite";
|
|
3
4
|
import { planningPatch, type PlanningFields, type Release, type ReleaseState } from "./planning.ts";
|
|
@@ -56,8 +57,20 @@ export interface HistoryEntry {
|
|
|
56
57
|
reviewCost?: ReviewCost;
|
|
57
58
|
/** #223 R4: on a verdict, whether the reviewer's context was new for it; set by the hub, never by the reviewer. */
|
|
58
59
|
reviewContext?: "fresh" | "continuing";
|
|
60
|
+
reviewerFamily?: string;
|
|
61
|
+
reviewerReason?: ReviewerPoolReason;
|
|
62
|
+
reviewBlocked?: true;
|
|
63
|
+
reviewerFailure?: { peer: string; head?: string; cause: "start_failed" | "quota" | "no_verdict" };
|
|
59
64
|
planning?: { revision: number; fields: (keyof PlanningFields)[]; previous: PlanningFields; pull?: true; split?: { children: { id: number; revision: number; history: number }[] } };
|
|
60
65
|
}
|
|
66
|
+
export type ReviewerHistory = Pick<HistoryEntry, "reviewerFamily" | "reviewerReason" | "reviewBlocked" | "reviewerFailure">;
|
|
67
|
+
function reviewerHistory(extra: ReviewerHistory): ReviewerHistory {
|
|
68
|
+
const id = (value: unknown) => typeof value === "string" && /^[a-z][a-z0-9-]{0,63}$/.test(value);
|
|
69
|
+
if (extra.reviewerFamily !== undefined && !id(extra.reviewerFamily) || extra.reviewerReason !== undefined && !["rotation", "implementer skip", "unavailable skip", "same-family fallback"].includes(extra.reviewerReason) || extra.reviewBlocked !== undefined && extra.reviewBlocked !== true) throw new Error("invalid reviewer metadata");
|
|
70
|
+
const failure = extra.reviewerFailure;
|
|
71
|
+
if (failure && (!id(failure.peer) || !["start_failed", "quota", "no_verdict"].includes(failure.cause) || failure.head !== undefined && (typeof failure.head !== "string" || !failure.head.trim() || failure.head.length > 300 || Buffer.byteLength(failure.head) > 1200 || /\p{Cc}/u.test(failure.head)))) throw new Error("invalid reviewer failure metadata");
|
|
72
|
+
return { ...(extra.reviewerFamily ? { reviewerFamily: extra.reviewerFamily } : {}), ...(extra.reviewerReason ? { reviewerReason: extra.reviewerReason } : {}), ...(extra.reviewBlocked ? { reviewBlocked: true } : {}), ...(failure ? { reviewerFailure: { peer: failure.peer, cause: failure.cause, ...(failure.head ? { head: failure.head } : {}) } } : {}) };
|
|
73
|
+
}
|
|
61
74
|
export interface Task extends PlanningFields {
|
|
62
75
|
offer?: TaskOffer;
|
|
63
76
|
cancelled?: TaskCancellation;
|
|
@@ -209,6 +222,7 @@ export class Board {
|
|
|
209
222
|
release_order INTEGER NOT NULL, state TEXT NOT NULL DEFAULT 'planning', planned_start INTEGER, cut INTEGER, ship INTEGER,
|
|
210
223
|
activated_at INTEGER, frozen_at INTEGER, shipped_at INTEGER, created INTEGER NOT NULL, updated INTEGER NOT NULL,
|
|
211
224
|
revision INTEGER NOT NULL DEFAULT 0)`);
|
|
225
|
+
if (!(this.db.query("PRAGMA table_info(releases)").all() as { name: string }[]).some(row => row.name === "train")) this.db.run("ALTER TABLE releases ADD COLUMN train TEXT");
|
|
212
226
|
|
|
213
227
|
}
|
|
214
228
|
|
|
@@ -288,7 +302,7 @@ export class Board {
|
|
|
288
302
|
|
|
289
303
|
/** The only way a task changes. Validates the move, records who did what, returns the new row. */
|
|
290
304
|
/** A `plan` in the patch replaces the old one whole: a new plan is the owner's current intent, not an addition. */
|
|
291
|
-
update(id: number, by: PeerId, event: string, patch: Partial<Pick<Task, "state" | "owner" | "reviewer" | "reserved" | "refs" | "plan" | "rejections" | "signals" | "sdlc" | "role" | "cancelled">>, note?: string, extra: Pick<HistoryEntry, "profile" | "sweep" | "reason" | "from" | "withheld" | "reviewCost" | "reviewContext"> = {}, expectedFlowRevision?: number): Task {
|
|
305
|
+
update(id: number, by: PeerId, event: string, patch: Partial<Pick<Task, "state" | "owner" | "reviewer" | "reserved" | "refs" | "plan" | "rejections" | "signals" | "sdlc" | "role" | "cancelled">>, note?: string, extra: Pick<HistoryEntry, "profile" | "sweep" | "reason" | "from" | "withheld" | "reviewCost" | "reviewContext" | keyof ReviewerHistory> = {}, expectedFlowRevision?: number): Task {
|
|
292
306
|
const task = this.get(id);
|
|
293
307
|
if (!task) throw new Error(`no task #${id}`);
|
|
294
308
|
if (expectedFlowRevision !== undefined && (!Number.isSafeInteger(expectedFlowRevision) || expectedFlowRevision < 0 || task.history.length !== expectedFlowRevision)) throw new Error("task flow revision changed");
|
|
@@ -303,7 +317,8 @@ export class Board {
|
|
|
303
317
|
if (patch.state === "backlog" && task.owner) throw new Error(`task #${id}: owned work cannot return to backlog`);
|
|
304
318
|
}
|
|
305
319
|
if (patch.state === "backlog" && taskHasStarted(task) && !untouchedReturn) throw new Error(`task #${id}: started work cannot return to backlog`);
|
|
306
|
-
const
|
|
320
|
+
const reviewMetadata = reviewerHistory(extra);
|
|
321
|
+
const history = [...task.history, { at: Date.now(), by, event, ...reviewMetadata, ...(note ? { note } : {}), ...(patch.cancelled?.supersededBy ? { supersededBy: patch.cancelled.supersededBy } : {}), ...("owner" in patch ? { owner: next.owner } : {}), ...(extra.profile ? { profile: extra.profile } : {}), ...(extra.sweep ? { sweep: extra.sweep } : {}), ...(extra.reason && TASK_MOVE_REASONS.includes(extra.reason) ? { reason: extra.reason } : {}), ...(extra.from ? { from: extra.from } : {}), ...(extra.withheld && note ? { withheld: true as const } : {}), ...(extra.reviewContext === "fresh" || extra.reviewContext === "continuing" ? { reviewContext: extra.reviewContext } : {}), ...(extra.reviewCost ? { reviewCost: extra.reviewCost } : {}) }];
|
|
307
322
|
this.taskWrite(() => this.db
|
|
308
323
|
.query("UPDATE tasks SET state = ?, owner = ?, reviewer = ?, reserved = ?, refs = ?, plan = ?, rejections = ?, signals = ?, history = ?, sdlc = ?, task_role = ?, cancelled = ?, updated = ? WHERE id = ?")
|
|
309
324
|
.run(next.state, next.owner, next.reviewer, next.reserved ?? null, JSON.stringify(next.refs), JSON.stringify(next.plan ?? {}), next.rejections, JSON.stringify(next.signals), JSON.stringify(history), next.sdlc ? JSON.stringify(next.sdlc) : null, next.role ?? null, next.cancelled ? JSON.stringify(next.cancelled) : null, Date.now(), id));
|
|
@@ -402,20 +417,33 @@ export class Board {
|
|
|
402
417
|
releases(): Release[] {
|
|
403
418
|
return (this.db.query("SELECT * FROM releases ORDER BY release_order,id").all() as Record<string, unknown>[]).map(parseRelease);
|
|
404
419
|
}
|
|
405
|
-
updateRelease(id: number, patch: Partial<Pick<Release, "state" | "plannedStart" | "cut" | "ship">>, revision: number): Release {
|
|
420
|
+
updateRelease(id: number, patch: Partial<Pick<Release, "state" | "plannedStart" | "cut" | "ship" | "train">>, revision: number): Release {
|
|
406
421
|
const release = this.release(id);
|
|
407
422
|
if (!release || release.revision !== revision) throw new Error("release revision changed");
|
|
408
423
|
const next = { ...release, ...patch }, now = Date.now();
|
|
409
424
|
const moves: Record<ReleaseState, ReleaseState[]> = { planning: ["active", "dropped"], active: ["frozen", "dropped"], frozen: ["shipped", "dropped"], shipped: [], dropped: [] };
|
|
410
425
|
if (next.state !== release.state && !moves[release.state].includes(next.state)) throw new Error("invalid release transition");
|
|
426
|
+
if (patch.train && next.state !== release.state && ["frozen", "shipped"].includes(next.state)) {
|
|
427
|
+
const event = next.train?.history.at(-1);
|
|
428
|
+
if (event?.event === (next.state === "frozen" ? "cut" : "shipped")) event.at = now;
|
|
429
|
+
}
|
|
411
430
|
const dates = [next.plannedStart, next.cut, next.ship].filter((date): date is number => date !== null);
|
|
412
431
|
if (dates.some(date => !Number.isSafeInteger(date) || date <= 0 || date > 8_640_000_000_000_000) || dates.some((date, i) => i > 0 && dates[i-1]! > date)) throw new Error("invalid release planned dates");
|
|
413
|
-
const result = this.db.query("UPDATE releases SET state=?,planned_start=?,cut=?,ship=?,activated_at=?,frozen_at=?,shipped_at=?,updated=?,revision=revision+1 WHERE id=? AND revision=?")
|
|
414
|
-
.run(next.state, next.plannedStart, next.cut, next.ship, release.activatedAt ?? (next.state === "active" ? now : null), release.frozenAt ?? (next.state === "frozen" ? now : null), release.shippedAt ?? (next.state === "shipped" ? now : null), now, id, revision);
|
|
432
|
+
const result = this.db.query("UPDATE releases SET state=?,planned_start=?,cut=?,ship=?,activated_at=?,frozen_at=?,shipped_at=?,updated=?,train=?,revision=revision+1 WHERE id=? AND revision=?")
|
|
433
|
+
.run(next.state, next.plannedStart, next.cut, next.ship, release.activatedAt ?? (next.state === "active" ? now : null), release.frozenAt ?? (next.state === "frozen" ? now : null), release.shippedAt ?? (next.state === "shipped" ? now : null), now, next.train === undefined ? null : JSON.stringify(next.train), id, revision);
|
|
415
434
|
if (result.changes !== 1) throw new Error("release revision changed");
|
|
416
435
|
return this.release(id)!;
|
|
417
436
|
}
|
|
418
437
|
|
|
438
|
+
/** Cross-connection scope changes cannot race the final freeze write. */
|
|
439
|
+
freezeRelease(id: number, revision: number, scope: Task[], train: NonNullable<Release["train"]>): Release {
|
|
440
|
+
return this.db.transaction(() => {
|
|
441
|
+
const release = this.release(id);
|
|
442
|
+
if (!release || release.revision !== revision || release.state !== "active" || this.releases().some(row => row.id !== id && ["active", "frozen"].includes(row.state)) || JSON.stringify(this.list().filter(task => task.release === id)) !== JSON.stringify(scope)) throw new Error("release scope changed before freeze");
|
|
443
|
+
return this.updateRelease(id, { state: "frozen", train }, revision);
|
|
444
|
+
})();
|
|
445
|
+
}
|
|
446
|
+
|
|
419
447
|
recordOutcome(peer: PeerId, cls: TaskClass, ok: boolean, at = Date.now()): void {
|
|
420
448
|
this.db.query("INSERT INTO outcomes (peer, class, ok, at) VALUES (?, ?, ?, ?)").run(peer, cls, ok ? 1 : 0, at);
|
|
421
449
|
this.db.query("DELETE FROM outcomes WHERE at < ?").run(at - OUTCOMES_KEPT_MS); // nothing older is read
|
|
@@ -474,7 +502,7 @@ function parse(row: Record<string, unknown>): Task {
|
|
|
474
502
|
}
|
|
475
503
|
|
|
476
504
|
function parseRelease(row: Record<string, unknown>): Release {
|
|
477
|
-
return { id: Number(row.id), name: String(row.name), goal: String(row.goal), order: Number(row.release_order), state: row.state as ReleaseState,
|
|
505
|
+
return { ...(row.train == null ? {} : { train: JSON.parse(String(row.train)) }), id: Number(row.id), name: String(row.name), goal: String(row.goal), order: Number(row.release_order), state: row.state as ReleaseState,
|
|
478
506
|
plannedStart: row.planned_start as number | null, cut: row.cut as number | null, ship: row.ship as number | null,
|
|
479
507
|
activatedAt: row.activated_at as number | null, frozenAt: row.frozen_at as number | null, shippedAt: row.shipped_at as number | null,
|
|
480
508
|
created: Number(row.created), updated: Number(row.updated), revision: Number(row.revision) };
|
package/src/hub/bus.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { PreEffectToolRefusal } from "./tool-refusal.ts";
|
|
|
7
7
|
|
|
8
8
|
export type { DeliveryReceipt } from "./peers.ts";
|
|
9
9
|
|
|
10
|
+
export interface PublishResult {
|
|
11
|
+
targets: PeerId[];
|
|
12
|
+
dropped?: "hop" | "fyi";
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export type BusEvent =
|
|
11
16
|
| { t: "envelope"; env: Envelope; dropped?: "hop" | "fyi" }
|
|
12
17
|
| { t: "overflow"; env: Envelope; peer: PeerId }
|
|
@@ -294,6 +299,7 @@ export class Bus {
|
|
|
294
299
|
// Persist each deferred -> queued transition while deliveries are still fenced. A failed checkpoint
|
|
295
300
|
// leaves the hold and the original pending head intact; a crash after it resumes from the journal.
|
|
296
301
|
this.flushDeferredHub();
|
|
302
|
+
this.deferHub = false;
|
|
297
303
|
this.recoveryHeld = false;
|
|
298
304
|
this.onQueues?.();
|
|
299
305
|
for (const id of this.peers.keys()) void this.drain(id);
|
|
@@ -698,8 +704,13 @@ export class Bus {
|
|
|
698
704
|
|
|
699
705
|
/** Returns the peers the envelope was accepted for, including durable deferred notices. */
|
|
700
706
|
publish(env: Envelope): PeerId[] {
|
|
707
|
+
return this.publishResult(env).targets;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Publication outcome distinguishes a policy drop from an empty audience. */
|
|
711
|
+
publishResult(env: Envelope): PublishResult {
|
|
701
712
|
if (this.storageError) throw new Error("delivery journal unavailable");
|
|
702
|
-
if (this.seen.has(env.id) || this.deferredHub.has(env.id)) return [];
|
|
713
|
+
if (this.seen.has(env.id) || this.deferredHub.has(env.id)) return { targets: [] };
|
|
703
714
|
if (this.recoveryHeld && (this.deferHub || this.replayingHub) && env.from === HUB && env.priority !== "fyi" && env.hop <= MAX_HOP) {
|
|
704
715
|
if (this.deferredHub.size >= MAX_DEFERRED_HUB) throw new RecoveryDeferredHubError();
|
|
705
716
|
const targets = this.audience(env);
|
|
@@ -707,21 +718,21 @@ export class Bus {
|
|
|
707
718
|
this.deferredHub.set(copy.id, copy);
|
|
708
719
|
try { this.persist(); }
|
|
709
720
|
catch (error) { this.deferredHub.delete(copy.id); throw error; }
|
|
710
|
-
return targets;
|
|
721
|
+
return { targets };
|
|
711
722
|
}
|
|
712
723
|
return this.publishNow(env);
|
|
713
724
|
}
|
|
714
725
|
|
|
715
|
-
private publishNow(env: Envelope):
|
|
726
|
+
private publishNow(env: Envelope): PublishResult {
|
|
716
727
|
if (this.storageError) throw new Error("delivery journal unavailable");
|
|
717
|
-
if (this.seen.has(env.id)) return [];
|
|
728
|
+
if (this.seen.has(env.id)) return { targets: [] };
|
|
718
729
|
this.seen.set(env.id, env);
|
|
719
730
|
if (this.seen.size > SEEN_CAP) this.seen.delete(this.seen.keys().next().value as string);
|
|
720
731
|
|
|
721
732
|
// fyi is for the record (console now, task board from M4), never worth an agent's turn.
|
|
722
733
|
const dropped = env.hop > MAX_HOP ? "hop" : env.priority === "fyi" ? "fyi" : undefined;
|
|
723
734
|
this.emit({ t: "envelope", env, ...(dropped ? { dropped } : {}) });
|
|
724
|
-
if (dropped) { this.persist(); return []; }
|
|
735
|
+
if (dropped) { this.persist(); return { targets: [], dropped }; }
|
|
725
736
|
|
|
726
737
|
const hushed = this.hushed(env);
|
|
727
738
|
if (hushed.length) this.emit({ t: "quiet", env, peers: hushed.map((h) => h.peer), reason: hushed[0]!.reason });
|
|
@@ -756,7 +767,7 @@ export class Bus {
|
|
|
756
767
|
}
|
|
757
768
|
const offRoster = targets.filter(id => !this.isRostered(id));
|
|
758
769
|
if (offRoster.length) this.emit({ t: "off_roster", env, peers: offRoster });
|
|
759
|
-
return targets;
|
|
770
|
+
return { targets };
|
|
760
771
|
}
|
|
761
772
|
|
|
762
773
|
isRostered(peer: PeerId): boolean { return this.opts.rostered?.(peer) ?? true; }
|
package/src/hub/config-apply.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { reviewerPoolConfig } from "./reviewer-pool-config.ts";
|
|
1
2
|
import type { HubConfig } from "./daemon.ts";
|
|
2
3
|
import { ALL_MACHINE_LOCAL, configRepositoryPresent, configTracked } from "./config-trust.ts";
|
|
3
|
-
import { ADAPTERS, SUPPORTED_PEERS, PLANNER_PEERS, START_MODE_PEERS } from "../adapters/registry.ts";
|
|
4
|
+
import { ADAPTERS, DEFAULT_REVIEW_POOL, REVIEW_POOL_KINDS, SUPPORTED_PEERS, PLANNER_PEERS, START_MODE_PEERS } from "../adapters/registry.ts";
|
|
4
5
|
import { CLASSES } from "./board.ts";
|
|
5
6
|
import { permissionDefaults } from "./permission-mode.ts";
|
|
6
7
|
import { peerStartConfig, terminalTemplate } from "./start-mode.ts";
|
|
@@ -138,7 +139,7 @@ const schema: Record<string, Check> = {
|
|
|
138
139
|
peers: (v, k) => { map(block({ start_mode: words(["tui", "headless"]) }), START_MODE_PEERS)(v, k); peerStartConfig(v); },
|
|
139
140
|
terminal: (v, k) => { block({ open: strings })(v, k); terminalTemplate(object(v, k).open); },
|
|
140
141
|
limits: block({ sender_per_min: number(0), pair_per_min: number(0), important_per_hour: number(0), repeat_window_s: number(0) }),
|
|
141
|
-
review: block({ adaptive: bool, min_reviews: number(1, Number.MAX_SAFE_INTEGER, true) }), recovery: block({ auto_resume_after_crash: bool }),
|
|
142
|
+
review: block({ adaptive: bool, min_reviews: number(1, Number.MAX_SAFE_INTEGER, true), pool: v => { reviewerPoolConfig({ pool: v }, DEFAULT_REVIEW_POOL, REVIEW_POOL_KINDS); }, rerotate: bool }), recovery: block({ auto_resume_after_crash: bool }),
|
|
142
143
|
capabilities: map(strings), coordination: words(["advisory", "turn-free"]), experiments: block({ stale_notices: words(["drop", "deliver"]) }),
|
|
143
144
|
};
|
|
144
145
|
export function validateRawConfig(raw: unknown, declarations?: InstancesConfig): asserts raw is Record<string, unknown> {
|
|
@@ -8,7 +8,7 @@ export function stateDirFor(cwd: string): string {
|
|
|
8
8
|
|
|
9
9
|
/** Control WS wire version. 2 = `deliver` carries `envs` (digests); 3 = `tools` role and task messages; 4 = budget messages and `hub_checkpoint`; 5 = `ask`; 6 = console-only `ui` session bootstrap; 8 = controlled recovery; 9 = Pi bridge metadata; 10 = durable delivery receipts; 11 = queue hold diagnostics; 12 = generation-bound channel settlement and execution budgets; 13 = turn-free facts (`facts` with tool, session and transcript binding, `silenced`) and per-recipient send results (issue #108). 14 = native context-window status and session-bound context checkpoint request ids (issue #185). 15 = conductor tools and permission deadlines/closed events (issues #190, #194). 16 = a Claude session without channel evidence: hello `channel: false` and `inbox` (issue #205). The plugin is installed apart from the daemon, so they can drift. */
|
|
10
10
|
export const PROTOCOL = 16;
|
|
11
|
-
export const DAEMON_FEATURES = ["task_cancel", "recovery_deferred_hub", "peer_instances"] as const;
|
|
11
|
+
export const DAEMON_FEATURES = ["native_hook_launch", "task_cancel", "recovery_deferred_hub", "peer_instances", "release_trains", "reviewer_pool"] as const;
|
|
12
12
|
/** Protocols a current coordinator may authenticate while upgrading a running source. */
|
|
13
13
|
export const RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, 15, PROTOCOL] as const;
|
|
14
14
|
|
|
@@ -32,6 +32,8 @@ export interface ControlDescriptor {
|
|
|
32
32
|
protocol?: number;
|
|
33
33
|
pid?: number;
|
|
34
34
|
features?: readonly string[];
|
|
35
|
+
/** The daemon's accepted native start modes, used by launchers when the file value is retained. */
|
|
36
|
+
startModes?: Readonly<Record<string, "tui" | "headless">>;
|
|
35
37
|
/** #226: the daemon's processSignature, written into status.json only (not the status reply); absent before 0.12.21. */
|
|
36
38
|
pidSignature?: string;
|
|
37
39
|
}
|
|
@@ -42,8 +44,12 @@ export function readControl(stateDir: string): ControlDescriptor | undefined {
|
|
|
42
44
|
const status = JSON.parse(readFileSync(join(stateDir, "status.json"), "utf8"));
|
|
43
45
|
const token = readFileSync(join(stateDir, "control-token"), "utf8").trim();
|
|
44
46
|
if (!Number.isInteger(status.controlPort) || status.controlPort < 1 || status.controlPort > 65535 || !token) return undefined;
|
|
47
|
+
const startModes = status.startModes && typeof status.startModes === "object" && !Array.isArray(status.startModes)
|
|
48
|
+
? Object.fromEntries(Object.entries(status.startModes).filter((entry): entry is [string, "tui" | "headless"] => entry[1] === "tui" || entry[1] === "headless"))
|
|
49
|
+
: undefined;
|
|
45
50
|
return { url: `ws://127.0.0.1:${status.controlPort}`, token, projectId: status.projectId,
|
|
46
51
|
instanceId: status.instanceId, cwd: status.cwd, protocol: status.protocol, pid: status.pid, features: Array.isArray(status.features) ? status.features.filter((feature: unknown): feature is string => typeof feature === "string") : [],
|
|
52
|
+
...(startModes ? { startModes } : {}),
|
|
47
53
|
...(typeof status.pidSignature === "string" && status.pidSignature ? { pidSignature: status.pidSignature } : {}) };
|
|
48
54
|
} catch {
|
|
49
55
|
return undefined;
|