@staix/agent-hub 0.12.44 → 0.12.45
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 +3 -3
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/docs/adding-an-ai-tool.md +102 -14
- package/docs/agent-notes/README.md +9 -0
- package/docs/agent-notes/adapters.md +58 -44
- package/docs/agent-notes/budget.md +1 -1
- package/docs/agent-notes/daemon.md +9 -7
- package/docs/agent-notes/local-worker.md +4 -3
- package/docs/agent-notes/tasks.md +8 -5
- package/docs/agent-notes/tests.md +7 -7
- package/docs/operations.md +99 -7
- package/docs/quickstart.md +5 -1
- package/docs/sdlc.md +26 -8
- package/docs/security.md +6 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +114 -5
- package/docs/verification/2026-10-21-469-r1-dashboard-tokens.md +43 -0
- package/docs/verification/2026-10-21-469-r2-dashboard-shell.md +54 -0
- package/docs/verification/2026-10-21-469-r3-dashboard-board.md +51 -0
- package/docs/verification/2026-10-21-469-r4-dashboard-views.md +77 -0
- package/docs/verification/2026-10-21-469-r6-dashboard-motion-states.md +82 -0
- package/docs/verification/2026-10-22-0.12.45.md +60 -0
- package/docs/verification/assets/469-r1-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r1-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-agents-768-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1024-light.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r2-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-board-768-light.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-board-390-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-390-dark.png +0 -0
- package/docs/verification/assets/469-r3-plan-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-plan-390-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-memory-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-message-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-390-light.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-light.png +0 -0
- package/docs/verification/assets/469-r6-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-toasts-390-light.png +0 -0
- package/docs/verified.json +19 -16
- package/package.json +5 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +9818 -9605
- package/src/adapters/adapter-api.ts +2 -0
- package/src/adapters/agy/definition.ts +3 -1
- package/src/adapters/claude-channel.ts +14 -5
- package/src/adapters/codex-appserver.ts +1 -0
- package/src/adapters/conformance/acp.ts +114 -0
- package/src/adapters/conformance/codex.ts +191 -0
- package/src/adapters/conformance/enrollment.ts +43 -0
- package/src/adapters/conformance/factories.ts +17 -0
- package/src/adapters/conformance/fake-paths.ts +12 -0
- package/src/adapters/conformance/fakes/acp-server.ts +267 -0
- package/src/adapters/conformance/fakes/acp-watchdog-agent.ts +10 -0
- package/src/adapters/conformance/fakes/app-server.ts +119 -0
- package/src/adapters/conformance/fakes/codex-bin.ts +15 -0
- package/src/adapters/conformance/fakes/jcode-agent.ts +91 -0
- package/src/adapters/conformance/fakes/model-server.ts +39 -0
- package/src/adapters/conformance/fakes/pi-rpc.ts +32 -0
- package/src/adapters/conformance/index.ts +11 -0
- package/src/adapters/conformance/kit.ts +50 -0
- package/src/adapters/conformance/local-pi.ts +100 -0
- package/src/adapters/conformance/owned-acp.ts +44 -0
- package/src/adapters/definitions/builtin-metadata.ts +23 -6
- package/src/adapters/definitions/native-cli-entry-kimi.ts +7 -1
- package/src/adapters/definitions/native-cli-entry.ts +7 -19
- package/src/adapters/definitions/native-cli-preview.ts +2 -11
- package/src/adapters/definitions/qwen.ts +4 -1
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-module-scan.ts +126 -0
- package/src/adapters/external-modules.ts +240 -0
- package/src/adapters/pi.ts +2 -0
- package/src/adapters/public.ts +7 -0
- package/src/adapters/registry-types.ts +10 -0
- package/src/adapters/registry.ts +51 -15
- package/src/cli/help.ts +21 -16
- package/src/cli/identity.ts +1 -1
- package/src/cli/main.ts +23 -5
- package/src/cli/upgrade-labels.ts +1 -1
- package/src/cli/upgrade-presentation.ts +1 -1
- package/src/cli/upgrade-restore-state.ts +3 -2
- package/src/cli/upgrade-runtime.ts +4 -1
- package/src/hub/board.ts +30 -4
- package/src/hub/config-apply.ts +18 -10
- package/src/hub/config-reload.ts +8 -5
- package/src/hub/config-trust.ts +1 -1
- package/src/hub/control-client.ts +4 -1
- package/src/hub/daemon.ts +95 -39
- package/src/hub/facts.ts +76 -13
- package/src/hub/hub-tools.ts +21 -5
- package/src/hub/instance-config.ts +17 -5
- package/src/hub/instance-execution.ts +20 -8
- package/src/hub/instances.ts +2 -1
- package/src/hub/peers.ts +9 -0
- package/src/hub/plan-conformance.ts +83 -0
- package/src/hub/review-findings.ts +104 -0
- package/src/hub/roster.ts +31 -5
- package/src/hub/sdlc-queue.ts +19 -4
- package/src/hub/sdlc-release.ts +16 -1
- package/src/hub/sdlc.ts +27 -12
- package/src/hub/settings.ts +5 -1
- package/src/hub/snapshots.ts +8 -6
- package/src/hub/tasks.ts +275 -45
- package/src/hub/ui.ts +2 -2
- package/src/ui/index.html +559 -78
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +44 -0
|
@@ -66,8 +66,9 @@ export function decodeRestoreReceipt(value: unknown, options: { authority?: "cur
|
|
|
66
66
|
return { state: "done", binding: terminalBinding(value, historical) };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
/** The authority is the caller's: a decision reads the current one, a screen that only shows what was recorded reads the historical one (#225). */
|
|
70
|
+
export function restoreReceiptStatus(value: unknown, options: { authority?: "current" | "historical" } = {}): RestoreReceiptStatus {
|
|
71
|
+
const receipt = decodeRestoreReceipt(value, options);
|
|
71
72
|
return !receipt ? "unstarted" : receipt.state === "waiting" ? `waiting:${receipt.origin}` : receipt.state;
|
|
72
73
|
}
|
|
73
74
|
export function canChooseFresh(value: unknown): boolean {
|
|
@@ -145,6 +145,7 @@ export async function endPlannedPeer(planned: PlannedProject, peer: RecoveryPeer
|
|
|
145
145
|
/** Declared identities cannot be reinterpreted against an older running source's unbound peer rows. */
|
|
146
146
|
export function instanceRecoverySourceRefusal(projectRoot: string, features: unknown, observedPeers?: unknown): string | undefined {
|
|
147
147
|
const bound = readInstanceConfig(projectRoot);
|
|
148
|
+
if (bound.adapter_modules?.length && !(Array.isArray(features) && features.includes("external_adapter_modules"))) return "source lacks external_adapter_modules; restart with a supporting source before recovering module peers";
|
|
148
149
|
const configured = Object.keys(bound.adapters ?? {});
|
|
149
150
|
const configuredIds = [...configured, ...Object.entries(bound.instances ?? {}).filter(([, entry]) => configured.includes(entry.kind)).map(([id]) => id)];
|
|
150
151
|
if (configuredIds.length && !(Array.isArray(features) && features.every(feature => typeof feature === "string") && features.includes("configured_acp"))) {
|
|
@@ -159,7 +160,7 @@ export function instanceRecoverySourceRefusal(projectRoot: string, features: unk
|
|
|
159
160
|
if (peers.every(([, peer]) => !!peer && typeof peer === "object" && typeof (peer as { state?: unknown }).state === "string") &&
|
|
160
161
|
!peers.some(([id, peer]) => declared.includes(id) && (peer as { state: string }).state !== "offline")) return undefined;
|
|
161
162
|
}
|
|
162
|
-
return "source lacks peer_instances support
|
|
163
|
+
return "source lacks peer_instances support and a declared instance of this project may be live; restart the source with a supporting agent-hub version before retrying recovery; for a first upgrade, end the extra native sessions first (a source whose declared instances are all offline proceeds without further steps); only if one cannot be ended, temporarily remove its declaration, upgrade the source, then declare it again; legacy peer identities were not reinterpreted";
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
export async function inspectRecovery(project: Project, targetFeatures?: readonly string[], purpose?: "cleanup"): Promise<Inspection> {
|
|
@@ -626,6 +627,7 @@ export function makeRecoveryDriver(run: RunCommand = runCommand, liveTestPeer?:
|
|
|
626
627
|
return { ...target, ...(op.targetFeatures === undefined ? {} : { targetFeatures: [...op.targetFeatures] }) };
|
|
627
628
|
}
|
|
628
629
|
if (cached && op.targetFeatures !== undefined) {
|
|
630
|
+
if (!op.targetFeatures.includes("external_adapter_modules") && op.plan.projects.some(planned => typeof planned.project?.root === "string" && (readInstanceConfig(planned.project.root).adapter_modules?.length ?? 0) > 0)) throw new FinalRefusal("target lacks external_adapter_modules; module peers require a supporting target before recovery effects");
|
|
629
631
|
if (!op.targetFeatures.includes("configured_acp") && op.plan.projects.some(planned => typeof planned.project?.root === "string" && Object.keys(readInstanceConfig(planned.project.root).adapters ?? {}).length > 0)) throw new FinalRefusal("target lacks configured_acp; configured adapters require a supporting target before recovery effects");
|
|
630
632
|
if (!op.targetFeatures.includes("peer_instances") && op.plan.projects.some(planned => typeof planned.project?.root === "string" && Object.keys(readInstanceConfig(planned.project.root).instances ?? {}).length > 0)) throw new FinalRefusal("target lacks peer_instances; declared project instances require a supporting target before recovery effects");
|
|
631
633
|
return { ...target, targetFeatures: [...op.targetFeatures] };
|
|
@@ -638,6 +640,7 @@ export function makeRecoveryDriver(run: RunCommand = runCommand, liveTestPeer?:
|
|
|
638
640
|
if (!metadata || typeof metadata !== "object" || !Number.isInteger(metadata.protocol)) throw new Error(`the staged target's control protocol could not be read (probe exit ${probe.code}); staged package retained, runtimes unchanged; resume again once it can run`);
|
|
639
641
|
if (!Array.isArray(metadata.features) || !metadata.features.every((feature: unknown) => typeof feature === "string" && feature.length > 0)) throw new Error("the staged target features could not be read; staged package retained, runtimes unchanged");
|
|
640
642
|
const targetFeatures: readonly string[] = [...metadata.features];
|
|
643
|
+
if (!targetFeatures.includes("external_adapter_modules") && op.plan.projects.some(planned => typeof planned.project?.root === "string" && (readInstanceConfig(planned.project.root).adapter_modules?.length ?? 0) > 0)) throw new FinalRefusal("target lacks external_adapter_modules; module peers require a supporting target before recovery effects");
|
|
641
644
|
if (!targetFeatures.includes("configured_acp") && op.plan.projects.some(planned => typeof planned.project?.root === "string" && Object.keys(readInstanceConfig(planned.project.root).adapters ?? {}).length > 0)) throw new FinalRefusal("target lacks configured_acp; configured adapters require a supporting target before recovery effects");
|
|
642
645
|
if (metadata.protocol !== PROTOCOL) throw new FinalRefusal("target protocol requires a newer coordinator; staged package retained, runtimes unchanged");
|
|
643
646
|
if (!targetFeatures.includes("peer_instances") && op.plan.projects.some(planned => typeof planned.project?.root === "string" && Object.keys(readInstanceConfig(planned.project.root).instances ?? {}).length > 0)) throw new FinalRefusal("target lacks peer_instances; declared project instances require a supporting target before recovery effects");
|
package/src/hub/board.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { planFingerprint } from "./plan-conformance.ts";
|
|
1
2
|
import type { ReviewerPoolReason } from "./reviewer-pool.ts";
|
|
2
3
|
import type { GitHubSource } from "./github-import.ts";
|
|
3
4
|
import { Database } from "bun:sqlite";
|
|
@@ -6,6 +7,7 @@ import { HUB, type PeerId } from "./envelope.ts";
|
|
|
6
7
|
import type { SweepRecord } from "./task-sweep.ts";
|
|
7
8
|
import { HUB_DB_BUSY_TIMEOUT_MS } from "./db-timeout.ts";
|
|
8
9
|
import type { SdlcReceipt } from "./sdlc.ts";
|
|
10
|
+
import { parseReviewFindings, type ReviewFindingSet } from "./review-findings.ts";
|
|
9
11
|
import type { CostPlan, ReviewCost } from "./cost-governance.ts";
|
|
10
12
|
|
|
11
13
|
export const CLASSES = ["plan", "implement", "bulk_edit", "test", "review", "summarize", "triage"] as const;
|
|
@@ -28,7 +30,7 @@ export interface TaskRefs {
|
|
|
28
30
|
/** What an owner says it will change, before it starts (issue #31). Each list holds short free-text items. */
|
|
29
31
|
export const PLAN_KEYS = ["paths", "symbols", "signatures", "insertion_points"] as const;
|
|
30
32
|
export const DOCUMENT_PLAN_KEYS = ["paths", "sections", "sources", "claims"] as const;
|
|
31
|
-
export const ALL_PLAN_KEYS = [...PLAN_KEYS, "sections", "sources", "claims"] as const;
|
|
33
|
+
export const ALL_PLAN_KEYS = [...PLAN_KEYS, "sections", "sources", "claims", "proof", "risks"] as const;
|
|
32
34
|
export type TaskPlan = Partial<Record<(typeof ALL_PLAN_KEYS)[number], string[]>> & CostPlan;
|
|
33
35
|
/** Task outcomes older than this are pruned: demotion (issue #36) reads one week, seven half-lives. */
|
|
34
36
|
export const OUTCOMES_KEPT_MS = 7 * 24 * 3_600_000;
|
|
@@ -61,6 +63,13 @@ export interface HistoryEntry {
|
|
|
61
63
|
reviewCost?: ReviewCost;
|
|
62
64
|
/** #223 R4: on a verdict, whether the reviewer's context was new for it; set by the hub, never by the reviewer. */
|
|
63
65
|
reviewContext?: "fresh" | "continuing";
|
|
66
|
+
/** Structured review report bound to the reviewed artifact, stored in the existing history JSON. */
|
|
67
|
+
reviewFindings?: ReviewFindingSet;
|
|
68
|
+
/** Host-authored plan content revision, separate from backlog planningRevision. */
|
|
69
|
+
planRevision?: { revision: number; fingerprint: string };
|
|
70
|
+
planObservation?: { repo: string; pr: number; head: string; revision: number; fingerprint: string };
|
|
71
|
+
planDepartureInvalidated?: true;
|
|
72
|
+
planDeparture?: { generation: string; scope?: string; repo: string; pr: number; head: string; revision: number; fingerprint: string; paths: string[] };
|
|
64
73
|
reviewerFamily?: string;
|
|
65
74
|
reviewerReason?: ReviewerPoolReason;
|
|
66
75
|
reviewBlocked?: true;
|
|
@@ -153,6 +162,8 @@ export class Board {
|
|
|
153
162
|
private pendingChanges: { task: Task; entry: HistoryEntry }[] | undefined;
|
|
154
163
|
/** Every recorded change, as it lands (telemetry, issue #40). */
|
|
155
164
|
onChange?: (task: Task, entry: HistoryEntry) => void;
|
|
165
|
+
/** Host lifecycle policy, installed by Tasks; off projects keep their original history shape. */
|
|
166
|
+
trackPlanRevisions?: () => boolean;
|
|
156
167
|
private changed(task: Task, entry: HistoryEntry): void {
|
|
157
168
|
if (this.pendingChanges) { this.pendingChanges.push({ task, entry }); return; }
|
|
158
169
|
try { this.onChange?.(task, entry); } catch { /* never throw after a board write */ }
|
|
@@ -232,7 +243,7 @@ export class Board {
|
|
|
232
243
|
|
|
233
244
|
propose(by: PeerId, t: { title: string; detail?: string; class: TaskClass; profile?: import("./profiles.ts").ProfileName; profileClass?: string; customProfile?: true; role?: string; refs?: TaskRefs; plan?: TaskPlan; deps?: number[]; reserved?: PeerId; signals?: string[]; backlog?: boolean; external?: GitHubSource }): Task {
|
|
234
245
|
const now = Date.now();
|
|
235
|
-
const history: HistoryEntry[] = [{ at: now, by, event: t.backlog ? "backlogged" : "proposed" }];
|
|
246
|
+
const history: HistoryEntry[] = [{ at: now, by, event: t.backlog ? "backlogged" : "proposed", ...(this.trackPlanRevisions?.() && t.plan && Object.keys(t.plan).length ? { planRevision: { revision: 1, fingerprint: planFingerprint(t.plan) } } : {}) }];
|
|
236
247
|
const { lastInsertRowid } = this.taskWrite(() => this.db
|
|
237
248
|
.query("INSERT INTO tasks (title, detail, class, state, refs, plan, deps, reserved, signals, history, created, updated, project_profile, task_role, external_repo, external_issue) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
238
249
|
.run(t.title, t.detail ?? "", t.class, t.backlog ? "backlog" : "proposed", JSON.stringify(t.refs ?? {}), JSON.stringify(t.plan ?? {}), JSON.stringify(t.deps ?? []), t.reserved ?? null, JSON.stringify(t.signals ?? []), JSON.stringify(history), now, now, t.profile && (t.profile !== "dev" || t.customProfile || t.profileClass !== undefined && t.profileClass !== t.class) ? JSON.stringify({ profile: t.profile, profileClass: t.profileClass ?? t.class }) : null, t.role ?? null, t.external?.repo.toLowerCase() ?? null, t.external?.issue ?? null));
|
|
@@ -307,7 +318,7 @@ export class Board {
|
|
|
307
318
|
|
|
308
319
|
/** The only way a task changes. Validates the move, records who did what, returns the new row. */
|
|
309
320
|
/** A `plan` in the patch replaces the old one whole: a new plan is the owner's current intent, not an addition. */
|
|
310
|
-
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" | "capacity" | "withheld" | "noWrites" | "reviewCost" | "reviewContext" | keyof ReviewerHistory> = {}, expectedFlowRevision?: number): Task {
|
|
321
|
+
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" | "capacity" | "withheld" | "noWrites" | "reviewCost" | "reviewContext" | "reviewFindings" | "planObservation" | "planDeparture" | "planDepartureInvalidated" | keyof ReviewerHistory> = {}, expectedFlowRevision?: number): Task {
|
|
311
322
|
const task = this.get(id);
|
|
312
323
|
if (!task) throw new Error(`no task #${id}`);
|
|
313
324
|
if (expectedFlowRevision !== undefined && (!Number.isSafeInteger(expectedFlowRevision) || expectedFlowRevision < 0 || task.history.length !== expectedFlowRevision)) throw new Error("task flow revision changed");
|
|
@@ -323,7 +334,22 @@ export class Board {
|
|
|
323
334
|
}
|
|
324
335
|
if (patch.state === "backlog" && taskHasStarted(task) && !untouchedReturn) throw new Error(`task #${id}: started work cannot return to backlog`);
|
|
325
336
|
const reviewMetadata = reviewerHistory(extra);
|
|
326
|
-
|
|
337
|
+
let reviewFindings: ReviewFindingSet | undefined;
|
|
338
|
+
if (extra.reviewFindings !== undefined) {
|
|
339
|
+
if (!["approved", "changes_requested"].includes(event) || patch.state !== event) throw new Error("finding metadata requires a saved review verdict");
|
|
340
|
+
const head = extra.reviewFindings.head;
|
|
341
|
+
if (head !== undefined && !/^[a-f0-9]{40}$/.test(head)) throw new Error("finding metadata requires a full reviewed head");
|
|
342
|
+
reviewFindings = { ...(head ? { head } : {}), items: parseReviewFindings(extra.reviewFindings.items, { person: by === "user", maxNits: 64 }) };
|
|
343
|
+
}
|
|
344
|
+
if (extra.planDeparture && (by !== "user" || event !== "plan departure accepted")) throw new Error("plan departure acceptance is person-only");
|
|
345
|
+
const trackPlan = this.trackPlanRevisions?.() === true, hasDeparture = task.history.some(h => h.planDeparture);
|
|
346
|
+
const fingerprint = patch.plan && (trackPlan || hasDeparture) ? planFingerprint(next.plan ?? {}) : undefined;
|
|
347
|
+
const planChanged = fingerprint !== undefined && fingerprint !== planFingerprint(task.plan ?? {});
|
|
348
|
+
const planRevision = trackPlan && planChanged ? { revision: (task.history.findLast(h => h.planRevision)?.planRevision?.revision ?? 0) + 1, fingerprint: fingerprint! } : undefined;
|
|
349
|
+
// Disabling reporting must not let plan or repository refs edits/reverts resurrect prior consent.
|
|
350
|
+
const repositoryRefsChanged = patch.refs !== undefined && JSON.stringify([task.refs.repo, task.refs.paths]) !== JSON.stringify([next.refs.repo, next.refs.paths]);
|
|
351
|
+
const departureInvalidated = extra.planDepartureInvalidated === true || hasDeparture && (planChanged || repositoryRefsChanged);
|
|
352
|
+
const history = [...task.history, { at: Date.now(), by, event, ...(planRevision ? { planRevision } : {}), ...(extra.planObservation ? { planObservation: extra.planObservation } : {}), ...(extra.planDeparture ? { planDeparture: extra.planDeparture } : {}), ...(departureInvalidated ? { planDepartureInvalidated: true as const } : {}), ...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.capacity === true ? { capacity: true as const } : {}), ...(extra.withheld && (note || reviewFindings) ? { withheld: true as const } : {}), ...(reviewFindings ? { reviewFindings } : {}), ...(extra.reviewContext === "fresh" || extra.reviewContext === "continuing" ? { reviewContext: extra.reviewContext } : {}), ...(extra.reviewCost ? { reviewCost: extra.reviewCost } : {}), ...(event === "done" && extra.noWrites === true ? { noWrites: true as const } : {}) }];
|
|
327
353
|
this.taskWrite(() => this.db
|
|
328
354
|
.query("UPDATE tasks SET state = ?, owner = ?, reviewer = ?, reserved = ?, refs = ?, plan = ?, rejections = ?, signals = ?, history = ?, sdlc = ?, task_role = ?, cancelled = ?, updated = ? WHERE id = ?")
|
|
329
355
|
.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));
|
package/src/hub/config-apply.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { adapterModuleDeclarations } from "../adapters/external-modules.ts";
|
|
2
|
+
import { externalMetadataDefinitions, type ExternalAdapterMetadata } from "../adapters/external-metadata.ts";
|
|
1
3
|
import { reviewerPoolConfig } from "./reviewer-pool-config.ts";
|
|
2
4
|
import type { HubConfig } from "./daemon.ts";
|
|
3
5
|
import { ALL_MACHINE_LOCAL, configRepositoryPresent, configTracked } from "./config-trust.ts";
|
|
@@ -10,7 +12,7 @@ import { resolveProfile } from "./profiles.ts";
|
|
|
10
12
|
import { taskSweepConfig } from "./task-sweep.ts";
|
|
11
13
|
import { projectionConfig } from "./text-projection.ts";
|
|
12
14
|
import { validateInstances, type InstancesConfig } from "./instances.ts";
|
|
13
|
-
import { peerRosterConfig } from "./roster.ts";
|
|
15
|
+
import { peerRosterConfig, staleInstanceKey } from "./roster.ts";
|
|
14
16
|
import { CHARACTER_IDS } from "../pet/characters.ts";
|
|
15
17
|
import { petPalette } from "../pet/config.ts";
|
|
16
18
|
|
|
@@ -18,9 +20,9 @@ export type ApplyClass = "live" | "next use" | "restart";
|
|
|
18
20
|
export interface ApplyRule { applies: ApplyClass; reason?: string }
|
|
19
21
|
export interface ConfigIdentityScope { catalog: ProjectAdapterCatalog; instances: InstancesConfig }
|
|
20
22
|
/** Plain captured metadata stays cloneable with ConfigSources; native callbacks are compiled outside it. */
|
|
21
|
-
export interface ConfigIdentitySnapshot { adapters?: HubConfig["adapters"]; instances: InstancesConfig }
|
|
23
|
+
export interface ConfigIdentitySnapshot { adapters?: HubConfig["adapters"]; externalAdapters?: readonly ExternalAdapterMetadata[]; instances: InstancesConfig }
|
|
22
24
|
export function configIdentityScope(snapshot: ConfigIdentitySnapshot): ConfigIdentityScope {
|
|
23
|
-
return { catalog: createProjectAdapterCatalog(snapshot.adapters), instances: snapshot.instances };
|
|
25
|
+
return { catalog: createProjectAdapterCatalog(snapshot.adapters, externalMetadataDefinitions(snapshot.externalAdapters)), instances: snapshot.instances };
|
|
24
26
|
}
|
|
25
27
|
export interface ConfigChange extends ApplyRule { key: string; before: unknown; after: unknown }
|
|
26
28
|
export interface ConfigLayer { name: "config.json" | "config.local.json"; raw: Record<string, unknown> }
|
|
@@ -134,6 +136,7 @@ const schema: Record<string, Check> = {
|
|
|
134
136
|
...Object.assign({}, ...ADAPTERS.map(adapter => adapter.configuration?.schema?.(nativeValidators) ?? {})),
|
|
135
137
|
watchdog_ms: number(1, 2_147_483_647), kimi_cmd: argv, qwen_cmd: argv, jcode_cmd: argv, codex_bin: text, jcode_home: text, jcode_provider_profile: text,
|
|
136
138
|
permission_modes: v => { permissionDefaults(v); }, protect_instruction_files: bool,
|
|
139
|
+
adapter_modules: v => { adapterModuleDeclarations(v); },
|
|
137
140
|
adapters: v => { createProjectAdapterCatalog(v); }, instances: v => { validateInstances(v, createProjectAdapterCatalog()); }, pet: validatePetConfig,
|
|
138
141
|
batch_max: number(1, 100_000, true), batch_ms: number(0, 2_147_483_647), queue_cap: number(1, 1_000_000, true),
|
|
139
142
|
memory: block({ enabled: bool, worker_url: text, inject_tokens: number(0, 1_000_000, true), brief_items: number(0, 10_000, true), native: bool,
|
|
@@ -166,27 +169,28 @@ const schema: Record<string, Check> = {
|
|
|
166
169
|
capabilities: map(strings), coordination: words(["advisory", "turn-free"]), experiments: block({ stale_notices: words(["drop", "deliver"]) }),
|
|
167
170
|
};
|
|
168
171
|
export const SCHEMA_KEYS: readonly string[] = Object.freeze(Object.keys(schema));
|
|
169
|
-
export function validateRawConfig(raw: unknown, declarations?: InstancesConfig, context?: ProjectAdapterCatalog): asserts raw is Record<string, unknown> {
|
|
172
|
+
export function validateRawConfig(raw: unknown, declarations?: InstancesConfig, context?: ProjectAdapterCatalog, inertModuleRoster = false): asserts raw is Record<string, unknown> {
|
|
170
173
|
const value = object(raw, "config");
|
|
171
|
-
const catalog = value.adapters !== undefined ? createProjectAdapterCatalog(value.adapters) : context ?? createProjectAdapterCatalog();
|
|
174
|
+
const catalog = value.adapters !== undefined ? createProjectAdapterCatalog(value.adapters, context?.externalDefinitions) : context ?? createProjectAdapterCatalog();
|
|
172
175
|
const instances = validateInstances(value.instances ?? declarations, catalog);
|
|
173
176
|
block({ ...schema,
|
|
174
177
|
instances: v => { validateInstances(v, catalog); },
|
|
175
178
|
permission_modes: v => { permissionDefaults(v, instances, catalog); },
|
|
176
|
-
roster: v => { peerRosterConfig(v, instances, catalog); },
|
|
179
|
+
roster: v => { peerRosterConfig(v, instances, catalog, inertModuleRoster); },
|
|
177
180
|
peers: v => { peerStartConfig(v, instances, catalog); },
|
|
178
181
|
})(raw, "config");
|
|
179
182
|
}
|
|
180
|
-
export function validateEffectiveConfig(value: unknown): asserts value is HubConfig {
|
|
183
|
+
export function validateEffectiveConfig(value: unknown, catalog?: ProjectAdapterCatalog): asserts value is HubConfig {
|
|
181
184
|
const raw = { ...object(value, "config") };
|
|
182
185
|
delete raw.ignored; delete raw.retired; delete raw.permission_default_sources;
|
|
183
|
-
validateRawConfig(raw);
|
|
186
|
+
validateRawConfig(raw, undefined, catalog);
|
|
184
187
|
const profile = resolveProfile(raw.profile, raw.profile_overrides);
|
|
185
188
|
for (const key of Object.keys(object(raw.checks, "checks"))) if (!["timeout_s", "runner", ...CLASSES, ...Object.keys(profile.classes)].includes(key)) fail(`checks.${key}`, "unknown task class");
|
|
186
189
|
for (const adapter of ADAPTERS) adapter.configuration?.validateEffective?.(raw, fail);
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
const restart: Record<string, string> = {
|
|
193
|
+
adapter_modules: "External adapter modules are verified and imported only at daemon startup",
|
|
190
194
|
adapters: "Configured adapter identity, commands and capability policy are fixed at daemon startup",
|
|
191
195
|
instances: "Native instance identity, worktree, ports and account bindings are fixed at daemon startup",
|
|
192
196
|
...Object.assign({}, ...ADAPTERS.map(adapter => adapter.configuration?.restartReasons ?? {})),
|
|
@@ -214,7 +218,7 @@ export function configApply(key: string, scopes: readonly ConfigIdentityScope[]
|
|
|
214
218
|
if (shape?.fields) { if (!Object.hasOwn(shape.fields, part)) return fail(key, "unknown configuration key"); check = shape.fields[part]; }
|
|
215
219
|
else if (shape?.item) { if (shape.keys && !shape.keys.includes(part) && !(top === "roster" && scopes.some(scope => scope.catalog.definitionOf(part, scope.instances)))) return fail(key, "unknown configuration key"); check = shape.item; }
|
|
216
220
|
else if (index === 0 && (special[top]?.includes(part) || top === "checks" && /^[a-z][a-z0-9_]{0,47}$/.test(part))) break;
|
|
217
|
-
else if (["permission_modes", "catalog_roles", "profile_overrides", "text_projection", "peers", "instances", "adapters"].includes(top)) break;
|
|
221
|
+
else if (["permission_modes", "catalog_roles", "profile_overrides", "text_projection", "peers", "instances", "adapters", "adapter_modules"].includes(top)) break;
|
|
218
222
|
else return fail(key, "unknown configuration key");
|
|
219
223
|
}
|
|
220
224
|
}
|
|
@@ -237,7 +241,11 @@ export function configChanges(running: HubConfig, next: HubConfig, scopes: reado
|
|
|
237
241
|
if (same(before, after)) return;
|
|
238
242
|
if (before && after && typeof before === "object" && typeof after === "object" && !Array.isArray(before) && !Array.isArray(after)) {
|
|
239
243
|
for (const child of new Set([...Object.keys(before), ...Object.keys(after)])) walk(at(before, [child]), at(after, [child]), key ? `${key}.${child}` : child);
|
|
240
|
-
} else
|
|
244
|
+
} else {
|
|
245
|
+
// #225: deleting a roster key whose instance was undeclared is live and needs no identity; adding or setting one stays an unknown key.
|
|
246
|
+
const stale = after === undefined && /^roster\.[^.]+$/.test(key) && scopes.some(scope => staleInstanceKey(key.slice("roster.".length), scope.catalog));
|
|
247
|
+
changes.push({ key, before, after, ...(stale ? { applies: "live" as const } : configApply(key, scopes)) });
|
|
248
|
+
}
|
|
241
249
|
};
|
|
242
250
|
const clean = (v: HubConfig) => Object.fromEntries(Object.entries(v).filter(([key]) => !["ignored", "retired", "permission_default_sources"].includes(key)));
|
|
243
251
|
walk(clean(running), clean(next), ""); return changes.sort((a, b) => a.key.localeCompare(b.key));
|
package/src/hub/config-reload.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ADAPTERS, createProjectAdapterCatalog } from "../adapters/registry.ts";
|
|
2
|
+
import { externalMetadataDefinitions, type ExternalAdapterMetadata } from "../adapters/external-metadata.ts";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { readFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -86,7 +87,7 @@ function validateRoutingRaw(value: unknown): void {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/** Strict reloads never use routing's last-good fallback. Startup can retain its existing fallback explicitly. */
|
|
89
|
-
export function parseConfigSources(texts: ConfigSourceTexts, fallbackRouting?: Routing, cwd?: string): ConfigSources {
|
|
90
|
+
export function parseConfigSources(texts: ConfigSourceTexts, fallbackRouting?: Routing, cwd?: string, externalAdapters?: readonly ExternalAdapterMetadata[]): ConfigSources {
|
|
90
91
|
const layers: ConfigLayer[] = [], trustLayers: ConfigSources["trustLayers"] = [];
|
|
91
92
|
const jsonLayers: ConfigLayer[] = [];
|
|
92
93
|
for (const name of ["config.json", "config.local.json"] as const) {
|
|
@@ -103,13 +104,15 @@ export function parseConfigSources(texts: ConfigSourceTexts, fallbackRouting?: R
|
|
|
103
104
|
const localRaw = jsonLayers.find(layer => layer.name === "config.local.json")?.raw ?? {};
|
|
104
105
|
let declared;
|
|
105
106
|
let identityAdapters: ConfigIdentitySnapshot["adapters"];
|
|
107
|
+
let inertModuleRoster = false;
|
|
106
108
|
let catalog = createProjectAdapterCatalog();
|
|
107
109
|
try {
|
|
108
110
|
if (cwd) {
|
|
109
|
-
const accepted = readInstanceConfig(cwd, jsonLayers);
|
|
111
|
+
const accepted = readInstanceConfig(cwd, jsonLayers, externalAdapters === undefined ? undefined : externalMetadataDefinitions(externalAdapters));
|
|
110
112
|
catalog = accepted.catalog; declared = accepted.instances; identityAdapters = accepted.adapters;
|
|
113
|
+
inertModuleRoster = !!accepted.adapter_modules?.length;
|
|
111
114
|
} else {
|
|
112
|
-
catalog = createProjectAdapterCatalog(localRaw?.adapters);
|
|
115
|
+
catalog = createProjectAdapterCatalog(localRaw?.adapters, externalMetadataDefinitions(externalAdapters));
|
|
113
116
|
identityAdapters = localRaw?.adapters as ConfigIdentitySnapshot["adapters"];
|
|
114
117
|
declared = validateInstances(localRaw?.instances, catalog);
|
|
115
118
|
}
|
|
@@ -135,7 +138,7 @@ export function parseConfigSources(texts: ConfigSourceTexts, fallbackRouting?: R
|
|
|
135
138
|
const structural = { ...raw };
|
|
136
139
|
// Declaration provenance was checked before compilation; ignored source data is never re-admitted.
|
|
137
140
|
delete structural.adapters; delete structural.instances;
|
|
138
|
-
validateRawConfig(structural, declared, catalog);
|
|
141
|
+
validateRawConfig(structural, declared, catalog, inertModuleRoster);
|
|
139
142
|
}
|
|
140
143
|
catch (error) { throw new Error(`.agenthub/${name}: ${(error as Error).message}`); }
|
|
141
144
|
}
|
|
@@ -153,5 +156,5 @@ export function parseConfigSources(texts: ConfigSourceTexts, fallbackRouting?: R
|
|
|
153
156
|
try { routing = parseRouting(base, overlay); }
|
|
154
157
|
catch { throw new Error(".agenthub/routing.toml / routing.local.toml: invalid effective routing"); }
|
|
155
158
|
}
|
|
156
|
-
return { texts, layers, trustLayers, routing, fingerprint: configFingerprint(texts), identity: { ...(identityAdapters ? { adapters: structuredClone(identityAdapters) } : {}), instances: declared ?? validateInstances(undefined, catalog) } };
|
|
159
|
+
return { texts, layers, trustLayers, routing, fingerprint: configFingerprint(texts), identity: { ...(identityAdapters ? { adapters: structuredClone(identityAdapters) } : {}), instances: declared ?? validateInstances(undefined, catalog), ...(catalog.externalDefinitions.length ? { externalAdapters: catalog.externalDefinitions.map(({ kind, displayName }) => ({ kind, displayName })) } : {}) } };
|
|
157
160
|
}
|
package/src/hub/config-trust.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const MACHINE_LOCAL = [
|
|
|
29
29
|
"terminal.open", // the command a hub-made TUI start runs to open a terminal (#269)
|
|
30
30
|
] as const;
|
|
31
31
|
/** Legacy exported field list stays compatible; new policies use the complete list below. */
|
|
32
|
-
export const ALL_MACHINE_LOCAL = [...MACHINE_LOCAL, "protect_instruction_files", "qwen_cmd", "jcode_cmd", "jcode_home", "jcode_provider_profile", "adapters", "text_projection", "instances"] as const;
|
|
32
|
+
export const ALL_MACHINE_LOCAL = [...MACHINE_LOCAL, "protect_instruction_files", "qwen_cmd", "jcode_cmd", "jcode_home", "jcode_provider_profile", "adapters", "adapter_modules", "text_projection", "instances"] as const;
|
|
33
33
|
|
|
34
34
|
/** A known repository, including an unreadable marker, must never be mistaken for a plain directory. */
|
|
35
35
|
export function configRepositoryPresent(cwd: string): boolean {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { projectContext } from "./project.ts";
|
|
4
|
+
import { externalAdapterMetadata, type ExternalAdapterMetadata } from "../adapters/external-metadata.ts";
|
|
4
5
|
|
|
5
6
|
export function stateDirFor(cwd: string): string {
|
|
6
7
|
return projectContext(cwd).stateDir;
|
|
@@ -8,7 +9,7 @@ export function stateDirFor(cwd: string): string {
|
|
|
8
9
|
|
|
9
10
|
/** 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
11
|
export const PROTOCOL = 16;
|
|
11
|
-
export const DAEMON_FEATURES = ["configured_acp", "native_hook_launch", "task_cancel", "recovery_deferred_hub", "peer_instances", "release_trains", "reviewer_pool", "native_tui_launch", "terminal_owner", "cli_audit_delegated"] as const;
|
|
12
|
+
export const DAEMON_FEATURES = ["external_adapter_modules", "configured_acp", "native_hook_launch", "task_cancel", "recovery_deferred_hub", "peer_instances", "release_trains", "reviewer_pool", "native_tui_launch", "terminal_owner", "cli_audit_delegated"] as const;
|
|
12
13
|
/** Protocols a current coordinator may authenticate while upgrading a running source. */
|
|
13
14
|
export const RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, 15, PROTOCOL] as const;
|
|
14
15
|
|
|
@@ -38,6 +39,7 @@ export interface ControlDescriptor {
|
|
|
38
39
|
features?: readonly string[];
|
|
39
40
|
/** The daemon's accepted native start modes, used by launchers when the file value is retained. */
|
|
40
41
|
startModes?: Readonly<Record<string, "tui" | "headless">>;
|
|
42
|
+
externalAdapters?: readonly ExternalAdapterMetadata[];
|
|
41
43
|
/** #226: the daemon's processSignature, written into status.json only (not the status reply); absent before 0.12.21. */
|
|
42
44
|
pidSignature?: string;
|
|
43
45
|
}
|
|
@@ -54,6 +56,7 @@ export function readControl(stateDir: string): ControlDescriptor | undefined {
|
|
|
54
56
|
return { url: `ws://127.0.0.1:${status.controlPort}`, token, projectId: status.projectId,
|
|
55
57
|
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") : [],
|
|
56
58
|
...(startModes ? { startModes } : {}),
|
|
59
|
+
...(status.externalAdapters !== undefined ? { externalAdapters: externalAdapterMetadata(status.externalAdapters) } : {}),
|
|
57
60
|
...(typeof status.pidSignature === "string" && status.pidSignature ? { pidSignature: status.pidSignature } : {}) };
|
|
58
61
|
} catch {
|
|
59
62
|
return undefined;
|