@staix/agent-hub 0.30.0 → 0.32.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 (130) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -7
  3. package/docs/agent-notes/README.md +1 -0
  4. package/docs/agent-notes/adapters.md +5 -2
  5. package/docs/agent-notes/budget.md +1 -0
  6. package/docs/agent-notes/bus.md +11 -4
  7. package/docs/agent-notes/daemon.md +16 -8
  8. package/docs/agent-notes/local-worker.md +1 -0
  9. package/docs/agent-notes/tasks.md +7 -3
  10. package/docs/agent-notes/tests.md +6 -4
  11. package/docs/design/adapters.md +52 -0
  12. package/docs/design/agent-hub-architecture.md +46 -0
  13. package/docs/design/bus.md +178 -1
  14. package/docs/design/task-board.md +82 -0
  15. package/docs/dogfood.md +23 -4
  16. package/docs/events.md +3 -0
  17. package/docs/memory.md +17 -1
  18. package/docs/operations.md +355 -78
  19. package/docs/profiles.md +35 -0
  20. package/docs/quickstart.md +24 -5
  21. package/docs/security.md +178 -60
  22. package/docs/specs/2026-09-19-agent-hub-design.md +127 -14
  23. package/docs/specs/2026-09-19-multi-project-design.md +7 -2
  24. package/docs/verification/2026-11-04-1171-channel-client-evidence.md +31 -0
  25. package/docs/verification/2026-11-05-0.31.0.md +182 -0
  26. package/docs/verification/2026-11-05-claude-implementer-2.1.318.md +70 -0
  27. package/docs/verification/2026-11-05-hardcoded-values-classification-rules.md +311 -0
  28. package/docs/verification/2026-11-05-hardcoded-values-ratchet.md +55 -0
  29. package/docs/verification/2026-11-06-0.32.0.md +149 -0
  30. package/docs/verification/2026-11-06-rooms-presence-cost.md +152 -0
  31. package/docs/verification/claude-shell-wrapper-native.py +1 -1
  32. package/docs/verified.json +14 -14
  33. package/package.json +3 -4
  34. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  35. package/plugins/agent-hub/server.js +4393 -3609
  36. package/src/adapters/acp.ts +5 -4
  37. package/src/adapters/channel-peer.ts +72 -3
  38. package/src/adapters/claude-channel-evidence.ts +52 -0
  39. package/src/adapters/claude-channel.ts +61 -18
  40. package/src/adapters/claude-implementer.ts +29 -7
  41. package/src/adapters/claude-print-child.ts +12 -2
  42. package/src/adapters/codex-appserver.ts +13 -12
  43. package/src/adapters/compat.ts +3 -3
  44. package/src/adapters/definitions/builtin-metadata.ts +13 -13
  45. package/src/adapters/definitions/qwen.ts +1 -0
  46. package/src/adapters/local-worker.ts +61 -18
  47. package/src/adapters/pi.ts +48 -6
  48. package/src/adapters/registry-types.ts +12 -0
  49. package/src/adapters/registry.ts +2 -0
  50. package/src/adapters/reviewer-print.ts +50 -11
  51. package/src/broker/server.ts +44 -13
  52. package/src/broker/store.ts +38 -8
  53. package/src/cli/answer-args.ts +50 -0
  54. package/src/cli/attention.ts +17 -2
  55. package/src/cli/backlog-return-args.ts +37 -0
  56. package/src/cli/console-question.ts +110 -0
  57. package/src/cli/console-state.ts +78 -17
  58. package/src/cli/console.ts +54 -1
  59. package/src/cli/handoff-args.ts +15 -5
  60. package/src/cli/handoff-output.ts +15 -2
  61. package/src/cli/help.ts +8 -5
  62. package/src/cli/identity.ts +13 -3
  63. package/src/cli/main.ts +128 -16
  64. package/src/cli/memory.ts +107 -3
  65. package/src/cli/output.ts +29 -6
  66. package/src/cli/pet.ts +4 -4
  67. package/src/cli/results-args.ts +30 -0
  68. package/src/cli/room.ts +20 -10
  69. package/src/cli/runtime-version-warning.ts +50 -0
  70. package/src/cli/status-lines.ts +41 -1
  71. package/src/cli/tail-render.ts +1 -0
  72. package/src/cli/upgrade-interactive.ts +14 -2
  73. package/src/hub/action-class.ts +9 -1
  74. package/src/hub/board.ts +369 -18
  75. package/src/hub/bus.ts +511 -53
  76. package/src/hub/completion-detail.ts +50 -0
  77. package/src/hub/completions-store.ts +168 -0
  78. package/src/hub/completions.ts +120 -0
  79. package/src/hub/config-apply.ts +7 -5
  80. package/src/hub/config-reload.ts +10 -1
  81. package/src/hub/control-client.ts +40 -4
  82. package/src/hub/daemon.ts +1576 -139
  83. package/src/hub/delivery-failure.ts +14 -0
  84. package/src/hub/delivery-hints.ts +14 -4
  85. package/src/hub/delivery-journal.ts +751 -42
  86. package/src/hub/delivery-superseded.ts +4 -1
  87. package/src/hub/envelope.ts +3 -3
  88. package/src/hub/events.ts +6 -0
  89. package/src/hub/handoff.ts +145 -6
  90. package/src/hub/hub-tools.ts +56 -7
  91. package/src/hub/manager.ts +59 -3
  92. package/src/hub/memory-policy.ts +44 -1
  93. package/src/hub/memory-sync-policy.ts +109 -0
  94. package/src/hub/memory-sync-state.ts +145 -0
  95. package/src/hub/no-writes.ts +1 -1
  96. package/src/hub/operational-settings.ts +11 -2
  97. package/src/hub/operational-view.ts +12 -1
  98. package/src/hub/peers.ts +25 -2
  99. package/src/hub/planning.ts +6 -1
  100. package/src/hub/question-desk.ts +201 -0
  101. package/src/hub/question-detach.ts +6 -0
  102. package/src/hub/questions.ts +260 -0
  103. package/src/hub/research-evidence.ts +106 -0
  104. package/src/hub/restart.ts +14 -0
  105. package/src/hub/review-anchor.ts +181 -0
  106. package/src/hub/review-brief.ts +13 -0
  107. package/src/hub/review-comments.ts +115 -0
  108. package/src/hub/review-findings.ts +7 -2
  109. package/src/hub/review-material.ts +76 -0
  110. package/src/hub/routing.ts +3 -1
  111. package/src/hub/task-sweep.ts +2 -0
  112. package/src/hub/tasks.ts +577 -63
  113. package/src/memory/capture.ts +5 -2
  114. package/src/pet/config.ts +25 -13
  115. package/src/rooms/commands.ts +61 -16
  116. package/src/rooms/link.ts +338 -36
  117. package/src/rooms/liveness.ts +11 -0
  118. package/src/rooms/remote-peer.ts +186 -19
  119. package/src/rooms/service.ts +475 -35
  120. package/src/rooms/status.ts +8 -1
  121. package/src/rooms/store.ts +447 -65
  122. package/src/rooms/wire.ts +24 -6
  123. package/src/ui/index.html +142 -9
  124. package/src/ui/needs.ts +10 -4
  125. package/src/ui/plugin-questions.ts +56 -0
  126. package/src/ui/plugin-results.ts +81 -0
  127. package/src/ui/plugin.ts +207 -27
  128. package/src/pet/window.html +0 -114
  129. package/src/pet/window.jxa.js +0 -122
  130. package/src/pet/window.ts +0 -154
@@ -1,16 +1,27 @@
1
1
  /** A host-owned room transport, never a task owner, reviewer, tool caller or native runtime. */
2
2
  import { BasePeer, type DeliveryReceipt } from "../hub/peers.ts";
3
3
  import type { Envelope, PeerState } from "../hub/envelope.ts";
4
- import { parseRemotePeerId, type RoomChatEnvelope } from "./application.ts";
4
+ import { encodeRoomMessage, parseRemotePeerId, type RoomChatEnvelope } from "./application.ts";
5
5
  import { readRoomOutbound, roomBoundary, type RoomBoundaryContext } from "./boundary.ts";
6
+ import { RoomNotSentError } from "./link.ts";
6
7
 
7
8
  export interface RemotePeerPorts {
8
9
  member: string;
9
10
  context: (env: Envelope) => RoomBoundaryContext;
10
11
  send: (env: RoomChatEnvelope, deliveryId: string, expiresAt: number, current: () => boolean, withheld: boolean) => Promise<void>;
11
12
  now?: () => number;
13
+ /** Current Bus admission, separate from immutable room trust/exposure proof. */
14
+ ready?: () => boolean;
12
15
  /** Record post-publication trust loss locally before any late signed outcome can settle it. */
13
- quarantine?: (deliveryId: string) => void;
16
+ quarantine?: (deliveryId: string, reason?: "not_exposed" | "boundary_refused" | "invalid_message") => void;
17
+ /** Requeue a send that the room link proves never reached its durable outbox. */
18
+ notSent?: (deliveryId: string) => boolean;
19
+ /** True for any durable RoomStore row with this Bus delivery ID; false only proves absence. */
20
+ queued?: (deliveryId: string) => boolean | undefined;
21
+ /** Persist a conservative Bus hold when neither requeue nor durable Store retry is available. */
22
+ holdMissing?: (deliveryId: string) => boolean;
23
+ /** Wake host retry scheduling after an active intent meets an external Bus fence. */
24
+ waiting?: () => void;
14
25
  }
15
26
 
16
27
  export class RemotePeer extends BasePeer {
@@ -20,8 +31,11 @@ export class RemotePeer extends BasePeer {
20
31
  private generation = 0;
21
32
  private running = false;
22
33
  private online = false;
34
+ private blockedNotSent = false;
23
35
  private observed: PeerState = "offline";
24
36
  private readonly pending = new Set<string>();
37
+ private readonly waitingForReady = new Set<string>();
38
+ private readonly sending = new Set<string>();
25
39
  private readonly deadlines = new Map<string, number>();
26
40
  constructor(id: string, readonly remoteRoom: string, readonly remoteQueueTtlMs: number, private readonly ports: RemotePeerPorts) {
27
41
  super(id);
@@ -31,50 +45,203 @@ export class RemotePeer extends BasePeer {
31
45
  }
32
46
  // Room ack/TTL deadlines, not the native inactivity watchdog, own transport settlement.
33
47
  protected override touch(): void { this.clearWatchdog(); }
34
- private update(): void { this.setState(this.pending.size ? "busy" : this.running && this.online ? this.observed : "offline"); }
48
+ private update(): void { this.setState(this.pending.size ? "busy" : this.running && this.online && !this.blockedNotSent ? this.observed : "offline"); }
49
+ private head(): string | undefined { return this.pending.values().next().value as string | undefined; }
50
+ /** Reserve recovered Store intents synchronously before any proof decrypt/screen await. */
51
+ reservePending(deliveryId: string): void {
52
+ this.pending.add(deliveryId);
53
+ this.waitingForReady.add(deliveryId);
54
+ this.blockedNotSent = false;
55
+ this.update();
56
+ }
35
57
  async start(): Promise<void> { this.running = true; this.generation++; this.update(); }
36
58
  async stop(): Promise<void> {
37
59
  this.running = false; this.generation++;
38
- for (const id of [...this.pending]) { this.quarantine(id); this.receipt({ id, state: "needs_review", reason: "room transport stopped" }); }
60
+ // Active journal rows and their sealed room frames survive a sender restart.
61
+ // Stop only fences in-flight callbacks; it is not evidence of an uncertain send.
62
+ this.pending.clear();
63
+ this.waitingForReady.clear();
39
64
  this.deadlines.clear(); this.update();
40
65
  }
41
- private quarantine(id: string): void { try { this.ports.quarantine?.(id); } catch { /* the Bus still retains uncertainty if room storage is unavailable */ } }
42
- presence(state: PeerState, online: boolean): void { this.observed = state; this.online = online; this.update(); }
66
+ private quarantine(id: string, reason?: "not_exposed" | "boundary_refused" | "invalid_message"): void { try { this.ports.quarantine?.(id, reason); } catch { /* The durable application remains for TTL review if local storage is unavailable. */ } }
67
+ private ready(): boolean { try { return this.ports.ready?.() !== false; } catch { return false; } }
68
+ waitForReady(deliveryId: string): void {
69
+ this.waitingForReady.add(deliveryId);
70
+ this.pending.add(deliveryId);
71
+ this.blockedNotSent = false;
72
+ this.update();
73
+ try { this.ports.waiting?.(); } catch { /* A future real Bus callback will retry. */ }
74
+ }
75
+ /** Consume an immediate resume token while retaining the active FIFO head. */
76
+ deferResume(deliveryId: string): void { this.waitingForReady.delete(deliveryId); }
77
+ readyToResume(deliveryId: string): boolean {
78
+ return this.waitingForReady.has(deliveryId) && this.head() === deliveryId && this.running && this.online && !this.blockedNotSent && this.ready();
79
+ }
80
+ isPendingHead(deliveryId: string): boolean { return this.head() === deliveryId; }
81
+ private holdMissing(id: string): boolean { try { return this.ports.holdMissing?.(id) === true; } catch { return false; } }
82
+ private notSent(id: string, blockUntilWake = true): void {
83
+ let retained: boolean | undefined;
84
+ try { retained = this.ports.queued?.(id); } catch { retained = undefined; }
85
+ if (retained === true) {
86
+ // A prior attempt is already durable. Never create a second Bus delivery ID.
87
+ if (!blockUntilWake || !this.ready()) this.waitForReady(id);
88
+ else { this.blockedNotSent = true; this.update(); }
89
+ return;
90
+ }
91
+ if (retained === undefined) {
92
+ // Store read failure cannot prove not-sent, so preserve the active RAM fence.
93
+ this.blockedNotSent = true; this.update(); return;
94
+ }
95
+ let requeued = false;
96
+ try { requeued = this.ports.notSent?.(id) === true; } catch { /* Keep uncertainty handling on the state transition below. */ }
97
+ // An expired requeue may have completed the Bus row synchronously. In that
98
+ // case its receipt already removed this ID and drained the following FIFO item.
99
+ if (!this.pending.has(id)) { this.update(); return; }
100
+ if (requeued) {
101
+ this.waitingForReady.delete(id);
102
+ this.blockedNotSent = blockUntilWake;
103
+ this.pending.delete(id);
104
+ this.update();
105
+ return;
106
+ }
107
+ const held = retained === false && this.holdMissing(id);
108
+ if (held) {
109
+ this.waitingForReady.delete(id);
110
+ this.blockedNotSent = false;
111
+ this.pending.delete(id);
112
+ } else this.blockedNotSent = true; // Keep the ID busy if the journal write failed or the actor went stale.
113
+ this.update();
114
+ }
115
+ presence(state: PeerState, online: boolean): void {
116
+ if (online && (!this.online || state !== this.observed)) this.blockedNotSent = false;
117
+ this.observed = state; this.online = online; this.update();
118
+ }
119
+ /** Rehydrate a journal-owned delivery without creating a new bus delivery or application id. */
120
+ async resume(env: Envelope, deliveryId: string, expiresAt: number, immutable: RoomChatEnvelope): Promise<void> {
121
+ if (!this.running || !this.online || !this.ready() || this.blockedNotSent || !deliveryId || this.head() !== deliveryId || !Number.isSafeInteger(expiresAt) || expiresAt <= (this.ports.now?.() ?? Date.now())) return;
122
+ this.waitingForReady.delete(deliveryId);
123
+ this.pending.add(deliveryId); this.update();
124
+ await this.publish(env, deliveryId, expiresAt, this.generation, immutable);
125
+ }
126
+ /** A link becoming ready is an actual wake-up after a typed not-sent refusal. */
127
+ wake(): void { this.blockedNotSent = false; this.update(); }
43
128
  setRemoteDeadline(envelopeId: string, expiresAt: number): void {
44
129
  if (Number.isSafeInteger(expiresAt) && expiresAt > 0 && this.deadlines.size < 200) this.deadlines.set(envelopeId, expiresAt);
45
130
  }
46
131
  receipt(receipt: DeliveryReceipt): boolean {
47
132
  if (!this.pending.has(receipt.id)) return false;
48
- if (receipt.state !== "accepted") this.pending.delete(receipt.id);
133
+ if (receipt.state !== "accepted") { this.pending.delete(receipt.id); this.waitingForReady.delete(receipt.id); }
49
134
  this.delivery(receipt); // Journal settlement precedes a state transition that may drain the next item.
50
135
  this.update();
51
136
  return true;
52
137
  }
138
+ /** Clear transport wait after the Bus records a signed outcome or durable hold. */
139
+ settled(deliveryId: string): void { this.pending.delete(deliveryId); this.waitingForReady.delete(deliveryId); this.update(); }
140
+ /** Reserve a restart-era active Store delivery before asynchronous proof validation. */
141
+ retainPending(deliveryId: string): void {
142
+ if (this.pending.has(deliveryId)) return;
143
+ this.pending.add(deliveryId);
144
+ this.waitingForReady.add(deliveryId);
145
+ this.update();
146
+ }
53
147
  async deliver(envs: Envelope[], deliveryId?: string): Promise<void> {
54
- if (!this.running || !this.online || !deliveryId || envs.length !== 1) throw new Error("room: unavailable");
148
+ if (!deliveryId || envs.length !== 1) throw new Error("room: unavailable");
149
+ if (!this.running || !this.online) { this.notSent(deliveryId); return; }
150
+ if (!this.ready()) { this.notSent(deliveryId, false); return; }
55
151
  const env = envs[0]!, generation = this.generation;
56
152
  const expiresAt = this.deadlines.get(env.id);
57
153
  this.deadlines.delete(env.id);
58
- const context = this.ports.context(env);
59
- const current = () => this.running && this.online && this.generation === generation && context.current();
154
+ if (expiresAt === undefined) throw new Error("room: queued deadline unavailable");
155
+ this.pending.add(deliveryId); this.update();
156
+ await this.publish(env, deliveryId, expiresAt, generation);
157
+ }
158
+ private async publish(env: Envelope, deliveryId: string, expiresAt: number, generation: number, immutable?: RoomChatEnvelope): Promise<void> {
159
+ if (this.sending.has(deliveryId)) return;
160
+ this.sending.add(deliveryId);
161
+ try { await this.publishOnce(env, deliveryId, expiresAt, generation, immutable); }
162
+ finally { this.sending.delete(deliveryId); }
163
+ }
164
+ private async publishOnce(env: Envelope, deliveryId: string, expiresAt: number, generation: number, immutable?: RoomChatEnvelope): Promise<void> {
165
+ let context: RoomBoundaryContext;
166
+ try { context = this.ports.context(env); }
167
+ catch { this.notSent(deliveryId); return; }
168
+ const trusted = () => { try { return context.current() === true; } catch { return false; } };
169
+ let readinessLost = false;
170
+ const current = () => {
171
+ const ready = this.ready();
172
+ if (!ready) readinessLost = true;
173
+ return this.pending.has(deliveryId) && this.running && this.online && this.generation === generation && ready && trusted();
174
+ };
60
175
  const result = await roomBoundary(env, { ...context, current });
61
- if (!current()) throw new Error("room: state_changed");
176
+ if (!this.pending.has(deliveryId)) return;
177
+ if (!current()) {
178
+ if (!trusted()) { this.quarantine(deliveryId, "boundary_refused"); this.receipt({ id: deliveryId, state: "needs_review", reason: "room trust changed before publication" }); }
179
+ else if (!this.running || !this.online || generation !== this.generation) this.notSent(deliveryId);
180
+ else if (!this.ready()) this.notSent(deliveryId, false);
181
+ return;
182
+ }
183
+ if (readinessLost && result.status === "refused" && trusted()) { this.notSent(deliveryId, false); return; }
62
184
  if (result.status === "refused") {
63
- this.delivery({ id: deliveryId, state: "failed_safe", reason: `room: ${result.code}` });
185
+ const reason = result.code === "not_exposed" || result.code === "invalid_message" ? result.code : "boundary_refused";
186
+ if (immutable) {
187
+ this.quarantine(deliveryId, reason);
188
+ this.receipt({ id: deliveryId, state: "needs_review", reason: `room retry refused: ${result.code}` });
189
+ } else this.receipt({ id: deliveryId, state: "failed_safe", reason: `room: ${result.code}` });
64
190
  return;
65
191
  }
66
192
  const view = readRoomOutbound(result.proof);
67
- if (!view || expiresAt === undefined || expiresAt <= (this.ports.now?.() ?? Date.now())) {
68
- this.delivery({ id: deliveryId, state: "needs_review", reason: "room queued deadline unavailable or expired" });
193
+ if (!view || expiresAt <= (this.ports.now?.() ?? Date.now())) {
194
+ if (immutable) this.quarantine(deliveryId, "boundary_refused");
195
+ this.receipt({ id: deliveryId, state: "needs_review", reason: "room queued deadline unavailable or expired" });
196
+ return;
197
+ }
198
+ if (immutable && Buffer.compare(Buffer.from(encodeRoomMessage({ v: 1, type: "chat", envelope: view })), Buffer.from(encodeRoomMessage({ v: 1, type: "chat", envelope: immutable }))) !== 0) {
199
+ this.quarantine(deliveryId, "boundary_refused");
200
+ this.receipt({ id: deliveryId, state: "needs_review", reason: "room retry privacy proof changed from immutable application" });
69
201
  return;
70
202
  }
71
- this.pending.add(deliveryId); this.update();
72
203
  try {
73
204
  await this.ports.send(view, deliveryId, expiresAt, () => current() && readRoomOutbound(result.proof) !== undefined, result.withheld);
74
- if (!current()) { this.quarantine(deliveryId); this.receipt({ id: deliveryId, state: "needs_review", reason: "room trust changed during publication" }); }
75
- } catch {
76
- this.quarantine(deliveryId);
77
- this.receipt({ id: deliveryId, state: "needs_review", reason: "room publication outcome is uncertain" });
205
+ if (!this.pending.has(deliveryId)) return;
206
+ if (!trusted() && this.running && this.online && generation === this.generation) {
207
+ this.quarantine(deliveryId, "boundary_refused"); this.receipt({ id: deliveryId, state: "needs_review", reason: "room trust changed during publication" });
208
+ return;
209
+ }
210
+ if (!this.ready()) {
211
+ let retained: boolean | undefined;
212
+ try { retained = this.ports.queued?.(deliveryId); } catch { retained = undefined; }
213
+ if (immutable || retained === true) this.waitForReady(deliveryId);
214
+ else this.notSent(deliveryId, false);
215
+ return;
216
+ }
217
+ } catch (error) {
218
+ if (!this.pending.has(deliveryId)) return;
219
+ if (error instanceof RoomNotSentError) {
220
+ let retained: boolean | undefined;
221
+ try { retained = this.ports.queued?.(deliveryId); } catch { retained = undefined; }
222
+ if (immutable || retained === true) {
223
+ if (!this.ready()) this.waitForReady(deliveryId);
224
+ } else this.notSent(deliveryId);
225
+ return;
226
+ }
227
+ if (!trusted()) {
228
+ this.quarantine(deliveryId, "boundary_refused");
229
+ this.receipt({ id: deliveryId, state: "needs_review", reason: "room trust changed during publication" });
230
+ return;
231
+ }
232
+ if (!this.running || !this.online || generation !== this.generation) return;
233
+ let retained: boolean | undefined;
234
+ try { retained = this.ports.queued?.(deliveryId); } catch { retained = undefined; }
235
+ if (!this.ready()) {
236
+ if (immutable || retained === true) this.waitForReady(deliveryId);
237
+ else this.notSent(deliveryId, false);
238
+ return;
239
+ }
240
+ if (retained !== true) {
241
+ if (retained === false && this.holdMissing(deliveryId)) {
242
+ this.pending.delete(deliveryId); this.blockedNotSent = false; this.update();
243
+ } else { this.blockedNotSent = true; this.update(); }
244
+ } // A durable intent stays active and retries on its next ACK deadline.
78
245
  }
79
246
  }
80
247
  }