@staix/agent-hub 0.19.0 → 0.21.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.
Files changed (184) hide show
  1. package/CHANGELOG.md +70 -3
  2. package/README.md +2 -2
  3. package/docs/agent-notes/README.md +11 -0
  4. package/docs/agent-notes/adapters.md +10 -5
  5. package/docs/agent-notes/benchmarks.md +27 -0
  6. package/docs/agent-notes/budget.md +2 -1
  7. package/docs/agent-notes/bus.md +3 -3
  8. package/docs/agent-notes/daemon.md +20 -7
  9. package/docs/agent-notes/local-worker.md +5 -0
  10. package/docs/agent-notes/models.md +2 -0
  11. package/docs/agent-notes/tasks.md +20 -10
  12. package/docs/agent-notes/tests.md +13 -5
  13. package/docs/bench.md +1 -1
  14. package/docs/data-policy.md +2 -0
  15. package/docs/dogfood.md +41 -5
  16. package/docs/events.md +2 -1
  17. package/docs/memory.md +9 -3
  18. package/docs/operations.md +598 -63
  19. package/docs/profiles.md +13 -1
  20. package/docs/quickstart.md +18 -9
  21. package/docs/research.md +7 -4
  22. package/docs/sdlc.md +66 -10
  23. package/docs/security.md +397 -35
  24. package/docs/smoke.md +127 -0
  25. package/docs/specs/2026-09-19-agent-hub-design.md +1149 -19
  26. package/docs/verification/2026-10-27-0.19.0.md +5 -5
  27. package/docs/verification/2026-10-27-0.20.0.md +75 -0
  28. package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-bash-permission.json +635 -0
  29. package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-permission-transport.json +403 -0
  30. package/docs/verification/2026-10-28-claude-implementer-permission-triage.md +123 -0
  31. package/docs/verification/2026-10-28-jcode-0.99.1-usage.md +41 -0
  32. package/docs/verification/2026-10-28-realpath-name-audit.md +222 -0
  33. package/docs/verification/2026-10-28-routing-f0bb8ad0.md +1000 -0
  34. package/docs/verification/2026-10-29-0.21.0.md +61 -0
  35. package/docs/verification/2026-10-29-model-effort-capabilities.md +123 -0
  36. package/docs/verification/2026-10-29-modelrouting-b152ddb9.md +953 -0
  37. package/docs/verification/claude-implementer-permission-triage.ts +160 -0
  38. package/docs/verification/claude-permission-transport-native.py +71 -14
  39. package/docs/verification/profiles/YYYY-MM-DD-research.md +1 -1
  40. package/docs/verified.json +26 -18
  41. package/package.json +2 -2
  42. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  43. package/plugins/agent-hub/server.js +1116 -284
  44. package/src/adapters/acp.ts +87 -3
  45. package/src/adapters/channel-peer.ts +11 -5
  46. package/src/adapters/claude-channel.ts +13 -6
  47. package/src/adapters/claude-implementer.ts +61 -7
  48. package/src/adapters/codex-appserver.ts +88 -5
  49. package/src/adapters/conformance/enrollment.ts +4 -4
  50. package/src/adapters/conformance/factories.ts +2 -1
  51. package/src/adapters/conformance/fakes/jcode-agent.ts +6 -2
  52. package/src/adapters/conformance/kit.ts +10 -6
  53. package/src/adapters/definitions/builtin-metadata.ts +3 -114
  54. package/src/adapters/definitions/native-config-defaults.ts +1 -1
  55. package/src/adapters/definitions/native-config-policy.ts +3 -2
  56. package/src/adapters/definitions/native-context.ts +12 -0
  57. package/src/adapters/external-metadata.ts +48 -0
  58. package/src/adapters/external-modules.ts +63 -3
  59. package/src/adapters/jcode.ts +6 -2
  60. package/src/adapters/local-worker.ts +42 -25
  61. package/src/adapters/native-proof-ports.ts +10 -0
  62. package/src/adapters/pi.ts +7 -0
  63. package/src/adapters/registry-types.ts +4 -0
  64. package/src/adapters/reviewer-print.ts +1 -0
  65. package/src/broker/protocol.ts +203 -0
  66. package/src/broker/server.ts +314 -0
  67. package/src/broker/store.ts +548 -0
  68. package/src/cli/bench.ts +3 -2
  69. package/src/cli/broker.ts +58 -0
  70. package/src/cli/conductor-recovery.ts +142 -0
  71. package/src/cli/configured-acp-entry.ts +26 -6
  72. package/src/cli/console-state.ts +59 -17
  73. package/src/cli/console.ts +17 -2
  74. package/src/cli/facts-hook.ts +3 -1
  75. package/src/cli/help.ts +28 -16
  76. package/src/cli/identity.ts +23 -10
  77. package/src/cli/init.ts +34 -7
  78. package/src/cli/intake-output.ts +33 -0
  79. package/src/cli/main.ts +314 -71
  80. package/src/cli/output.ts +41 -1
  81. package/src/cli/recovery-package.ts +18 -1
  82. package/src/cli/role-holders.ts +35 -0
  83. package/src/cli/room.ts +304 -0
  84. package/src/cli/say-input.ts +16 -0
  85. package/src/cli/schedule-output.ts +49 -0
  86. package/src/cli/sdlc-output.ts +44 -6
  87. package/src/cli/smoke-pty-bridge.ts +33 -0
  88. package/src/cli/smoke-pty.ts +145 -0
  89. package/src/cli/smoke-run.ts +139 -0
  90. package/src/cli/smoke-target.ts +130 -0
  91. package/src/cli/source-output.ts +29 -0
  92. package/src/cli/task-output.ts +3 -2
  93. package/src/cli/upgrade-runtime.ts +98 -16
  94. package/src/cli/upgrade.ts +66 -7
  95. package/src/hub/ask.ts +76 -12
  96. package/src/hub/board.ts +132 -22
  97. package/src/hub/bus.ts +399 -56
  98. package/src/hub/child-process.ts +2 -0
  99. package/src/hub/ci-watch.ts +162 -0
  100. package/src/hub/claude-failure-stage.ts +85 -0
  101. package/src/hub/claude-launch-proof.ts +67 -2
  102. package/src/hub/claude-shell-wrapper.ts +11 -5
  103. package/src/hub/conductor.ts +17 -4
  104. package/src/hub/config-apply.ts +32 -5
  105. package/src/hub/config-reload.ts +7 -5
  106. package/src/hub/config-trust.ts +8 -1
  107. package/src/hub/context-window.ts +33 -3
  108. package/src/hub/control-client.ts +14 -2
  109. package/src/hub/corpus-config.ts +34 -0
  110. package/src/hub/corpus-evidence.ts +184 -0
  111. package/src/hub/corpus.ts +373 -0
  112. package/src/hub/cost-governance.ts +26 -0
  113. package/src/hub/daemon.ts +1228 -242
  114. package/src/hub/deliverable-files.ts +8 -2
  115. package/src/hub/delivery-journal.ts +123 -17
  116. package/src/hub/dogfood-profile.ts +32 -5
  117. package/src/hub/dogfood-read.ts +6 -2
  118. package/src/hub/envelope.ts +11 -0
  119. package/src/hub/eval-gate.ts +131 -0
  120. package/src/hub/events.ts +1 -1
  121. package/src/hub/external-instance.ts +69 -0
  122. package/src/hub/facts.ts +15 -5
  123. package/src/hub/folder-trigger.ts +223 -0
  124. package/src/hub/hub-tools.ts +2 -1
  125. package/src/hub/inference.ts +20 -5
  126. package/src/hub/instance-config.ts +69 -3
  127. package/src/hub/instance-execution.ts +6 -2
  128. package/src/hub/instances.ts +32 -4
  129. package/src/hub/intake.ts +125 -0
  130. package/src/hub/learning.ts +85 -0
  131. package/src/hub/lifecycle.ts +7 -3
  132. package/src/hub/limits.ts +12 -5
  133. package/src/hub/native-permission-request.ts +77 -0
  134. package/src/hub/operational-authority.ts +63 -5
  135. package/src/hub/operational-settings.ts +76 -0
  136. package/src/hub/operational-view.ts +1 -1
  137. package/src/hub/packs.ts +132 -0
  138. package/src/hub/path-policy.ts +7 -0
  139. package/src/hub/peer-doing.ts +130 -0
  140. package/src/hub/peers.ts +46 -0
  141. package/src/hub/queue-backpressure.ts +58 -0
  142. package/src/hub/read-tools.ts +2 -0
  143. package/src/hub/recovery-authority.ts +151 -0
  144. package/src/hub/recovery-policy.ts +220 -0
  145. package/src/hub/research.ts +24 -14
  146. package/src/hub/restart.ts +7 -0
  147. package/src/hub/routing.ts +54 -11
  148. package/src/hub/schedules.ts +266 -0
  149. package/src/hub/sdlc-merge.ts +38 -14
  150. package/src/hub/sdlc-metrics.ts +32 -10
  151. package/src/hub/sdlc-queue.ts +7 -1
  152. package/src/hub/sdlc-release.ts +1 -1
  153. package/src/hub/sdlc.ts +37 -7
  154. package/src/hub/security-baseline.ts +38 -0
  155. package/src/hub/settings.ts +7 -1
  156. package/src/hub/shipped-packs.ts +8 -0
  157. package/src/hub/snapshots.ts +72 -6
  158. package/src/hub/source-read.ts +160 -0
  159. package/src/hub/sources.ts +311 -0
  160. package/src/hub/task-shell-default.ts +3 -2
  161. package/src/hub/tasks.ts +1047 -115
  162. package/src/hub/workflow.ts +1 -1
  163. package/src/local/deny.ts +21 -0
  164. package/src/local/doc-read.ts +69 -0
  165. package/src/local/proxy.ts +4 -13
  166. package/src/local/sandbox.ts +3 -1
  167. package/src/local/tools.ts +37 -3
  168. package/src/local/web-fetch-config.ts +43 -0
  169. package/src/local/web-fetch.ts +183 -0
  170. package/src/memory/brief.ts +85 -13
  171. package/src/memory/capture.ts +1 -1
  172. package/src/rooms/application.ts +114 -0
  173. package/src/rooms/boundary.ts +103 -0
  174. package/src/rooms/commands.ts +294 -0
  175. package/src/rooms/crypto.ts +371 -0
  176. package/src/rooms/link.ts +584 -0
  177. package/src/rooms/policy.ts +82 -0
  178. package/src/rooms/remote-peer.ts +80 -0
  179. package/src/rooms/service.ts +426 -0
  180. package/src/rooms/status.ts +146 -0
  181. package/src/rooms/store.ts +1210 -0
  182. package/src/rooms/wire.ts +465 -0
  183. package/src/ui/index.html +91 -4
  184. package/templates/packs/example.pack.json +13 -0
@@ -0,0 +1,160 @@
1
+ /** #544 opt-in diagnostic producer. It characterizes deliberate permission-push omission outside the regression suite. No installed Claude binary, account or model provider is used. */
2
+ import { expect, test } from "bun:test";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8
+ import { COMPAT, assessVersion, trustFile, writeTrust } from "../../src/adapters/compat.ts";
9
+ import { startDaemon, loadConfig } from "../../src/hub/daemon.ts";
10
+ import { ControlClient } from "../../src/hub/control-client.ts";
11
+ import { hubGitEnv, hubGitSync } from "../../src/hub/git.ts";
12
+ import { hostExecutable } from "../../src/hub/host-executable.ts";
13
+ import { realPath } from "../../src/hub/project.ts";
14
+ import { shellQuote } from "../../src/cli/shell-quote.ts";
15
+ import { taskShellCommand } from "../../src/hub/task-shell-command.ts";
16
+ import { sandboxAvailable } from "../../src/local/sandbox.ts";
17
+ import { runConsole, type ConsoleTerminal } from "../../src/cli/console.ts";
18
+
19
+ // Keep testing the trusted-but-unverified path if a later native receipt verifies 2.1.312.
20
+ const VERSION = COMPAT["claude-implementer"].verified.includes("2.1.312") ? "9999.0.0" : "2.1.312";
21
+ const REPO_ROOT = resolve(import.meta.dir, "../..");
22
+ const COMMAND = "bun test test/ci-watch.test.ts";
23
+ async function until(check: () => boolean, label: string): Promise<void> {
24
+ for (let n = 0; n < 400 && !check(); n++) await new Promise<void>(done => setImmediate(done));
25
+ expect(check(), label).toBe(true);
26
+ }
27
+ async function withDeadline<T>(promise: Promise<T>, label: string): Promise<T> {
28
+ return new Promise((resolve, reject) => {
29
+ const timer = setTimeout(() => reject(new Error(`triage barrier timed out: ${label}`)), 5000);
30
+ promise.then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
31
+ });
32
+ }
33
+ function deferred<T>() { let resolve!: (value: T) => void; const promise = new Promise<T>(done => { resolve = done; }); return { promise, resolve }; }
34
+ async function fixture() {
35
+ const root = realPath(mkdtempSync(join(tmpdir(), "ahub-544-"))), hostBin = realPath(mkdtempSync(join(tmpdir(), "ahub-544-host-bin-")));
36
+ const peer = "claude-triage", tree = join(root, ".worktrees", peer), home = join(root, "home"), bin = join(root, "bin"), stateDir = join(root, ".agenthub", "state"), callFile = join(root, "fake-call.json");
37
+ const keys = ["HOME", "CLAUDE_CONFIG_DIR", "AGENTHUB_HOME", "AMEM_HOME", "PATH"] as const;
38
+ const previous = Object.fromEntries(keys.map(key => [key, process.env[key]]));
39
+ const clients: ControlClient[] = []; let daemon: Awaited<ReturnType<typeof startDaemon>> | undefined;
40
+ const close = async () => {
41
+ for (const client of clients) client.close();
42
+ await daemon?.stop();
43
+ for (const key of keys) { if (previous[key] === undefined) delete process.env[key]; else process.env[key] = previous[key]; }
44
+ rmSync(root, { recursive: true, force: true }); rmSync(hostBin, { recursive: true, force: true });
45
+ };
46
+ try {
47
+ mkdirSync(home); mkdirSync(bin); mkdirSync(stateDir, { recursive: true });
48
+ const git = hostExecutable("git", root, previous.PATH ?? "/usr/bin:/bin");
49
+ writeFileSync(join(hostBin, "git"), `#!/bin/sh\nexport GIT_CONFIG_SYSTEM=/dev/null\nexport GIT_CONFIG_GLOBAL=/dev/null\nexec ${shellQuote(git)} "$@"\n`, { mode: 0o700 });
50
+ const env = hubGitEnv({ ...process.env, HOME: home, PATH: `${hostBin}:${previous.PATH ?? "/usr/bin:/bin"}` }, { repositoryIsolation: true });
51
+ for (const args of [["init", "-q"], ["-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "--allow-empty", "-qm", "triage fixture"], ["worktree", "add", "-qb", "triage/worker", tree], ["remote", "add", "origin", "https://github.com/fixture/triage.git"]]) expect(hubGitSync(["-C", root, ...args], { encoding: "utf8", env }).status).toBe(0);
52
+ mkdirSync(join(tree, "test")); writeFileSync(join(tree, "test", "ci-watch.test.ts"), 'throw new Error("fixture command must not execute");\n');
53
+ writeFileSync(join(bin, "claude"), `#!${process.execPath}\nimport{writeFileSync,renameSync}from"node:fs";\nif(process.argv.includes("--version")){console.log(${JSON.stringify(VERSION + " (Claude Code)")});process.exit(0)}\nawait new Response(Bun.stdin.stream()).text();\nwriteFileSync(${JSON.stringify(callFile + ".tmp")},JSON.stringify({args:process.argv.slice(2),launchId:process.env.AGENTHUB_LAUNCH_ID,peer:process.env.AGENTHUB_PEER_ID}));renameSync(${JSON.stringify(callFile + ".tmp")},${JSON.stringify(callFile)});\nsetInterval(()=>{},1000);\n`, { mode: 0o700 });
54
+ Object.assign(process.env, { HOME: home, CLAUDE_CONFIG_DIR: join(home, "claude"), AGENTHUB_HOME: join(home, "hub"), AMEM_HOME: join(home, "memory"), PATH: `${bin}:${hostBin}:${previous.PATH ?? "/usr/bin:/bin"}` });
55
+ writeTrust("claude-implementer", VERSION, false, trustFile(join(home, "hub")));
56
+ writeFileSync(join(root, ".agenthub", "config.local.json"), JSON.stringify({ instances: { [peer]: { kind: "claude", roles: ["implementer"], start_mode: "headless", worktree: `.worktrees/${peer}`, permission_mode: "ask-when-needed" } }, roles: { [peer]: ["implementer"] }, roster: { [peer]: true }, memory: { enabled: false, native: false }, inference: { enabled: false }, mlx: { enabled: false }, snapshots: { enabled: false }, pi: { enabled: false, auto_start: false }, task_sweep: { enabled: false }, batch_ms: 0 }));
57
+ daemon = await startDaemon({ cwd: root, stateDir, config: loadConfig(root), controlPort: 0, codexAppPort: 0, codexProxyPort: 0, permissionTimeoutMs: 4000, agentDrift: false, shellDefaultBranchProbe: async () => "main" });
58
+ const person = await ControlClient.connect(stateDir, { role: "console", projectRoot: root }); clients.push(person);
59
+ const instanceId = (await person.request({ t: "status" })).status.instanceId;
60
+ expect(await person.request({ t: "start", peer })).toMatchObject({ ok: true, executionCwd: tree });
61
+ const proposed = await person.request({ t: "task", op: "hub_task_propose", args: { title: "isolated permission triage", class: "test", owner: peer } });
62
+ expect(proposed.ok).toBe(true); const task = Number(String(proposed.text).match(/task #(\d+)/)?.[1]); expect(task).toBeGreaterThan(0);
63
+ // The fake records before idling. This waits for its consumed stdin, never an installed native process.
64
+ for (let n = 0; n < 300 && !existsSync(callFile); n++) await Bun.sleep(5);
65
+ expect(existsSync(callFile)).toBe(true);
66
+ const call = JSON.parse(readFileSync(callFile, "utf8")) as { args: string[]; launchId: string; peer: string };
67
+ const tools = await ControlClient.connect(stateDir, { role: "tools", peer, launchId: call.launchId, projectRoot: root }); clients.push(tools);
68
+ expect(await tools.request({ t: "task", op: "hub_task_accept", args: { id: task } })).toMatchObject({ ok: true });
69
+ const connectPerson = async () => { const client = await ControlClient.connect(stateDir, { role: "console", projectRoot: root }); clients.push(client); return client; };
70
+ return { root, home, peer, tree, stateDir, person, tools, call, instanceId, close, connectPerson };
71
+ } catch (error) { await close(); throw error; }
72
+ }
73
+
74
+ const permissionPush = (client: ControlClient) => {
75
+ const pending = deferred<any>(), closed = deferred<any>(), pushes: any[] = [];
76
+ client.onPush = message => { pushes.push(message); if (message.t === "permission") pending.resolve(message); if (message.t === "permission_closed") closed.resolve(message); };
77
+ client.send({ t: "tail" });
78
+ return { pending: () => withDeadline(pending.promise, "permission push"), closed: () => withDeadline(closed.promise, "permission closure"), pushes };
79
+ };
80
+
81
+ // This proves host policy using a fake native and never redeems the issued shell ticket.
82
+ test("#544 trusted unverified admission does not itself disable the closed host-wrapper permission path", async () => {
83
+ expect(assessVersion("claude-implementer", VERSION, [])).toMatchObject({ ok: false, state: "refused" });
84
+ expect(assessVersion("claude-implementer", VERSION, [{ id: "claude-implementer", version: VERSION, at: 1 }])).toMatchObject({ ok: true, state: "trusted" });
85
+ expect(taskShellCommand(COMMAND, { branch: "triage/worker", defaultBranch: "main" })?.kind).toBe("test");
86
+ const f = await fixture();
87
+ try {
88
+ expect(f.call.args[f.call.args.indexOf("--permission-mode") + 1]).toBe("default");
89
+ expect(f.call.args[f.call.args.indexOf("--permission-prompt-tool") + 1]).toBe("mcp__agent-hub__hub_permission_request");
90
+ expect(f.call.args).not.toContain("bypassPermissions"); expect(f.call.args).not.toContain("dontAsk");
91
+ const answer = await f.tools.request({ t: "permission_request", tool: "Bash", input: { command: COMMAND }, wrapperProtocol: 1 });
92
+ if (sandboxAvailable()) {
93
+ expect(answer).toMatchObject({ ok: true, behavior: "allow", wrapperProtocol: 1 });
94
+ expect(answer.updatedInput.command).not.toBe(COMMAND);
95
+ } else {
96
+ expect(answer).toMatchObject({ ok: true, behavior: "deny", message: "no person answered" });
97
+ expect(answer.updatedInput).toBeUndefined();
98
+ }
99
+ } finally { await f.close(); }
100
+ }, 20_000);
101
+
102
+ test("#544 real daemon pending approvals are pushed, replayed to a fresh console, then withdrawn on timeout", async () => {
103
+ const f = await fixture();
104
+ try {
105
+ const live = permissionPush(f.person);
106
+ const answer = f.tools.request({ t: "permission_request", tool: "Bash", input: { command: "pwd && pwd" }, wrapperProtocol: 1 }); void answer.catch(() => {});
107
+ const request = await live.pending();
108
+ expect(request).toMatchObject({ peer: f.peer, options: [{ optionId: "allow", name: "Allow once", kind: "allow_once" }, { optionId: "deny", name: "Deny", kind: "reject_once" }] });
109
+ expect((await f.person.request({ t: "ui_snapshot" })).permissions).toContainEqual(expect.objectContaining({ id: request.id, peer: f.peer }));
110
+ expect((await f.person.request({ t: "status" })).status.attentionItems).toContainEqual(expect.objectContaining({ kind: "approval", id: request.id, peer: f.peer, level: "act" }));
111
+ const late = await f.connectPerson(), replay = permissionPush(late);
112
+ expect((await replay.pending()).id).toBe(request.id);
113
+ expect(await answer).toMatchObject({ ok: true, behavior: "deny", message: "no person answered" });
114
+ expect(await live.closed()).toMatchObject({ id: request.id, outcome: "cancelled", reason: "expired" });
115
+ expect((await f.person.request({ t: "ui_snapshot" })).permissions).toEqual([]);
116
+ expect((await f.person.request({ t: "status" })).status.attentionItems.filter((item: { kind: string }) => item.kind === "approval")).toEqual([]);
117
+ } finally { await f.close(); }
118
+ }, 20_000);
119
+
120
+ test("#544 missing native tool_use_id denies in the production MCP channel before any daemon approval", async () => {
121
+ const f = await fixture(), observed = permissionPush(f.person); let client: Client | undefined;
122
+ try {
123
+ await f.person.request({ t: "status" }); // Same socket barrier: tail is registered before the test calls MCP.
124
+ const env = Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined && !entry[0].startsWith("AGENTHUB_") && !["CLAUDECODE", "CLAUDE_CODE_SESSION_ID"].includes(entry[0])));
125
+ Object.assign(env, { HOME: f.home, CLAUDE_CONFIG_DIR: join(f.home, "claude"), AGENTHUB_HOME: join(f.home, "hub"), AMEM_HOME: join(f.home, "memory"), AGENTHUB_MODE: "tools", AGENTHUB_PERMISSION_TOOL: "1", AGENTHUB_PEER_ID: f.peer, AGENTHUB_PROJECT_DIR: f.root, AGENTHUB_STATE_DIR: f.stateDir, AGENTHUB_INSTANCE_ID: f.instanceId, AGENTHUB_LAUNCH_ID: f.call.launchId });
126
+ const transport = new StdioClientTransport({ command: process.execPath, args: [join(REPO_ROOT, "src", "adapters", "claude-channel.ts")], cwd: f.tree, env, stderr: "pipe" });
127
+ let diagnostics = ""; transport.stderr?.on("data", chunk => { diagnostics = (diagnostics + String(chunk)).slice(-16384); });
128
+ client = new Client({ name: "permission-triage-fixture", version: "1" }); await client.connect(transport);
129
+ for (let n = 0; n < 300 && !diagnostics.includes(`connected to hub as "${f.peer}"`); n++) await Bun.sleep(5);
130
+ expect(diagnostics).toContain(`connected to hub as "${f.peer}"`);
131
+ const schema = (await client.listTools()).tools.find(tool => tool.name === "hub_permission_request")!.inputSchema;
132
+ expect(schema.required).toEqual(["tool_name", "input", "tool_use_id"]);
133
+ const result = await client.callTool({ name: "hub_permission_request", arguments: { tool_name: "Bash", input: { command: COMMAND } } });
134
+ const content = result.content as { type: string; text?: string }[];
135
+ expect(JSON.parse(content.filter(row => row.type === "text").map(row => row.text).join("\n"))).toEqual({ behavior: "deny", message: "invalid native permission request" });
136
+ expect(observed.pushes.filter(message => message.t === "permission")).toEqual([]);
137
+ expect((await f.person.request({ t: "ui_snapshot" })).permissions).toEqual([]);
138
+ expect((await f.person.request({ t: "status" })).status.attentionItems.filter((item: { kind: string }) => item.kind === "approval")).toEqual([]);
139
+ } finally { await client?.close(); await f.close(); }
140
+ }, 20_000);
141
+
142
+ // Deliberately dropped push: this proves the UI's current information-source boundary,
143
+ // not that a real WebSocket lost a message in the reported incident.
144
+ test("#544 console status refresh alone cannot reconstruct a missed permission push, while a later push renders", async () => {
145
+ const root = mkdtempSync(join(tmpdir(), "ahub-544-console-")), output: string[] = [];
146
+ const id = "triage-approval", expiresAt = Date.now() + 60_000;
147
+ let input = (_text: string) => {}, signal = () => {}, statusReads = 0;
148
+ const terminal: ConsoleTerminal = { columns: 120, rows: 30, isTTY: true, write: text => { output.push(text); }, raw: () => {}, onData: listener => { input = listener; return () => {}; }, onResize: () => () => {}, onSignal: listener => { signal = listener; return () => {}; } };
149
+ const client = { onPush: (_message: any) => {}, onClose: (_code: number, _reason: string) => {}, send: (_message: any) => {}, close: () => {}, request: async (message: any) => {
150
+ if (message.t === "status") statusReads++;
151
+ return { ok: true, status: { peers: {}, attentionItems: [{ kind: "approval", level: "act", id, peer: "claude-triage", until: expiresAt }] }, budget: {}, text: "[]", deliveries: [] };
152
+ } };
153
+ const running = runConsole({ client, cwd: root, stateDir: root, terminal, panels: true, noPet: true, color: false });
154
+ try {
155
+ await until(() => statusReads >= 1, "initial positive status"); input("2"); await until(() => statusReads >= 2, "refresh after choosing approvals");
156
+ expect(output.join("")).toContain("0 approvals"); expect(output.join("")).not.toContain("TRIAGE-PENDING-BASH");
157
+ client.onPush({ t: "permission", id, peer: "claude-triage", title: "TRIAGE-PENDING-BASH", expiresAt, options: [{ optionId: "allow", name: "Allow once", kind: "allow_once" }, { optionId: "deny", name: "Deny", kind: "reject_once" }] });
158
+ expect(output.join("")).toContain("1 approval"); expect(output.join("")).toContain("TRIAGE-PENDING-BASH");
159
+ } finally { signal(); await running; rmSync(root, { recursive: true, force: true }); }
160
+ });
@@ -1,13 +1,26 @@
1
1
  # Model-free native receipt of the headless Claude permission transport (#843 AC5, task #507).
2
2
  # Repeats the case table of 2026-10-23-headless-claude-permissions.md on each installed binary given.
3
3
  # Usage: python3 claude-permission-transport-native.py OUTPUT --claude /absolute/claude [--claude ...]
4
- # No account, no provider, no model: a loopback server answers with a fixed Write tool call, then a completion.
5
- import argparse, hashlib, http.server, json, os, pathlib, signal, subprocess, threading, time
4
+ # Default: nine Write cases. --tool Bash selects a harmless fixture writer; --rewrite-bash narrows Bash to two replacement cases.
5
+ # No account, no provider, no model: a loopback server supplies the fixed tool call, then a completion.
6
+ import argparse, hashlib, http.server, json, os, pathlib, re, shlex, signal, subprocess, threading, time
7
+
8
+ def mcp_name(value):
9
+ if not re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,63}', value):
10
+ raise argparse.ArgumentTypeError('MCP names must be 1..64 ASCII letters, digits, underscores or hyphens, starting with a letter')
11
+ return value
12
+
6
13
 
7
14
  parser = argparse.ArgumentParser()
8
15
  parser.add_argument('output')
9
16
  parser.add_argument('--claude', action='append', required=True)
17
+ parser.add_argument('--tool', choices=('Write', 'Bash'), default='Write')
18
+ parser.add_argument('--rewrite-bash', action='store_true', help='Bash only: two allow/deny cases proving native updatedInput command replacement')
19
+ parser.add_argument('--server-name', type=mcp_name, default='fixture')
20
+ parser.add_argument('--permission-tool', type=mcp_name, default='approve')
21
+ parser.add_argument('--require-tool-use-id', action='store_true', help='Advertise the three required production permission fields and reject extra properties')
10
22
  args = parser.parse_args()
23
+ if args.rewrite_bash and args.tool != 'Bash': parser.error('--rewrite-bash requires --tool Bash')
11
24
  ROOT = pathlib.Path(args.output).resolve()
12
25
  ROOT.mkdir(mode=0o700, parents=False, exist_ok=False)
13
26
 
@@ -20,13 +33,22 @@ for line in sys.stdin:
20
33
  method, rid = q.get('method'), q.get('id')
21
34
  if rid is None: continue
22
35
  if method == 'initialize':
23
- r = {'protocolVersion': '2024-11-05', 'capabilities': {'tools': {}}, 'serverInfo': {'name': 'fixture', 'version': '1'}}
36
+ r = {'protocolVersion': '2024-11-05', 'capabilities': {'tools': {}}, 'serverInfo': {'name': os.environ.get('SERVER_NAME', 'fixture'), 'version': '1'}}
24
37
  elif method == 'tools/list':
25
- tools = [] if case == 'missing-tool' else [{'name': 'approve', 'description': 'Person approval fixture', 'inputSchema': {'type': 'object', 'properties': {'tool_name': {'type': 'string'}, 'input': {'type': 'object'}, 'tool_use_id': {'type': 'string'}}, 'required': ['tool_name', 'input']}}]
38
+ schema = {'type': 'object', 'properties': {'tool_name': {'type': 'string'}, 'input': {'type': 'object'}, 'tool_use_id': {'type': 'string'}}, 'required': ['tool_name', 'input']}
39
+ if os.environ.get('REQUIRE_TOOL_USE_ID') == '1':
40
+ schema['required'].append('tool_use_id'); schema['additionalProperties'] = False
41
+ schema['properties']['tool_name']['maxLength'] = 128; schema['properties']['tool_use_id']['maxLength'] = 256
42
+ tools = [] if case == 'missing-tool' else [{'name': os.environ.get('PERMISSION_TOOL', 'approve'), 'description': 'Person approval fixture', 'inputSchema': schema}]
26
43
  r = {'tools': tools}
27
44
  elif method == 'tools/call':
28
45
  args = q.get('params', {}).get('arguments', {})
29
- with open(os.environ['RECEIPT'], 'a') as f: print(json.dumps({'arguments': args}), file=f)
46
+ record = {'arguments': args}
47
+ if os.environ.get('PERMISSION_TOOL') or os.environ.get('REQUIRE_TOOL_USE_ID') == '1': record['tool'] = q.get('params', {}).get('name')
48
+ rewrite = os.environ.get('REWRITE_COMMAND')
49
+ if rewrite:
50
+ record['response'] = {'behavior': 'allow', 'updatedInput': {**args.get('input', {}), 'command': rewrite}}
51
+ with open(os.environ['RECEIPT'], 'a') as f: print(json.dumps(record), file=f)
30
52
  if case == 'pending': time.sleep(120)
31
53
  if case == 'disconnect': sys.exit(1)
32
54
  if case == 'mcp-error':
@@ -35,7 +57,7 @@ for line in sys.stdin:
35
57
  if case in ('deny', 'hook-deny', 'deadline'):
36
58
  value = {'behavior': 'deny', 'message': 'no person answered' if case == 'deadline' else 'fixture denied'}
37
59
  else:
38
- value = {'behavior': 'allow', 'updatedInput': args.get('input', {})}
60
+ value = record.get('response', {'behavior': 'allow', 'updatedInput': args.get('input', {})})
39
61
  r = {'content': [{'type': 'text', 'text': json.dumps(value)}]}
40
62
  else:
41
63
  r = {}
@@ -58,7 +80,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
58
80
  msgs = q.get('messages', [])
59
81
  results = sum(1 for m in msgs if isinstance(m.get('content'), list) for x in m['content'] if isinstance(x, dict) and x.get('type') == 'tool_result')
60
82
  done = results >= 1
61
- content = [{'type': 'text', 'text': 'Fixture finished.'}] if done else [{'type': 'tool_use', 'id': 'toolu_probe_0', 'name': 'Write', 'input': {'file_path': state['target'], 'content': 'probe'}}]
83
+ content = [{'type': 'text', 'text': 'Fixture finished.'}] if done else [{'type': 'tool_use', 'id': 'toolu_probe_0', 'name': state['tool'], 'input': state['input']}]
62
84
  result = {'id': 'msg_probe_%d' % results, 'type': 'message', 'role': 'assistant', 'model': 'claude-sonnet-4-5', 'content': content, 'stop_reason': 'end_turn' if done else 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 100, 'output_tokens': 10}}
63
85
  if not q.get('stream'):
64
86
  self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(json.dumps(result).encode()); return
@@ -101,7 +123,7 @@ def rows(file):
101
123
  return [json.loads(line) for line in file.read_text().splitlines() if line] if file.exists() else []
102
124
 
103
125
 
104
- HOOK_FAIL = {'hooks': {'PreToolUse': [{'matcher': 'Write', 'hooks': [{'type': 'command', 'command': 'exit 1'}]}]}}
126
+ HOOK_FAIL = {'hooks': {'PreToolUse': [{'matcher': args.tool, 'hooks': [{'type': 'command', 'command': 'exit 1'}]}]}}
105
127
  # case -> (settings, expected native exit, mcp calls, file written, tool_result is_error, external stop expected)
106
128
  CASES = {
107
129
  'allow': ({}, 0, 1, True, False, False),
@@ -115,6 +137,7 @@ CASES = {
115
137
  'deadline': ({}, 0, 1, False, True, False),
116
138
  }
117
139
 
140
+ selected_cases = {case: value for case, value in CASES.items() if not args.rewrite_bash or case in ('allow', 'deny')}
118
141
  results = []
119
142
  for index, raw in enumerate(args.claude):
120
143
  binary = pathlib.Path(raw).resolve()
@@ -124,20 +147,42 @@ for index, raw in enumerate(args.claude):
124
147
  v = subprocess.run([str(binary), '--version'], env={'HOME': str(ident), 'CLAUDE_CONFIG_DIR': str(ident / 'config'), 'PATH': '/usr/bin:/bin'}, capture_output=True, text=True, timeout=10)
125
148
  assert v.returncode == 0, v
126
149
  version = v.stdout.strip().split()[0]
127
- for case, (settings, want_exit, want_calls, want_file, want_error, want_stop) in CASES.items():
150
+ for case, (settings, want_exit, want_calls, want_file, want_error, want_stop) in selected_cases.items():
128
151
  d = ROOT / ('%d-%s-%s' % (index, version, case)); d.mkdir()
129
152
  home = d / 'home'; home.mkdir(); work = d / 'work'; work.mkdir(); work = work.resolve()
130
153
  target = work / 'probe.txt'
131
154
  out = {'version': version, 'binarySha256': digest, 'case': case}
155
+ if args.tool != 'Write': out.update(tool=args.tool, rewriteBash=args.rewrite_bash)
156
+ if args.server_name != 'fixture' or args.permission_tool != 'approve' or args.require_tool_use_id: out.update(serverName=args.server_name, permissionTool=args.permission_tool, requireToolUseId=args.require_tool_use_id)
132
157
  srv = native = None
133
158
  try:
159
+ harmless = 'printf %s probe > ' + shlex.quote(str(target))
160
+ tool_input = {'file_path': str(target), 'content': 'probe'} if args.tool == 'Write' else {'command': 'bun test test/ci-watch.test.ts' if args.rewrite_bash else harmless}
161
+ settings = dict(settings)
162
+ if args.tool == 'Bash': settings['permissions'] = {'ask': ['Bash']}
163
+ fixture_path = '/usr/bin:/bin'
164
+ original_marker = work / 'original-command-ran'
165
+ if args.rewrite_bash:
166
+ (work / 'test').mkdir()
167
+ (work / 'test' / 'ci-watch.test.ts').write_text('throw new Error("original command must not execute");\n')
168
+ (work / 'test' / 'ci-watch.test.ts').chmod(0o600)
169
+ fixture_bin = d / 'bin'; fixture_bin.mkdir(mode=0o700)
170
+ fake_bun = fixture_bin / 'bun'
171
+ fake_bun.write_text('#!/bin/sh\nprintf %s original > ' + shlex.quote(str(original_marker)) + '\nexit 97\n')
172
+ fake_bun.chmod(0o700)
173
+ fixture_path = str(fixture_bin.resolve()) + ':' + fixture_path
174
+ mcp_env = {'CASE': case, 'RECEIPT': str(d / 'mcp.jsonl')}
175
+ if args.server_name != 'fixture': mcp_env['SERVER_NAME'] = args.server_name
176
+ if args.permission_tool != 'approve': mcp_env['PERMISSION_TOOL'] = args.permission_tool
177
+ if args.require_tool_use_id: mcp_env['REQUIRE_TOOL_USE_ID'] = '1'
178
+ if args.rewrite_bash and case == 'allow': mcp_env['REWRITE_COMMAND'] = harmless
134
179
  srv = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
135
- srv.state = {'requests': 0, 'target': str(target), 'tools_file': str(d / 'tools.json')}
180
+ srv.state = {'requests': 0, 'target': str(target), 'tools_file': str(d / 'tools.json'), 'tool': args.tool, 'input': tool_input}
136
181
  threading.Thread(target=srv.serve_forever, daemon=True).start()
137
- (d / 'mcp.json').write_text(json.dumps({'mcpServers': {'fixture': {'command': '/usr/bin/python3', 'args': [str(ROOT / 'mcp.py')], 'env': {'CASE': case, 'RECEIPT': str(d / 'mcp.jsonl')}}}}))
182
+ (d / 'mcp.json').write_text(json.dumps({'mcpServers': {args.server_name: {'command': '/usr/bin/python3', 'args': [str(ROOT / 'mcp.py')], 'env': mcp_env}}}))
138
183
  (d / 'settings.json').write_text(json.dumps(settings))
139
- env = {'HOME': str(home), 'CLAUDE_CONFIG_DIR': str(home / 'config'), 'PATH': '/usr/bin:/bin', 'ANTHROPIC_BASE_URL': 'http://127.0.0.1:%d' % srv.server_port, 'ANTHROPIC_API_KEY': 'synthetic-probe-not-a-credential', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC': '1', 'DISABLE_AUTOUPDATER': '1'}
140
- argv = [str(binary), '-p', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', str(d / 'mcp.json'), '--setting-sources', '', '--settings', str(d / 'settings.json'), '--permission-mode', 'default', '--permission-prompts', 'host', '--permission-prompt-tool', 'mcp__fixture__approve', '--model', 'claude-sonnet-4-5', 'Run the fixture write.']
184
+ env = {'HOME': str(home), 'CLAUDE_CONFIG_DIR': str(home / 'config'), 'PATH': fixture_path, 'ANTHROPIC_BASE_URL': 'http://127.0.0.1:%d' % srv.server_port, 'ANTHROPIC_API_KEY': 'synthetic-probe-not-a-credential', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC': '1', 'DISABLE_AUTOUPDATER': '1'}
185
+ argv = [str(binary), '-p', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', str(d / 'mcp.json'), '--setting-sources', '', '--settings', str(d / 'settings.json'), '--permission-mode', 'default', '--permission-prompts', 'host', '--permission-prompt-tool', 'mcp__%s__%s' % (args.server_name, args.permission_tool), '--model', 'claude-sonnet-4-5', 'Run the fixture write.' if args.tool == 'Write' else 'Run the fixture Bash command.']
141
186
  started = time.monotonic()
142
187
  native = subprocess.Popen(argv, cwd=work, env=env, stdout=open(d / 'stdout', 'w'), stderr=open(d / 'stderr', 'w'), start_new_session=True)
143
188
  stopped_externally = False
@@ -158,7 +203,14 @@ for index, raw in enumerate(args.claude):
158
203
  assert len(calls) == want_calls, ('mcp calls', calls)
159
204
  if want_calls:
160
205
  call = calls[0]['arguments']
161
- assert call['tool_name'] == 'Write' and call['input'] == {'file_path': str(target), 'content': 'probe'} and call['tool_use_id'] == 'toolu_probe_0', call
206
+ assert call['tool_name'] == args.tool and call['input'] == tool_input and call['tool_use_id'] == 'toolu_probe_0', call
207
+ if args.permission_tool != 'approve' or args.require_tool_use_id: assert calls[0]['tool'] == args.permission_tool, calls[0]
208
+ if args.rewrite_bash and case == 'allow':
209
+ assert calls[0]['response'] == {'behavior': 'allow', 'updatedInput': {'command': harmless}}, calls[0]
210
+ if args.rewrite_bash:
211
+ out['originalCommandExecuted'] = original_marker.exists()
212
+ assert not original_marker.exists(), 'original bun command executed instead of the rewritten fixture writer'
213
+ assert tool_input == {'command': 'bun test test/ci-watch.test.ts'}, tool_input
162
214
  assert written == want_file, ('file written', written)
163
215
  # A successful native tool_result carries no is_error key at all; only a failed one sets it to true.
164
216
  if want_error is not None: assert [b.get('is_error') is True for b in tool_results] == [want_error], tool_results
@@ -177,11 +229,16 @@ for index, raw in enumerate(args.claude):
177
229
  out['nativeGroupEmpty'] = empty_group(native.pid)
178
230
  if not out['nativeGroupEmpty']: out.update(passed=False, cleanup='native group not established empty')
179
231
  if srv: srv.shutdown()
232
+ if args.rewrite_bash:
233
+ out['originalCommandExecuted'] = original_marker.exists()
234
+ if out['originalCommandExecuted']: out.update(passed=False, error='original bun command executed instead of the rewritten fixture writer')
180
235
  out['binaryUnchanged'] = sha(binary) == digest
181
236
  if not out['binaryUnchanged']: out.update(passed=False, error='native binary changed during probe')
182
237
  results.append(out)
183
238
  print(json.dumps(out), flush=True)
184
239
  receipt = {'cases': results, 'certified': all(r['passed'] for r in results), 'account': False, 'inference': False}
240
+ if args.tool != 'Write': receipt.update(tool=args.tool, rewriteBash=args.rewrite_bash)
241
+ if args.server_name != 'fixture' or args.permission_tool != 'approve' or args.require_tool_use_id: receipt.update(serverName=args.server_name, permissionTool=args.permission_tool, requireToolUseId=args.require_tool_use_id)
185
242
  (ROOT / 'receipt.json').write_text(json.dumps(receipt, indent=2))
186
243
  print(json.dumps({'receipt': str(ROOT / 'receipt.json'), 'certified': receipt['certified']}), flush=True)
187
244
  raise SystemExit(0 if receipt['certified'] else 1)
@@ -25,7 +25,7 @@ Copy only emitted counts and anonymous task/claim slots. These placeholders rema
25
25
  - Missing check keys per run: unknown
26
26
  - Section-overlap notices and confirmed section edits: unknown
27
27
 
28
- Task-check history alone does not prove checks.fact_check ran. A model label or task approval alone does not prove a person verdict or agreement. Preserve every emitted null.
28
+ Task-check history alone does not prove checks.fact_check ran; only entries that carry the hub's own `checkKey` do (#901), and an older keyless entry makes the group unknown. A model label or task approval alone does not prove a person verdict or agreement; a verdict is the person's `ahub task claim` entry, and agreement needs the model's own verdict entered with it. Preserve every emitted null.
29
29
 
30
30
  ## Verification verdicts
31
31
 
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "documents": {
4
4
  "README.md": {
5
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
5
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
6
6
  "paths": [
7
7
  "package.json",
8
8
  "src/",
@@ -11,14 +11,14 @@
11
11
  ]
12
12
  },
13
13
  "docs/security.md": {
14
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
14
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
15
15
  "paths": [
16
16
  "src/",
17
17
  "templates/"
18
18
  ]
19
19
  },
20
20
  "docs/operations.md": {
21
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
21
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
22
22
  "paths": [
23
23
  "package.json",
24
24
  "src/",
@@ -35,7 +35,7 @@
35
35
  ]
36
36
  },
37
37
  "docs/quickstart.md": {
38
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
38
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
39
39
  "paths": [
40
40
  "package.json",
41
41
  "src/",
@@ -43,7 +43,7 @@
43
43
  ]
44
44
  },
45
45
  "docs/agent-notes/adapters.md": {
46
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
46
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
47
47
  "paths": [
48
48
  "src/adapters/",
49
49
  "src/pi/",
@@ -74,11 +74,14 @@
74
74
  "src/hub/permission-payload.ts",
75
75
  "src/hub/read-only-shell.ts",
76
76
  "src/hub/read-only-shell-proof.ts",
77
- "src/hub/operational-authority.ts"
77
+ "src/hub/operational-authority.ts",
78
+ "test/daemon-native-branches.json",
79
+ "test/daemon-native-branches.test.ts",
80
+ "src/hub/claude-launch-proof.ts"
78
81
  ]
79
82
  },
80
83
  "docs/agent-notes/benchmarks.md": {
81
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
84
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
82
85
  "paths": [
83
86
  "scripts/benchmarks/",
84
87
  "test/benchmarks/",
@@ -90,7 +93,7 @@
90
93
  ]
91
94
  },
92
95
  "docs/agent-notes/budget.md": {
93
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
96
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
94
97
  "paths": [
95
98
  "src/hub/budget.ts",
96
99
  "src/cli/statusline-tee.ts",
@@ -114,7 +117,7 @@
114
117
  ]
115
118
  },
116
119
  "docs/agent-notes/bus.md": {
117
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
120
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
118
121
  "paths": [
119
122
  "src/hub/bus.ts",
120
123
  "src/hub/envelope.ts",
@@ -129,7 +132,7 @@
129
132
  ]
130
133
  },
131
134
  "docs/agent-notes/daemon.md": {
132
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
135
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
133
136
  "paths": [
134
137
  "src/hub/daemon.ts",
135
138
  "src/hub/control-client.ts",
@@ -202,7 +205,7 @@
202
205
  ]
203
206
  },
204
207
  "docs/agent-notes/local-worker.md": {
205
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
208
+ "verifiedAgainst": "aeeaeb048860316dd4e08d84a658519ed6c0ad24",
206
209
  "paths": [
207
210
  "src/adapters/local-worker.ts",
208
211
  "src/local/",
@@ -214,7 +217,7 @@
214
217
  ]
215
218
  },
216
219
  "docs/agent-notes/models.md": {
217
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
220
+ "verifiedAgainst": "aeeaeb048860316dd4e08d84a658519ed6c0ad24",
218
221
  "paths": [
219
222
  "src/models/",
220
223
  "src/hub/inference.ts",
@@ -222,7 +225,7 @@
222
225
  ]
223
226
  },
224
227
  "docs/agent-notes/tasks.md": {
225
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
228
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
226
229
  "paths": [
227
230
  "src/hub/tasks.ts",
228
231
  "src/hub/board.ts",
@@ -256,11 +259,12 @@
256
259
  "src/hub/deliverables.ts",
257
260
  "src/hub/deliverable-files.ts",
258
261
  "src/hub/sdlc-merge.ts",
259
- "src/hub/sdlc-mirror.ts"
262
+ "src/hub/sdlc-mirror.ts",
263
+ "src/hub/ci-watch.ts"
260
264
  ]
261
265
  },
262
266
  "docs/agent-notes/tests.md": {
263
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
267
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
264
268
  "paths": [
265
269
  "test/",
266
270
  "scripts/check.sh",
@@ -280,11 +284,15 @@
280
284
  "scripts/check-closing-refs.mjs",
281
285
  ".github/workflows/closing-refs.yml",
282
286
  "scripts/lib/ts-ast.ts",
283
- "test/cli-json-guard.test.ts"
287
+ "test/cli-json-guard.test.ts",
288
+ "scripts/progress-tee.sh",
289
+ "scripts/release-prepare.ts",
290
+ "scripts/release-tag.ts",
291
+ "scripts/lib/release-plan.ts"
284
292
  ]
285
293
  },
286
294
  "docs/agent-notes/README.md": {
287
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
295
+ "verifiedAgainst": "43867c65caaadc8d7bdc44e034ecf1b933c8df77",
288
296
  "paths": [
289
297
  "src/cli/console-state.ts",
290
298
  "src/cli/attention.ts",
@@ -293,7 +301,7 @@
293
301
  ]
294
302
  },
295
303
  "src/cli/help.ts": {
296
- "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
304
+ "verifiedAgainst": "aeeaeb048860316dd4e08d84a658519ed6c0ad24",
297
305
  "paths": [
298
306
  "src/cli/main.ts",
299
307
  "src/cli/launch.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "bun": ">=1.4.2"
18
18
  },
19
19
  "dependencies": {
20
- "@staix/agent-mem": "0.10.0"
20
+ "@staix/agent-mem": "0.13.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
5
5
  "author": {
6
6
  "name": "Young Joon Lee",