@staix/agent-hub 0.27.0 → 0.29.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 +33 -0
- package/README.md +11 -3
- package/docs/agent-notes/adapters.md +4 -2
- package/docs/agent-notes/bus.md +4 -4
- package/docs/agent-notes/daemon.md +9 -3
- package/docs/agent-notes/local-worker.md +3 -1
- package/docs/agent-notes/tasks.md +7 -0
- package/docs/agent-notes/tests.md +7 -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 +21 -3
- package/docs/operations.md +370 -12
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +19 -3
- package/docs/sdlc.md +2 -2
- package/docs/security.md +24 -4
- package/docs/smoke.md +2 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +2 -0
- package/docs/verification/2026-10-31-codex-0166-reviewer-isolation.md +2 -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/verification/2026-11-03-0.29.0.md +59 -0
- package/docs/verification/2026-11-03-control-write-stall-windows.json +241 -0
- package/docs/verification/2026-11-03-control-write-stall-windows.md +20 -0
- package/docs/verification/2026-11-03-live-board-latency.json +556 -0
- package/docs/verification/2026-11-03-live-board-latency.md +64 -0
- package/docs/verified.json +14 -14
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +857 -125
- 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 +6 -5
- package/src/cli/identity.ts +9 -1
- 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 +106 -35
- package/src/cli/output.ts +19 -0
- package/src/cli/recovery-package.ts +9 -0
- package/src/cli/setup-homes.ts +18 -3
- package/src/cli/specs.ts +150 -0
- package/src/cli/terminal-recovery.ts +35 -11
- package/src/hub/board.ts +59 -5
- 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 +43 -4
- package/src/hub/control-write-trace.ts +409 -0
- package/src/hub/cost-governance.ts +22 -6
- package/src/hub/crash.ts +20 -0
- package/src/hub/daemon.ts +470 -98
- package/src/hub/delivery-hints.ts +6 -3
- package/src/hub/delivery-journal.ts +14 -3
- package/src/hub/hub-tools.ts +10 -6
- package/src/hub/peers.ts +1 -1
- package/src/hub/review-brief.ts +18 -0
- package/src/hub/routing.ts +14 -3
- package/src/hub/runtime-capture-prune.ts +4 -1
- package/src/hub/sdlc-release-authorization.ts +13 -4
- package/src/hub/sdlc-release.ts +23 -10
- package/src/hub/sdlc.ts +15 -2
- 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/task-worktree.ts +331 -0
- package/src/hub/tasks.ts +606 -53
- package/src/hub/terminal-open.ts +10 -5
- package/src/hub/tool-request.ts +131 -0
- package/src/local/deny.ts +49 -0
- package/src/local/tools.ts +4 -2
- package/src/memory/brief.ts +5 -0
- package/src/memory/client.ts +5 -2
- package/src/ui/office-model.ts +17 -1
- package/src/ui/office-space.ts +1 -1
- package/templates/config.json +1 -0
- 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/specs.block.md +5 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { BriefFile } from "./review-brief.ts";
|
|
2
|
+
import { parseSpecsConfig, SpecsEngine, type SpecsConfig, type SpecsCheckReport, type SpecsCoverReport } from "./specs.ts";
|
|
3
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { realPath } from "./project.ts";
|
|
6
|
+
|
|
7
|
+
export interface SpecAck { id: string; reason: string }
|
|
8
|
+
export interface SpecRef { id: string; path: string; title: string; reason?: string }
|
|
9
|
+
export interface SpecTaskReport {
|
|
10
|
+
head: string;
|
|
11
|
+
complete: boolean;
|
|
12
|
+
errors: number;
|
|
13
|
+
unchanged: SpecRef[];
|
|
14
|
+
changed: SpecRef[];
|
|
15
|
+
truncated?: boolean;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SpecsStatus { specCount: number; documents: number; errors: number; stale: number; truncated: boolean }
|
|
19
|
+
|
|
20
|
+
const UNAVAILABLE = new Set(["discovery-unavailable", "snapshot-unavailable", "coverage-unavailable"]);
|
|
21
|
+
|
|
22
|
+
/** Host-bound repository inspection only. Tasks owns caller/privacy projection and every board write. */
|
|
23
|
+
export class SpecTaskInspector {
|
|
24
|
+
private readonly engines = new Map<string, SpecsEngine>();
|
|
25
|
+
private readonly root: string;
|
|
26
|
+
constructor(root: string, private readonly config: () => SpecsConfig) { this.root = realPath(root); }
|
|
27
|
+
|
|
28
|
+
/** A positive absence only; unsafe or incomplete reads must not claim the block is missing. */
|
|
29
|
+
missingBlock(): boolean | undefined {
|
|
30
|
+
const file = join(this.root, "AGENTS.md");
|
|
31
|
+
let fd: number | undefined;
|
|
32
|
+
try {
|
|
33
|
+
const before = lstatSync(file);
|
|
34
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > 256 * 1024) return undefined;
|
|
35
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
36
|
+
const opened = fstatSync(fd);
|
|
37
|
+
if (opened.dev !== before.dev || opened.ino !== before.ino) return undefined;
|
|
38
|
+
const bytes = Buffer.alloc(before.size + 1);
|
|
39
|
+
let count = 0;
|
|
40
|
+
while (count < bytes.length) { const got = readSync(fd, bytes, count, bytes.length - count, count); if (!got) break; count += got; }
|
|
41
|
+
const text = bytes.subarray(0, count).toString("utf8"), after = lstatSync(file);
|
|
42
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeMs !== before.mtimeMs) return undefined;
|
|
43
|
+
const begin = text.indexOf("<!-- AGENT_HUB_SPECS:BEGIN"), end = text.indexOf("<!-- AGENT_HUB_SPECS:END");
|
|
44
|
+
return begin < 0 || end < begin;
|
|
45
|
+
} catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? true : undefined; }
|
|
46
|
+
finally { if (fd !== undefined) try { closeSync(fd); } catch { /* The optional hint must not break status. */ } }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private engine(commit?: string): SpecsEngine {
|
|
50
|
+
const config = parseSpecsConfig(this.config());
|
|
51
|
+
const key = JSON.stringify([config.roots, config.stamps_file, commit ?? "live"]);
|
|
52
|
+
const cached = this.engines.get(key);
|
|
53
|
+
if (cached) { this.engines.delete(key); this.engines.set(key, cached); return cached; }
|
|
54
|
+
const engine = new SpecsEngine(this.root, config, commit === undefined ? {} : { commit });
|
|
55
|
+
this.engines.set(key, engine);
|
|
56
|
+
// One live view and a small number of immutable submitted-head views, never an unbounded PR cache.
|
|
57
|
+
if (this.engines.size > 4) this.engines.delete(this.engines.keys().next().value!);
|
|
58
|
+
return engine;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
notice(paths: readonly string[]): SpecsCoverReport | undefined {
|
|
62
|
+
const config = this.config();
|
|
63
|
+
if (!config.enabled || config.review === "off" || !paths.length) return undefined;
|
|
64
|
+
const report = this.engine().covers(paths);
|
|
65
|
+
return report.specs.length ? report : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
read(action: "check", paths: readonly string[]): SpecsCheckReport;
|
|
69
|
+
read(action: "covers", paths: readonly string[]): SpecsCoverReport;
|
|
70
|
+
read(action: "check" | "covers", paths: readonly string[]): SpecsCheckReport | SpecsCoverReport;
|
|
71
|
+
read(action: "check" | "covers", paths: readonly string[]): SpecsCheckReport | SpecsCoverReport {
|
|
72
|
+
return action === "check" ? this.engine().check() : this.engine().covers(paths);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
status(): SpecsStatus | undefined {
|
|
76
|
+
const config = this.config();
|
|
77
|
+
if (!config.enabled) return undefined;
|
|
78
|
+
const report = this.engine().check();
|
|
79
|
+
if (!report.specCount && !report.freshness.length && !(config.stamps_file && report.errors)) return undefined;
|
|
80
|
+
return { specCount: report.specCount, documents: report.freshness.length, errors: report.errors, stale: report.stale, truncated: report.truncated };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
inspect(head: string, changedFiles: readonly BriefFile[] | undefined, options: { complete: boolean; acks?: readonly SpecAck[] }): SpecTaskReport | undefined {
|
|
84
|
+
const config = this.config();
|
|
85
|
+
if (!config.enabled || config.review === "off") return undefined;
|
|
86
|
+
// Commitless and explicitly empty changes carry no coverage obligation.
|
|
87
|
+
if (!/^[0-9a-f]{40}$/.test(head) || changedFiles?.length === 0 && options.complete) return undefined;
|
|
88
|
+
let engine: SpecsEngine, check: SpecsCheckReport;
|
|
89
|
+
try { engine = this.engine(head); check = engine.check(); }
|
|
90
|
+
catch { return { head, complete: false, errors: 0, unchanged: [], changed: [], reason: "submitted spec tree is unavailable" }; }
|
|
91
|
+
if (!check.specCount && !check.freshness.length && !config.stamps_file && !check.truncated && !check.errors) return undefined;
|
|
92
|
+
const coverage = engine.list();
|
|
93
|
+
const unavailable = coverage.items.some(item => UNAVAILABLE.has(item.code));
|
|
94
|
+
if (!options.complete || !changedFiles || coverage.truncated || unavailable) return { head, complete: false, errors: check.errors, unchanged: [], changed: [], truncated: coverage.truncated, reason: "spec coverage or submitted changed-file evidence is incomplete" };
|
|
95
|
+
const changedPaths = new Set(changedFiles.flatMap(file => [file.path, ...(file.oldPath ? [file.oldPath] : [])]));
|
|
96
|
+
const covers = engine.covers([...changedPaths]);
|
|
97
|
+
if (covers.items.some(item => UNAVAILABLE.has(item.code) || item.code === "invalid-cover-path")) return { head, complete: false, errors: check.errors, unchanged: [], changed: [], reason: "changed-file path coverage is unavailable" };
|
|
98
|
+
const acks = new Map((options.acks ?? []).map(ack => [ack.id, ack.reason]));
|
|
99
|
+
const changed: SpecRef[] = [], unchanged: SpecRef[] = [];
|
|
100
|
+
for (const spec of covers.specs) {
|
|
101
|
+
const row: SpecRef = { ...spec, ...(acks.has(spec.id) ? { reason: acks.get(spec.id)! } : {}) };
|
|
102
|
+
(changedPaths.has(spec.path) ? changed : unchanged).push(row);
|
|
103
|
+
}
|
|
104
|
+
const touchedSpec = coverage.specs.some(spec => changedPaths.has(spec.path));
|
|
105
|
+
if (!changed.length && !unchanged.length && !touchedSpec && !covers.truncated) return undefined;
|
|
106
|
+
return { head, complete: !covers.truncated, errors: check.errors, changed, unchanged, truncated: covers.truncated };
|
|
107
|
+
}
|
|
108
|
+
}
|