@staix/agent-hub 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +4 -4
- package/docs/agent-notes/README.md +11 -0
- package/docs/agent-notes/adapters.md +10 -6
- package/docs/agent-notes/benchmarks.md +1 -0
- package/docs/agent-notes/daemon.md +6 -2
- package/docs/agent-notes/local-worker.md +3 -1
- package/docs/agent-notes/tasks.md +11 -2
- package/docs/agent-notes/tests.md +7 -3
- package/docs/dogfood.md +22 -2
- package/docs/memory.md +1 -1
- package/docs/operations.md +162 -35
- package/docs/profiles.md +36 -0
- package/docs/quickstart.md +15 -5
- package/docs/sdlc.md +29 -3
- package/docs/security.md +44 -13
- package/docs/smoke.md +3 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +31 -10
- package/docs/verification/2026-10-25-0.18.0.md +76 -0
- package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
- package/docs/verification/2026-10-25-remote-docker-transport.md +56 -0
- package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
- package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
- package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
- package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
- package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
- package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
- package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
- package/docs/verification/2026-10-27-0.19.0.md +86 -0
- package/docs/verification/claude-permission-transport-native.py +187 -0
- package/docs/verification/claude-shell-wrapper-host.ts +20 -0
- package/docs/verification/claude-shell-wrapper-native.py +145 -0
- package/docs/verification/pi-140-model-free.ts +57 -0
- package/docs/verification/pi-140-native.mjs +21 -0
- package/docs/verification/profiles/YYYY-MM-DD-general.md +46 -0
- package/docs/verification/profiles/YYYY-MM-DD-research.md +61 -0
- package/docs/verification/profiles/YYYY-MM-DD-writing.md +46 -0
- package/docs/verified.json +24 -18
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +364 -98
- package/src/adapters/agy/entry.ts +1 -1
- package/src/adapters/claude-channel.ts +7 -3
- package/src/adapters/claude-implementer.ts +5 -3
- package/src/adapters/compat.ts +32 -18
- package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
- package/src/adapters/definitions/builtin-metadata.ts +3 -3
- package/src/adapters/definitions/native-factories.ts +7 -2
- package/src/adapters/definitions/reviewer-variants.ts +2 -2
- package/src/adapters/local-worker.ts +5 -3
- package/src/adapters/pi.ts +39 -3
- package/src/adapters/qwen-dual.ts +25 -0
- package/src/adapters/registry-types.ts +6 -2
- package/src/adapters/registry.ts +1 -0
- package/src/cli/agents.ts +69 -7
- package/src/cli/claude-shell-wrapper.ts +54 -0
- package/src/cli/console-state.ts +9 -2
- package/src/cli/help.ts +29 -28
- package/src/cli/identity.ts +87 -6
- package/src/cli/main.ts +198 -71
- package/src/cli/output.ts +11 -2
- package/src/cli/sdlc-output.ts +242 -0
- package/src/cli/task-output.ts +212 -0
- package/src/cli/upgrade-output.ts +57 -0
- package/src/hub/agent-drift.ts +61 -0
- package/src/hub/board.ts +31 -2
- package/src/hub/check-runner.ts +385 -25
- package/src/hub/claude-shell-wrapper.ts +349 -0
- package/src/hub/conductor.ts +5 -0
- package/src/hub/config-apply.ts +16 -7
- package/src/hub/config-edit-host.ts +1 -1
- package/src/hub/config-edit.ts +2 -2
- package/src/hub/config-tier.ts +3 -3
- package/src/hub/daemon.ts +530 -87
- package/src/hub/dogfood-profile.ts +248 -0
- package/src/hub/dogfood-read.ts +77 -14
- package/src/hub/dogfood.ts +5 -1
- package/src/hub/events.ts +1 -1
- package/src/hub/host-executable.ts +21 -1
- package/src/hub/hub-tools.ts +2 -2
- package/src/hub/instances.ts +18 -2
- package/src/hub/lifecycle.ts +1 -1
- package/src/hub/operational-authority.ts +67 -0
- package/src/hub/operational-view.ts +137 -0
- package/src/hub/orphan-process.ts +69 -0
- package/src/hub/permission-mode.ts +1 -1
- package/src/hub/read-only-shell-proof.ts +81 -0
- package/src/hub/read-only-shell.ts +162 -0
- package/src/hub/review-scope.ts +13 -0
- package/src/hub/reviewer-pool-config.ts +3 -2
- package/src/hub/reviewer-pool.ts +3 -0
- package/src/hub/routing.ts +23 -6
- package/src/hub/sdlc-merge.ts +299 -0
- package/src/hub/sdlc-metrics.ts +6 -4
- package/src/hub/sdlc-mirror.ts +129 -0
- package/src/hub/sdlc.ts +58 -12
- package/src/hub/settings.ts +11 -5
- package/src/hub/snapshots.ts +60 -0
- package/src/hub/task-repository.ts +31 -5
- package/src/hub/task-shell-command.ts +85 -0
- package/src/hub/task-shell-default.ts +78 -0
- package/src/hub/task-shell-proof.ts +171 -0
- package/src/hub/task-shell-sandbox.ts +61 -0
- package/src/hub/tasks.ts +378 -27
- package/src/local/deny.ts +91 -11
- package/src/local/sandbox.ts +67 -28
- package/src/local/shell-environment.ts +10 -0
- package/src/local/tools.ts +17 -4
- package/src/ui/index.html +3 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { COMPAT, type CompatId } from "../adapters/compat.ts";
|
|
2
|
+
import type { AgentVersionRow } from "../cli/agents.ts";
|
|
3
|
+
import { HUB, newEnvelope, sanitize, type PeerId } from "./envelope.ts";
|
|
4
|
+
|
|
5
|
+
/** #843 AC3: an installed gated CLI whose version is not `verified`. Ids, version, state and one receipt path, never output of the CLI. */
|
|
6
|
+
export interface DriftedAgent { id: CompatId; version: string; state: "in-line" | "trusted" | "refused"; receipt: string }
|
|
7
|
+
|
|
8
|
+
/** A version string that is safe to carry in a notice and a marker: what `COMPAT[id].parse` yields for a real CLI. */
|
|
9
|
+
const SAFE_VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pure: which rows are drift. A missing executable is not drift (no executable, no version), and a CLI whose
|
|
13
|
+
* version could not be read has no (id, version) to announce, so `unknown` is never drift either.
|
|
14
|
+
*/
|
|
15
|
+
export function driftedAgents(rows: readonly AgentVersionRow[]): DriftedAgent[] {
|
|
16
|
+
const out: DriftedAgent[] = [];
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
const { verdict } = row;
|
|
19
|
+
if (!row.executable || verdict.state === "verified" || verdict.state === "unknown") continue;
|
|
20
|
+
const version = verdict.version;
|
|
21
|
+
if (!version || !SAFE_VERSION.test(version)) continue;
|
|
22
|
+
out.push({ id: row.id, version, state: verdict.state, receipt: COMPAT[row.id].receipt });
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Durable key of one notice: the (id, version) and the planner it was queued for. */
|
|
28
|
+
export const driftMarker = (agent: Pick<DriftedAgent, "id" | "version">, planner: PeerId): string => `${agent.id}:${agent.version}@${planner}`;
|
|
29
|
+
|
|
30
|
+
export const driftNoticeBody = (agent: DriftedAgent): string =>
|
|
31
|
+
sanitize(`Agent CLI drift: ${agent.id} ${agent.version} ${agent.state}; last receipt ${agent.receipt}`);
|
|
32
|
+
|
|
33
|
+
export interface DriftDeps {
|
|
34
|
+
/** True when the (id, version, planner) marker is new and now recorded (durable: `Board.recordAgentDriftNotice`). */
|
|
35
|
+
record(marker: string): boolean;
|
|
36
|
+
/** Undo a record whose notice could not be queued, so the next sweep tries again. */
|
|
37
|
+
forget(marker: string): void;
|
|
38
|
+
/** The effective planner holders from the settled role map. */
|
|
39
|
+
planners(): readonly PeerId[];
|
|
40
|
+
isRostered(peer: PeerId): boolean;
|
|
41
|
+
publish(env: ReturnType<typeof newEnvelope>): PeerId[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One notice per (id, version) to each effective planner, kept durably per recipient: a planner that was queued is
|
|
46
|
+
* never told again, and one that could not be queued (or was not rostered yet) is retried alone by the next sweep.
|
|
47
|
+
* With no rostered planner nothing is recorded. Returns the notices queued.
|
|
48
|
+
*/
|
|
49
|
+
export function noticeDrift(drift: readonly DriftedAgent[], deps: DriftDeps): number {
|
|
50
|
+
const planners = deps.planners().filter(peer => deps.isRostered(peer));
|
|
51
|
+
let sent = 0;
|
|
52
|
+
for (const agent of drift) for (const planner of planners) {
|
|
53
|
+
const marker = driftMarker(agent, planner);
|
|
54
|
+
if (!deps.record(marker)) continue;
|
|
55
|
+
try {
|
|
56
|
+
if (!deps.publish(newEnvelope(HUB, driftNoticeBody(agent), { to: [planner], kind: "task" })).includes(planner)) throw new Error("the notice was not queued");
|
|
57
|
+
sent++;
|
|
58
|
+
} catch { deps.forget(marker); }
|
|
59
|
+
}
|
|
60
|
+
return sent;
|
|
61
|
+
}
|
package/src/hub/board.ts
CHANGED
|
@@ -72,6 +72,10 @@ export interface HistoryEntry {
|
|
|
72
72
|
reviewCost?: ReviewCost;
|
|
73
73
|
/** #223 R4: on a verdict, whether the reviewer's context was new for it; set by the hub, never by the reviewer. */
|
|
74
74
|
reviewContext?: "fresh" | "continuing";
|
|
75
|
+
/** #852: a verdict by a `code`-scope reviewer says so; a `full` reviewer's verdict carries nothing, so existing rows are unchanged. */
|
|
76
|
+
reviewScope?: "code";
|
|
77
|
+
/** #852 AC3: a `code`-scope reviewer's changes_requested with no open finding. It is recorded, returns the task to its owner and counts toward nothing. */
|
|
78
|
+
evidenceOnly?: true;
|
|
75
79
|
/** Structured review report bound to the reviewed artifact, stored in the existing history JSON. */
|
|
76
80
|
reviewFindings?: ReviewFindingSet;
|
|
77
81
|
/**
|
|
@@ -259,6 +263,8 @@ export class Board {
|
|
|
259
263
|
this.db.run("CREATE TABLE IF NOT EXISTS task_stalls (task INTEGER PRIMARY KEY, payload TEXT NOT NULL)");
|
|
260
264
|
// #481 S7: one row per log-tier band breach episode already announced, so a restart never repeats the notice.
|
|
261
265
|
this.db.run("CREATE TABLE IF NOT EXISTS band_notices (marker TEXT PRIMARY KEY, at INTEGER NOT NULL)");
|
|
266
|
+
// #843 AC3: one row per (agent id, installed version, planner) already queued, so a restart never repeats it.
|
|
267
|
+
this.db.run("CREATE TABLE IF NOT EXISTS agent_drift_notices (marker TEXT PRIMARY KEY, at INTEGER NOT NULL)");
|
|
262
268
|
// Derived status-cache generation: only Board task/stall writes advance it, never journal writes.
|
|
263
269
|
this.db.run("CREATE TABLE IF NOT EXISTS board_status_revision (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL)");
|
|
264
270
|
this.db.run("INSERT OR IGNORE INTO board_status_revision VALUES (1,0)");
|
|
@@ -339,6 +345,23 @@ export class Board {
|
|
|
339
345
|
return row.n;
|
|
340
346
|
}
|
|
341
347
|
|
|
348
|
+
/**
|
|
349
|
+
* #784 AC3: what `plannerWip` counts, split per peer in ONE grouped query (no per-peer or per-task reads on the status hot path). `active` is owned work that is not
|
|
350
|
+
* waiting for review, `reviewWait` is owned work in `in_review`, `reserved` is unowned proposed work a planner pull reserved. For every peer the three add up to
|
|
351
|
+
* `plannerWip(peer)`: this is a read-only view, admission keeps using `plannerWip` unchanged.
|
|
352
|
+
*/
|
|
353
|
+
plannerWipBreakdown(): Record<string, { active: number; reviewWait: number; reserved: number }> {
|
|
354
|
+
const rows = this.db.query(`SELECT peer, kind, COUNT(*) AS n FROM (
|
|
355
|
+
SELECT owner AS peer, CASE WHEN state = 'in_review' THEN 'reviewWait' ELSE 'active' END AS kind FROM tasks WHERE state NOT IN ('approved', 'cancelled', 'backlog') AND owner IS NOT NULL
|
|
356
|
+
UNION ALL
|
|
357
|
+
SELECT reserved AS peer, 'reserved' AS kind FROM tasks WHERE state = 'proposed' AND owner IS NULL AND reserved IS NOT NULL AND
|
|
358
|
+
json_extract((SELECT value FROM json_each(tasks.history) WHERE json_extract(value, '$.event') IN ('backlog pulled', 'backlog returned') ORDER BY CAST(key AS INTEGER) DESC LIMIT 1), '$.planning.pull') = 1
|
|
359
|
+
) GROUP BY peer, kind`).all() as { peer: string; kind: "active" | "reviewWait" | "reserved"; n: number }[];
|
|
360
|
+
const result: Record<string, { active: number; reviewWait: number; reserved: number }> = {};
|
|
361
|
+
for (const row of rows) (result[row.peer] ??= { active: 0, reviewWait: 0, reserved: 0 })[row.kind] = row.n;
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
342
365
|
recordTaskOffer(id: number, offer: TaskOffer, expectedFlowRevision: number): void {
|
|
343
366
|
const task = this.get(id);
|
|
344
367
|
if (!task || task.owner !== offer.owner || isClosedTaskState(task.state) || task.history.length !== expectedFlowRevision || offer.assignmentFlowRevision !== expectedFlowRevision) throw new Error("task offer changed");
|
|
@@ -356,6 +379,12 @@ export class Board {
|
|
|
356
379
|
if (!/^[a-z_]{1,64}:#[1-9][0-9]{0,9}:log$/.test(marker)) throw new Error("invalid band notice marker");
|
|
357
380
|
return this.db.query("INSERT OR IGNORE INTO band_notices(marker, at) VALUES(?, ?)").run(marker, Date.now()).changes > 0;
|
|
358
381
|
}
|
|
382
|
+
/** True when the (id, version, planner) marker was new and is now recorded; false when it was announced before. */
|
|
383
|
+
recordAgentDriftNotice(marker: string): boolean {
|
|
384
|
+
if (!/^[a-z][a-z0-9-]{0,31}:[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}@[0-9A-Za-z][0-9A-Za-z._-]{0,63}$/.test(marker)) throw new Error("invalid agent drift marker");
|
|
385
|
+
return this.db.query("INSERT OR IGNORE INTO agent_drift_notices(marker, at) VALUES(?, ?)").run(marker, Date.now()).changes > 0;
|
|
386
|
+
}
|
|
387
|
+
forgetAgentDriftNotice(marker: string): void { this.db.query("DELETE FROM agent_drift_notices WHERE marker=?").run(marker); }
|
|
359
388
|
clearStall(task: number): void { this.taskWrite(() => this.db.query("DELETE FROM task_stalls WHERE task=?").run(task)); }
|
|
360
389
|
stalls(): TaskStall[] {
|
|
361
390
|
return (this.db.query("SELECT payload FROM task_stalls ORDER BY task").all() as { payload: string }[]).flatMap(row => {
|
|
@@ -381,7 +410,7 @@ export class Board {
|
|
|
381
410
|
|
|
382
411
|
/** The only way a task changes. Validates the move, records who did what, returns the new row. */
|
|
383
412
|
/** A `plan` in the patch replaces the old one whole: a new plan is the owner's current intent, not an addition. */
|
|
384
|
-
update(id: number, by: PeerId, event: string, patch: Partial<Pick<Task, "state" | "owner" | "planner" | "reviewer" | "reserved" | "refs" | "plan" | "rejections" | "signals" | "sdlc" | "role" | "cancelled" | "codeSymbols">>, note?: string, extra: Pick<HistoryEntry, "plannerReason" | "planAuthor" | "selfReview" | "profile" | "sweep" | "reason" | "from" | "capacity" | "withheld" | "noWrites" | "reviewCost" | "reviewContext" | "reviewFindings" | "unmetCriteria" | "deliverables" | "planObservation" | "planDeparture" | "planDepartureInvalidated" | keyof ReviewerHistory> = {}, expectedFlowRevision?: number, codeSymbolsAuthority?: typeof HOST_CODE_SYMBOLS): Task {
|
|
413
|
+
update(id: number, by: PeerId, event: string, patch: Partial<Pick<Task, "state" | "owner" | "planner" | "reviewer" | "reserved" | "refs" | "plan" | "rejections" | "signals" | "sdlc" | "role" | "cancelled" | "codeSymbols">>, note?: string, extra: Pick<HistoryEntry, "plannerReason" | "planAuthor" | "selfReview" | "profile" | "sweep" | "reason" | "from" | "capacity" | "withheld" | "noWrites" | "reviewCost" | "reviewContext" | "reviewScope" | "evidenceOnly" | "reviewFindings" | "unmetCriteria" | "deliverables" | "planObservation" | "planDeparture" | "planDepartureInvalidated" | keyof ReviewerHistory> = {}, expectedFlowRevision?: number, codeSymbolsAuthority?: typeof HOST_CODE_SYMBOLS): Task {
|
|
385
414
|
const task = this.get(id);
|
|
386
415
|
if (!task) throw new Error(`no task #${id}`);
|
|
387
416
|
if (expectedFlowRevision !== undefined && (!Number.isSafeInteger(expectedFlowRevision) || expectedFlowRevision < 0 || task.history.length !== expectedFlowRevision)) throw new Error("task flow revision changed");
|
|
@@ -437,7 +466,7 @@ export class Board {
|
|
|
437
466
|
// Disabling reporting must not let plan or repository refs edits/reverts resurrect prior consent.
|
|
438
467
|
const repositoryRefsChanged = patch.refs !== undefined && JSON.stringify([task.refs.repo, task.refs.paths]) !== JSON.stringify([next.refs.repo, next.refs.paths]);
|
|
439
468
|
const departureInvalidated = extra.planDepartureInvalidated === true || hasDeparture && (planChanged || repositoryRefsChanged);
|
|
440
|
-
const history = [...task.history, { at: Date.now(), by, event, ...plannerHistory, ...authorHistory, ...(extra.selfReview === true ? { selfReview: true as const } : {}), ...(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 } : {}), ...(unmetCriteria?.length ? { unmetCriteria } : {}), ...(deliverables ? { deliverables } : {}), ...(extra.reviewContext === "fresh" || extra.reviewContext === "continuing" ? { reviewContext: extra.reviewContext } : {}), ...(extra.reviewCost ? { reviewCost: extra.reviewCost } : {}), ...(event === "done" && extra.noWrites === true ? { noWrites: true as const } : {}) }];
|
|
469
|
+
const history = [...task.history, { at: Date.now(), by, event, ...plannerHistory, ...authorHistory, ...(extra.selfReview === true ? { selfReview: true as const } : {}), ...(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 } : {}), ...(unmetCriteria?.length ? { unmetCriteria } : {}), ...(deliverables ? { deliverables } : {}), ...(extra.reviewContext === "fresh" || extra.reviewContext === "continuing" ? { reviewContext: extra.reviewContext } : {}), ...(extra.reviewScope === "code" ? { reviewScope: "code" as const } : {}), ...(extra.evidenceOnly === true && event === "changes_requested" ? { evidenceOnly: true as const } : {}), ...(extra.reviewCost ? { reviewCost: extra.reviewCost } : {}), ...(event === "done" && extra.noWrites === true ? { noWrites: true as const } : {}) }];
|
|
441
470
|
this.taskWrite(() => {
|
|
442
471
|
const result = this.db.query("UPDATE tasks SET state = ?, owner = ?, reviewer = ?, reserved = ?, refs = ?, plan = ?, rejections = ?, signals = ?, history = ?, sdlc = ?, task_role = ?, cancelled = ?, updated = ?, planner = ? WHERE id = ?")
|
|
443
472
|
.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(), planner, id);
|