@staix/agent-hub 0.16.0 → 0.18.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 +34 -0
- package/README.md +2 -2
- package/docs/agent-notes/README.md +5 -0
- package/docs/agent-notes/adapters.md +20 -5
- package/docs/agent-notes/benchmarks.md +1 -0
- package/docs/agent-notes/daemon.md +5 -3
- package/docs/agent-notes/local-worker.md +1 -0
- package/docs/agent-notes/tasks.md +9 -3
- package/docs/agent-notes/tests.md +6 -3
- package/docs/dogfood.md +21 -4
- package/docs/memory.md +1 -1
- package/docs/operations.md +150 -34
- package/docs/profiles.md +36 -0
- package/docs/quickstart.md +14 -4
- package/docs/roles.md +1 -0
- package/docs/sdlc.md +39 -13
- package/docs/security.md +41 -9
- package/docs/smoke.md +51 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +56 -10
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +3 -1
- package/docs/verification/2026-10-25-0.17.0.md +74 -0
- package/docs/verification/2026-10-25-0.18.0.md +76 -0
- package/docs/verification/2026-10-25-codex-0164-reviewer-tool-call.md +52 -0
- package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
- package/docs/verification/2026-10-25-kimi-231-reviewer-compat.md +48 -0
- package/docs/verification/2026-10-25-kimi-usage-ac11.md +48 -0
- package/docs/verification/2026-10-25-qwen-dual-output-hub-e2e.md +23 -0
- package/docs/verification/2026-10-25-qwen-dual-output-t0.md +20 -0
- package/docs/verification/2026-10-25-remote-docker-transport.md +46 -0
- package/docs/verification/2026-10-25-reviewer-no-verdict-diagnostics.md +35 -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 +28 -19
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1115 -192
- package/src/adapters/codex-review-isolation.ts +10 -1
- package/src/adapters/compat.ts +7 -2
- package/src/adapters/conformance/enrollment.ts +18 -4
- package/src/adapters/conformance/factories.ts +4 -1
- package/src/adapters/conformance/kit.ts +11 -0
- package/src/adapters/definitions/builtin-metadata.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-qwen.ts +32 -4
- package/src/adapters/definitions/native-cli-preview-qwen.ts +9 -1
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/definitions/native-recovery.ts +8 -0
- package/src/adapters/definitions/qwen.ts +7 -4
- package/src/adapters/local-worker.ts +5 -3
- package/src/adapters/qwen-dual.ts +578 -0
- package/src/adapters/registry-types.ts +5 -1
- package/src/adapters/registry.ts +9 -1
- package/src/adapters/reviewer-mcp.ts +12 -6
- package/src/adapters/reviewer-print.ts +100 -17
- 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 +190 -69
- package/src/cli/output.ts +7 -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/cli/upgrade.ts +4 -3
- package/src/hub/board.ts +17 -0
- package/src/hub/check-runner.ts +252 -22
- 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-tier.ts +1 -1
- package/src/hub/daemon.ts +487 -94
- 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 +2 -1
- package/src/hub/hub-tools.ts +2 -2
- package/src/hub/memory-plan-guard.ts +8 -33
- package/src/hub/operational-authority.ts +60 -0
- package/src/hub/operational-view.ts +141 -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 +80 -0
- package/src/hub/read-only-shell.ts +162 -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-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/tasks.ts +266 -28
- package/src/local/tools.ts +17 -4
- package/src/memory/brief.ts +45 -5
- package/src/ui/index.html +2 -1
package/src/hub/settings.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { operationalRosterSetting, operationalSetting } from "./operational-authority.ts";
|
|
1
2
|
import { ACTION_CLASSES } from "./action-class.ts";
|
|
2
3
|
import { definitionOf, type KindMap, type ProjectAdapterCatalog, ADAPTERS, PLANNER_PEERS, SUPPORTED_PEERS, START_MODE_PEERS } from "../adapters/registry.ts";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
@@ -193,12 +194,15 @@ export function parseSettingText(def: SettingDef, text: string): unknown {
|
|
|
193
194
|
}
|
|
194
195
|
export const settingText = (value: unknown): string => value === null || value === undefined ? "(not set)" : Array.isArray(value) ? value.join(",") || "(none)" : String(value);
|
|
195
196
|
|
|
197
|
+
export type SettingSource = "dashboard" | "terminal" | "conductor";
|
|
198
|
+
export type SettingWriteOptions = { preserveUndo?: boolean };
|
|
196
199
|
/** `ordinary`: a dashboard session from `ahub ui`. `settings`: one from `ahub ui --settings`. `terminal`: `ahub settings`. */
|
|
197
|
-
export type SettingAuthority = "ordinary" | "settings" | "terminal";
|
|
200
|
+
export type SettingAuthority = "ordinary" | "settings" | "terminal" | "conductor";
|
|
198
201
|
/** Why this authority may not set this value, or undefined. Removing a stored value can raise, so it counts as one. */
|
|
199
202
|
export function settingRefusal(def: SettingDef | string, value: SettingValue, authority: SettingAuthority): string | undefined {
|
|
200
203
|
if (typeof def === "string") return def || "invalid setting definition";
|
|
201
204
|
if (def.locked) return def.locked;
|
|
205
|
+
if (authority === "conductor") return operationalSetting(def) || operationalRosterSetting(def) ? undefined : `${def.key} is not an operational conductor setting`;
|
|
202
206
|
if (def.risk === "safe" || authority !== "ordinary") return undefined;
|
|
203
207
|
if (value !== null && value === def.floor) return undefined;
|
|
204
208
|
return `${def.key} can widen what agents do without asking: open a settings session with ahub ui --settings, or use ahub settings in a terminal`;
|
|
@@ -244,10 +248,12 @@ function refuseTracked(cwd: string, file: StoredFile): void {
|
|
|
244
248
|
// Unknown is not untracked where there is a repository to ask; only a project outside git has nothing tracked.
|
|
245
249
|
if (tracked === undefined && configRepositoryPresent(cwd)) throw new Error(`git could not confirm that .agenthub/${file} is untracked; nothing was written`);
|
|
246
250
|
}
|
|
247
|
-
/**
|
|
248
|
-
function store(cwd: string, stateDir: string, file: StoredFile, key: string, previous: string | null, next: string | null): void {
|
|
251
|
+
/** Person writes keep the one previous version; conductor writes preserve that existing receipt. */
|
|
252
|
+
function store(cwd: string, stateDir: string, file: StoredFile, key: string, previous: string | null, next: string | null, options: SettingWriteOptions = {}): void {
|
|
249
253
|
refuseTracked(cwd, file);
|
|
250
254
|
replaceFile(join(cwd, ".agenthub", file), file, next);
|
|
255
|
+
// A conductor write never replaces, deletes or rebases the person's undo receipt. The ordinary digest fence still applies.
|
|
256
|
+
if(options.preserveUndo)return;
|
|
251
257
|
// The record follows the file: a write that failed leaves the earlier record valid, because the file is as it left it.
|
|
252
258
|
const record: UndoRecord = { file, key, previous, written: digest(next), at: Date.now() };
|
|
253
259
|
try {
|
|
@@ -282,7 +288,7 @@ export function valueAt(doc: unknown, path: readonly string[]): unknown {
|
|
|
282
288
|
* Sets (or with null removes) one key of `.agenthub/config.local.json`. `validate` gets a scratch project that holds
|
|
283
289
|
* the shared config and the candidate, and throws when the hub's own loader would refuse it: nothing is written then.
|
|
284
290
|
*/
|
|
285
|
-
export function writeConfigSetting(cwd: string, stateDir: string, def: SettingDef, value: SettingValue, validate: (scratch: string) => unknown): boolean {
|
|
291
|
+
export function writeConfigSetting(cwd: string, stateDir: string, def: SettingDef, value: SettingValue, validate: (scratch: string) => unknown, options: SettingWriteOptions = {}): boolean {
|
|
286
292
|
const file = join(cwd, ".agenthub", CONFIG_LOCAL);
|
|
287
293
|
const before = readText(file);
|
|
288
294
|
const doc = before === null ? {} : parseObject(before, CONFIG_LOCAL);
|
|
@@ -315,7 +321,7 @@ export function writeConfigSetting(cwd: string, stateDir: string, def: SettingDe
|
|
|
315
321
|
} else node[leaf] = value;
|
|
316
322
|
const text = `${JSON.stringify(doc, null, 2)}\n`;
|
|
317
323
|
checkConfigText(cwd, text, validate);
|
|
318
|
-
store(cwd, stateDir, CONFIG_LOCAL, def.key, before, text);
|
|
324
|
+
store(cwd, stateDir, CONFIG_LOCAL, def.key, before, text, options);
|
|
319
325
|
return true;
|
|
320
326
|
}
|
|
321
327
|
/** Runs `validate` on a scratch project holding the shared config and this candidate for the machine-local one. */
|
package/src/hub/snapshots.ts
CHANGED
|
@@ -546,6 +546,66 @@ export interface TurnRecord {
|
|
|
546
546
|
native: string | null;
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Parse `git status --porcelain=v2 -z` output into the paths of dirty submodules (#798 D2). A submodule's
|
|
551
|
+
* uncommitted edits never move the superproject's gitlink, so a diff of two snapshot trees cannot see them.
|
|
552
|
+
* Submodule entries are the only ones whose mode field starts with `S`; `N` (normal) entries never qualify.
|
|
553
|
+
*/
|
|
554
|
+
export function parseDirtySubmodules(z: string): string[] {
|
|
555
|
+
const entries = z.split("\0");
|
|
556
|
+
const found = new Set<string>();
|
|
557
|
+
for (let i = 0; i < entries.length; i++) {
|
|
558
|
+
const entry = entries[i]!;
|
|
559
|
+
if (entry === "") continue;
|
|
560
|
+
const kind = entry[0];
|
|
561
|
+
if (kind === "1") {
|
|
562
|
+
// `1 XY SUB mH mI mW hH hI PATH`: the rest carries 7 spaces, so the path starts after the last one.
|
|
563
|
+
const rest = entry.slice(2);
|
|
564
|
+
const cut = nthSpace(rest, 6);
|
|
565
|
+
const path = cut < 0 ? "" : rest.slice(cut + 1);
|
|
566
|
+
if (path && submoduleField(rest.slice(0, cut))) found.add(path);
|
|
567
|
+
} else if (kind === "2") {
|
|
568
|
+
// `2 XY SUB mH mI mW hH hI Xscore PATH`, then the next entry is the original path: skip it.
|
|
569
|
+
const rest = entry.slice(2);
|
|
570
|
+
const cut = nthSpace(rest, 7);
|
|
571
|
+
const path = cut < 0 ? "" : rest.slice(cut + 1);
|
|
572
|
+
if (path && submoduleField(rest.slice(0, cut))) found.add(path);
|
|
573
|
+
i++;
|
|
574
|
+
} else if (kind === "u") {
|
|
575
|
+
// `u XY SUB m1 m2 m3 mW h1 h2 h3 PATH`: nine spaces in the rest, then the path.
|
|
576
|
+
const rest = entry.slice(2);
|
|
577
|
+
const cut = nthSpace(rest, 8);
|
|
578
|
+
const path = cut < 0 ? "" : rest.slice(cut + 1);
|
|
579
|
+
if (path && submoduleField(rest.slice(0, cut))) found.add(path);
|
|
580
|
+
}
|
|
581
|
+
// `?`, `!` and `#` entries carry no mode field: ignore them.
|
|
582
|
+
}
|
|
583
|
+
return [...found].sort();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** porcelain-v2 fields are `XY SUB ...`: the submodule marker is the first character of the second field. */
|
|
587
|
+
const submoduleField = (fields: string): boolean => fields.slice(3, 4) === "S";
|
|
588
|
+
/** Index of the nth space (0-based) after the leading kind character, or -1 when the entry is malformed. */
|
|
589
|
+
function nthSpace(rest: string, n: number): number {
|
|
590
|
+
let at = -1;
|
|
591
|
+
for (let seen = 0; seen <= n; seen++) {
|
|
592
|
+
at = rest.indexOf(" ", at + 1);
|
|
593
|
+
if (at < 0) return -1;
|
|
594
|
+
}
|
|
595
|
+
return at;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Dirty submodules of a repository: modified, untracked or new commits inside a submodule worktree are invisible
|
|
600
|
+
* to snapshot tree diffs (#798 D2). Returns undefined when git cannot answer. A project without `.gitmodules`
|
|
601
|
+
* pays no git call at all (D4).
|
|
602
|
+
*/
|
|
603
|
+
export function dirtySubmodules(repo: Repo, budget?: SnapshotBudget): string[] | undefined {
|
|
604
|
+
if (repo.shadow || !existsSync(join(repo.top, ".gitmodules"))) return [];
|
|
605
|
+
const result = git(repo, ["status", "--porcelain=v2", "-z", "--ignore-submodules=none", "--", scope(repo)], { GIT_OPTIONAL_LOCKS: "0" }, budget, 1024 * 1024);
|
|
606
|
+
return result.status === 0 ? parseDirtySubmodules(result.stdout) : undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
549
609
|
/** Turn records in hub.db: the daemon writes them, `ahub turns` and `ahub undo` read them. */
|
|
550
610
|
export class Turns {
|
|
551
611
|
private readonly db: Database;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { statSync } from "node:fs";
|
|
2
3
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
4
|
import type { TaskPlan, TaskRefs } from "./board.ts";
|
|
4
|
-
import { hubGitSync } from "./git.ts";
|
|
5
|
+
import { hubGitArgv, hubGitEnv, hubGitSync } from "./git.ts";
|
|
6
|
+
import { hostExecutable } from "./host-executable.ts";
|
|
5
7
|
import { canonicalPath, realPath } from "./project.ts";
|
|
6
8
|
import { isGitHubRepository } from "./spec-read.ts";
|
|
7
9
|
|
|
@@ -22,6 +24,33 @@ function slug(value: string): string | undefined {
|
|
|
22
24
|
} catch { return undefined; }
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** Share the advisory reader's bounds; only authority reads pin Git and isolate the actual checkout. */
|
|
28
|
+
function boundedGit(dir: string, args: string[], isolated = false): string | undefined {
|
|
29
|
+
try {
|
|
30
|
+
const options = { encoding: "utf8" as const, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BYTES };
|
|
31
|
+
const argv = ["-C", dir, ...args];
|
|
32
|
+
const found = isolated
|
|
33
|
+
? spawnSync(hostExecutable("git", dir), hubGitArgv(argv).slice(1), { ...options, cwd: dir, env: hubGitEnv(process.env, { repositoryIsolation: true }) })
|
|
34
|
+
: hubGitSync(argv, options);
|
|
35
|
+
return found.status === 0 && typeof found.stdout === "string" && found.stdout.trim() ? isolated ? found.stdout : found.stdout.trim() : undefined;
|
|
36
|
+
} catch { return undefined; } // Unknown identity never grants; never log remote URLs or stderr.
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Actual project-local GitHub origin only, never task refs, a bare slug or global-config fallback. */
|
|
40
|
+
export function githubOriginRepository(cwd: string): string | undefined {
|
|
41
|
+
let root: string;
|
|
42
|
+
try { root = realPath(cwd); } catch { return undefined; }
|
|
43
|
+
const reply = boundedGit(root, ["config", "--show-scope", "--get", "remote.origin.url"], true);
|
|
44
|
+
const remote = reply && /^(?:local|worktree)\t([^\r\n]+)\n$/.exec(reply)?.[1];
|
|
45
|
+
if (!remote || /[\s?#]/.test(remote)) return undefined;
|
|
46
|
+
const scp = /^(?:[^/@:]+@)?github\.com:[^/]+\/[^/]+\/?$/i.test(remote);
|
|
47
|
+
if (!scp) {
|
|
48
|
+
if (!/^(?:https?|ssh):\/\/(?:[^/@]+@)?github\.com(?::[0-9]+)?\//i.test(remote)) return undefined;
|
|
49
|
+
try { const url = new URL(remote); if (url.search || url.hash) return undefined; } catch { return undefined; }
|
|
50
|
+
}
|
|
51
|
+
return slug(remote);
|
|
52
|
+
}
|
|
53
|
+
|
|
25
54
|
/** Repository metadata is advisory, never an execution root or a grant. Git reads are bounded, local and cached. */
|
|
26
55
|
export class TaskRepositories {
|
|
27
56
|
readonly project: TaskRepository;
|
|
@@ -37,10 +66,7 @@ export class TaskRepositories {
|
|
|
37
66
|
this.roots.set(root, this.project);
|
|
38
67
|
}
|
|
39
68
|
private git(dir: string, args: string[]): string | undefined {
|
|
40
|
-
|
|
41
|
-
const found = hubGitSync(["-C", dir, ...args], { encoding: "utf8", timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BYTES });
|
|
42
|
-
return found.status === 0 && typeof found.stdout === "string" && found.stdout.trim() ? found.stdout.trim() : undefined;
|
|
43
|
-
} catch { return undefined; } // Unknown identity keeps a path key; never log remote URLs or stderr.
|
|
69
|
+
return boundedGit(dir, args);
|
|
44
70
|
}
|
|
45
71
|
private directory(path: string): string {
|
|
46
72
|
let cursor = resolve(path);
|