@staix/agent-hub 0.26.0 → 0.28.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 +30 -0
- package/README.md +13 -4
- package/docs/agent-notes/adapters.md +4 -2
- package/docs/agent-notes/bus.md +4 -4
- package/docs/agent-notes/daemon.md +8 -4
- package/docs/agent-notes/tasks.md +7 -2
- package/docs/agent-notes/tests.md +6 -5
- package/docs/design/adapters.md +76 -0
- package/docs/design/agent-hub-architecture.md +67 -0
- package/docs/design/agent-hub.md +52 -0
- package/docs/design/bus.md +48 -0
- package/docs/design/task-board.md +86 -0
- package/docs/dogfood.md +19 -2
- package/docs/operations.md +337 -15
- package/docs/profiles.md +2 -0
- package/docs/quickstart.md +15 -3
- package/docs/sdlc.md +4 -4
- package/docs/security.md +9 -4
- package/docs/smoke.md +2 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +3 -1
- package/docs/specs/2026-11-01-research-campaigns-design.md +12 -1
- package/docs/verification/2026-10-31-codex-0166-reviewer-isolation.md +2 -0
- package/docs/verification/2026-11-01-0.27.0.md +49 -0
- package/docs/verification/2026-11-02-0.28.0.md +54 -0
- package/docs/verification/2026-11-02-autonomy-t4.md +39 -0
- package/docs/verification/2026-11-02-codex-0166-reviewer-tool-call.md +53 -0
- package/docs/verification/2026-11-02-control-write-after.json +139 -0
- package/docs/verification/2026-11-02-control-write-baseline.json +127 -0
- package/docs/verification/2026-11-02-control-write-guarded.json +127 -0
- package/docs/verification/2026-11-02-control-write-outcomes.md +65 -0
- package/docs/verification/2026-11-02-orca-1.4.245-cli-contract.md +51 -0
- package/docs/verified.json +13 -13
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +344 -116
- package/src/adapters/claude-channel.ts +12 -2
- package/src/adapters/compat.ts +1 -1
- package/src/adapters/definitions/native-cli-claude-setup.ts +31 -9
- package/src/adapters/definitions/native-cli-entry-jcode.ts +33 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +2 -2
- package/src/adapters/jcode.ts +37 -4
- package/src/adapters/qwen-dual.ts +3 -2
- package/src/cli/agents.ts +4 -2
- package/src/cli/bench.ts +4 -2
- package/src/cli/board-read.ts +25 -0
- package/src/cli/claude-homes.ts +2 -2
- package/src/cli/console-state.ts +8 -3
- package/src/cli/help.ts +8 -6
- package/src/cli/identity.ts +14 -6
- package/src/cli/init-prompt.ts +179 -0
- package/src/cli/init-settings.ts +229 -0
- package/src/cli/init.ts +102 -9
- package/src/cli/launch.ts +3 -1
- package/src/cli/main.ts +112 -36
- package/src/cli/output.ts +14 -0
- package/src/cli/setup-homes.ts +18 -3
- package/src/cli/specs.ts +150 -0
- package/src/cli/study-output.ts +56 -0
- package/src/cli/study.ts +26 -3
- package/src/cli/terminal-recovery.ts +35 -11
- package/src/hub/action-policy.ts +5 -3
- package/src/hub/board.ts +56 -3
- package/src/hub/bus.ts +54 -7
- package/src/hub/child-process.ts +6 -4
- package/src/hub/config-apply.ts +4 -1
- package/src/hub/config-edit.ts +1 -1
- package/src/hub/config-tier.ts +1 -0
- package/src/hub/control-client.ts +28 -4
- package/src/hub/cost-governance.ts +22 -6
- package/src/hub/crash.ts +20 -0
- package/src/hub/daemon.ts +324 -64
- package/src/hub/delivery-hints.ts +6 -3
- package/src/hub/delivery-journal.ts +14 -3
- package/src/hub/events.ts +2 -1
- package/src/hub/hub-tools.ts +15 -9
- package/src/hub/no-writes.ts +1 -1
- package/src/hub/peers.ts +1 -1
- package/src/hub/project-runtime-state.ts +38 -13
- package/src/hub/review-brief.ts +18 -0
- package/src/hub/routing.ts +7 -3
- package/src/hub/sdlc-release-authorization.ts +11 -5
- package/src/hub/session-identity.ts +1 -1
- package/src/hub/settings.ts +2 -0
- package/src/hub/specs-config.ts +43 -0
- package/src/hub/specs-task.ts +108 -0
- package/src/hub/specs.ts +1008 -0
- package/src/hub/task-list-page.ts +75 -0
- package/src/hub/task-sweep.ts +10 -1
- package/src/hub/tasks.ts +448 -40
- package/src/hub/terminal-open.ts +10 -5
- package/src/hub/tool-request.ts +131 -0
- package/src/ui/office-model.ts +17 -1
- package/src/ui/office-space.ts +1 -1
- package/templates/config.json +1 -0
- package/templates/presets/confirmation-matrix.json +5 -5
- package/templates/profiles/dev.json +1 -0
- package/templates/profiles/general.json +1 -0
- package/templates/profiles/research.json +1 -0
- package/templates/profiles/writing.json +1 -0
- package/templates/release-guard.block.md +2 -1
- package/templates/specs.block.md +5 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PreEffectToolRefusal } from "./tool-refusal.ts";
|
|
2
|
+
|
|
3
|
+
/** #1083: `hub_task_list` pages. 628 tasks were 2.4 MB, over every agent tool-output limit. */
|
|
4
|
+
export const LIST_DEFAULT_LIMIT = 50;
|
|
5
|
+
export const LIST_MAX_LIMIT = 200;
|
|
6
|
+
|
|
7
|
+
export interface ListQuery {
|
|
8
|
+
limit: number;
|
|
9
|
+
/** Keyset position: the `updated` and `id` of the last row of the previous page. */
|
|
10
|
+
after?: { updated: number; id: number };
|
|
11
|
+
/** Exactly these tasks, full rows, no paging. */
|
|
12
|
+
ids?: number[];
|
|
13
|
+
/** Full rows instead of the compact id/state/owner/reviewer/title row. */
|
|
14
|
+
detail: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PAGING_KEYS = ["limit", "cursor", "ids", "detail"] as const;
|
|
18
|
+
|
|
19
|
+
/** True when the caller named any paging argument, whoever it is. */
|
|
20
|
+
export const hasPagingArgs = (args: Record<string, unknown>): boolean => PAGING_KEYS.some(key => args[key] !== undefined);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read and validate the paging arguments. Every refusal names the cause and the next step, because it is the only signal a model gets for a rejected call.
|
|
24
|
+
* `agent` callers always page; the person's console keeps the full array unless it names a paging argument, so `ahub tasks` output stays stable.
|
|
25
|
+
*/
|
|
26
|
+
export function parseListQuery(args: Record<string, unknown>, agent: boolean): ListQuery | undefined {
|
|
27
|
+
if (!agent && !hasPagingArgs(args)) return undefined;
|
|
28
|
+
const { limit, cursor, ids, detail } = args;
|
|
29
|
+
if (detail !== undefined && typeof detail !== "boolean") throw new PreEffectToolRefusal("detail must be true or false; omit it for compact rows (id, state, owner, reviewer, title)");
|
|
30
|
+
if (ids !== undefined) {
|
|
31
|
+
if (!Array.isArray(ids) || !ids.length || ids.length > LIST_MAX_LIMIT || ids.some(id => !Number.isSafeInteger(id) || (id as number) < 1) || new Set(ids).size !== ids.length) {
|
|
32
|
+
throw new PreEffectToolRefusal(`ids must be 1 to ${LIST_MAX_LIMIT} distinct positive task numbers; page with limit and cursor to read more than ${LIST_MAX_LIMIT} tasks`);
|
|
33
|
+
}
|
|
34
|
+
if (limit !== undefined || cursor !== undefined) throw new PreEffectToolRefusal("ids returns every task you list; drop limit and cursor, or drop ids to page the board");
|
|
35
|
+
return { limit: ids.length, ids: ids as number[], detail: true };
|
|
36
|
+
}
|
|
37
|
+
if (limit !== undefined && (!Number.isSafeInteger(limit) || (limit as number) < 1 || (limit as number) > LIST_MAX_LIMIT)) {
|
|
38
|
+
throw new PreEffectToolRefusal(`limit must be a whole number from 1 to ${LIST_MAX_LIMIT} (default ${LIST_DEFAULT_LIMIT}); read the rest with the cursor the reply gives`);
|
|
39
|
+
}
|
|
40
|
+
let after: ListQuery["after"];
|
|
41
|
+
if (cursor !== undefined) {
|
|
42
|
+
const match = typeof cursor === "string" && cursor.length <= 40 ? /^([0-9]{1,16})\.([1-9][0-9]{0,15})$/.exec(cursor) : null;
|
|
43
|
+
if (!match) throw new PreEffectToolRefusal("cursor must be the `next` value from the previous hub_task_list reply, unchanged; start again without a cursor");
|
|
44
|
+
after = { updated: Number(match[1]), id: Number(match[2]) };
|
|
45
|
+
}
|
|
46
|
+
return { limit: (limit as number | undefined) ?? LIST_DEFAULT_LIMIT, ...(after ? { after } : {}), detail: detail === true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ListPage<T> {
|
|
50
|
+
rows: T[];
|
|
51
|
+
/** Rows that matched the filters, before the cursor and the limit. */
|
|
52
|
+
total: number;
|
|
53
|
+
/** Pass as `cursor` for the next page; absent on the last page. */
|
|
54
|
+
next?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Newest updated first, ties by id descending, so a cursor stays valid while other tasks change. */
|
|
58
|
+
export function pageTasks<T extends { id: number; updated: number }>(rows: readonly T[], query: ListQuery): ListPage<T> {
|
|
59
|
+
const total = rows.length;
|
|
60
|
+
if (query.ids) {
|
|
61
|
+
const wanted = new Set(query.ids);
|
|
62
|
+
const found = rows.filter(row => wanted.has(row.id)).sort((a, b) => query.ids!.indexOf(a.id) - query.ids!.indexOf(b.id));
|
|
63
|
+
return { rows: found, total: found.length };
|
|
64
|
+
}
|
|
65
|
+
const ordered = [...rows].sort((a, b) => b.updated - a.updated || b.id - a.id);
|
|
66
|
+
const after = query.after;
|
|
67
|
+
const rest = after ? ordered.filter(row => row.updated < after.updated || row.updated === after.updated && row.id < after.id) : ordered;
|
|
68
|
+
const page = rest.slice(0, query.limit), last = page[page.length - 1];
|
|
69
|
+
return { rows: page, total, ...(rest.length > page.length && last ? { next: `${last.updated}.${last.id}` } : {}) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What a compact row keeps: enough to choose a task, never a body, plan or history. */
|
|
73
|
+
export function compactRow(view: Record<string, unknown>): Record<string, unknown> {
|
|
74
|
+
return { id: view.id, state: view.state, owner: view.owner ?? null, reviewer: view.reviewer ?? null, title: view.title };
|
|
75
|
+
}
|
package/src/hub/task-sweep.ts
CHANGED
|
@@ -66,8 +66,13 @@ export function nextSweep(task: Task, config: TaskSweepConfig, now: number, read
|
|
|
66
66
|
return { kind, activity, step: previous ? (previous.step + 1) as 2 | 3 : 1, at: now };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** A planned start is the earliest automatic start time; the card is eligible at that boundary. */
|
|
70
|
+
export function plannedStartPending(task: Pick<Task, "plannedStart">, now: number): boolean {
|
|
71
|
+
return task.plannedStart != null && (!Number.isFinite(now) || task.plannedStart > now);
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
export interface GroomingItem {
|
|
70
|
-
kind: "text-dependency" | "overlap" | "duplicate" | "over-wip" | "stale" | "superseded" | "blocked" | "closed-issue";
|
|
75
|
+
kind: "text-dependency" | "overlap" | "duplicate" | "over-wip" | "stale" | "superseded" | "blocked" | "closed-issue" | "scheduled";
|
|
71
76
|
task: number;
|
|
72
77
|
related?: number[];
|
|
73
78
|
code?: string;
|
|
@@ -75,6 +80,8 @@ export interface GroomingItem {
|
|
|
75
80
|
}
|
|
76
81
|
export interface PlannerGroomingOptions {
|
|
77
82
|
repository: string;
|
|
83
|
+
/** Host clock snapshot; omitted by legacy pure callers. */
|
|
84
|
+
now?: number;
|
|
78
85
|
limits: Record<string, number>;
|
|
79
86
|
wip: Record<string, number>;
|
|
80
87
|
states: Record<string, string>;
|
|
@@ -162,6 +169,8 @@ export function plannerGrooming(tasks: readonly Task[], options: PlannerGrooming
|
|
|
162
169
|
}
|
|
163
170
|
if (replacements.size) add({ kind: "superseded", task: task.id, related: [...replacements].sort((a, b) => a - b) });
|
|
164
171
|
if (task.state === "cancelled") continue; // Closed rows contribute supersession evidence only.
|
|
172
|
+
if ((task.state === "backlog" || task.state === "proposed" && !task.owner) && options.now !== undefined && plannedStartPending(task, options.now))
|
|
173
|
+
add({ kind: "scheduled", task: task.id });
|
|
165
174
|
const issues = new Set<number>();
|
|
166
175
|
for (const issue of [task.external?.issue, task.sdlc?.issue]) {
|
|
167
176
|
if (issue === undefined) continue;
|