@staix/agent-hub 0.29.0 → 0.31.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 (154) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -7
  3. package/docs/agent-notes/README.md +13 -0
  4. package/docs/agent-notes/adapters.md +7 -3
  5. package/docs/agent-notes/budget.md +1 -0
  6. package/docs/agent-notes/bus.md +10 -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 +10 -4
  10. package/docs/agent-notes/tests.md +8 -4
  11. package/docs/design/adapters.md +108 -0
  12. package/docs/design/agent-hub-architecture.md +58 -0
  13. package/docs/design/bus.md +147 -1
  14. package/docs/design/task-board.md +100 -0
  15. package/docs/dogfood.md +29 -8
  16. package/docs/events.md +3 -0
  17. package/docs/memory.md +17 -1
  18. package/docs/operations.md +410 -86
  19. package/docs/profiles.md +35 -0
  20. package/docs/quickstart.md +25 -5
  21. package/docs/sdlc.md +1 -1
  22. package/docs/security.md +182 -62
  23. package/docs/smoke.md +13 -2
  24. package/docs/specs/2026-09-19-agent-hub-design.md +84 -14
  25. package/docs/specs/2026-09-19-multi-project-design.md +7 -2
  26. package/docs/specs/2026-10-12-office-view-design.md +86 -3
  27. package/docs/verification/2026-11-03-0.30.0.md +54 -0
  28. package/docs/verification/2026-11-03-agy-1.4.12-launcher-nonce.md +42 -0
  29. package/docs/verification/2026-11-03-agy-1.4.12.md +90 -0
  30. package/docs/verification/2026-11-03-agy-tui-close.md +49 -0
  31. package/docs/verification/2026-11-03-grok-1.0.62-t0.md +150 -0
  32. package/docs/verification/2026-11-03-pi-150-compat.md +40 -0
  33. package/docs/verification/2026-11-03-pi-title-api.md +26 -0
  34. package/docs/verification/2026-11-03-qwen-0265-acp.md +55 -0
  35. package/docs/verification/2026-11-03-qwen-0265-dual-output.md +40 -0
  36. package/docs/verification/2026-11-04-1171-channel-client-evidence.md +31 -0
  37. package/docs/verification/2026-11-05-0.31.0.md +182 -0
  38. package/docs/verification/2026-11-05-claude-implementer-2.1.318.md +70 -0
  39. package/docs/verification/2026-11-05-hardcoded-values-classification-rules.md +257 -0
  40. package/docs/verification/2026-11-05-hardcoded-values-ratchet.md +55 -0
  41. package/docs/verification/2026-11-06-rooms-presence-cost.md +152 -0
  42. package/docs/verification/claude-shell-wrapper-native.py +1 -1
  43. package/docs/verification/dogfood/2026-11-03.md +3095 -0
  44. package/docs/verification/pi-150-model-free.ts +57 -0
  45. package/docs/verification/pi-150-native.mjs +21 -0
  46. package/docs/verification/pi-150-title-api.mjs +31 -0
  47. package/docs/verified.json +14 -14
  48. package/package.json +3 -4
  49. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  50. package/plugins/agent-hub/server.js +4565 -3617
  51. package/src/adapters/acp.ts +5 -4
  52. package/src/adapters/channel-peer.ts +72 -3
  53. package/src/adapters/claude-channel-evidence.ts +52 -0
  54. package/src/adapters/claude-channel.ts +61 -18
  55. package/src/adapters/claude-implementer.ts +29 -7
  56. package/src/adapters/claude-print-child.ts +12 -2
  57. package/src/adapters/codex-appserver.ts +13 -12
  58. package/src/adapters/compat.ts +13 -13
  59. package/src/adapters/definitions/builtin-metadata.ts +13 -13
  60. package/src/adapters/definitions/qwen.ts +1 -0
  61. package/src/adapters/local-worker.ts +82 -23
  62. package/src/adapters/pi.ts +140 -16
  63. package/src/adapters/registry-types.ts +16 -0
  64. package/src/adapters/registry.ts +4 -0
  65. package/src/adapters/reviewer-print.ts +50 -11
  66. package/src/broker/server.ts +44 -13
  67. package/src/broker/store.ts +38 -8
  68. package/src/cli/answer-args.ts +50 -0
  69. package/src/cli/attention.ts +17 -2
  70. package/src/cli/autonomy.ts +66 -1
  71. package/src/cli/backlog-return-args.ts +37 -0
  72. package/src/cli/console-question.ts +110 -0
  73. package/src/cli/console-state.ts +82 -17
  74. package/src/cli/console.ts +54 -1
  75. package/src/cli/handoff-args.ts +15 -5
  76. package/src/cli/handoff-output.ts +15 -2
  77. package/src/cli/help.ts +10 -5
  78. package/src/cli/identity.ts +21 -4
  79. package/src/cli/main.ts +167 -16
  80. package/src/cli/memory.ts +107 -3
  81. package/src/cli/output.ts +29 -6
  82. package/src/cli/pet.ts +4 -4
  83. package/src/cli/results-args.ts +30 -0
  84. package/src/cli/runtime-version-warning.ts +50 -0
  85. package/src/cli/status-lines.ts +41 -1
  86. package/src/cli/tail-render.ts +1 -0
  87. package/src/cli/upgrade-interactive.ts +14 -2
  88. package/src/cli/worktree-tidy.ts +158 -0
  89. package/src/cli/worktrees-output.ts +43 -0
  90. package/src/hub/action-class.ts +9 -1
  91. package/src/hub/board.ts +396 -20
  92. package/src/hub/bus.ts +497 -57
  93. package/src/hub/completion-detail.ts +50 -0
  94. package/src/hub/completions-store.ts +168 -0
  95. package/src/hub/completions.ts +120 -0
  96. package/src/hub/conductor.ts +4 -3
  97. package/src/hub/config-apply.ts +8 -5
  98. package/src/hub/config-reload.ts +10 -1
  99. package/src/hub/control-client.ts +40 -4
  100. package/src/hub/daemon.ts +1795 -142
  101. package/src/hub/delivery-failure.ts +14 -0
  102. package/src/hub/delivery-hints.ts +14 -4
  103. package/src/hub/delivery-journal.ts +751 -42
  104. package/src/hub/delivery-superseded.ts +4 -1
  105. package/src/hub/dogfood-read.ts +62 -9
  106. package/src/hub/envelope.ts +3 -3
  107. package/src/hub/events.ts +6 -0
  108. package/src/hub/handoff.ts +145 -6
  109. package/src/hub/hub-tools.ts +66 -11
  110. package/src/hub/manager.ts +67 -3
  111. package/src/hub/memory-policy.ts +44 -1
  112. package/src/hub/memory-sync-policy.ts +109 -0
  113. package/src/hub/memory-sync-state.ts +145 -0
  114. package/src/hub/no-writes.ts +1 -1
  115. package/src/hub/operational-settings.ts +11 -2
  116. package/src/hub/operational-view.ts +12 -1
  117. package/src/hub/peer-doing.ts +33 -3
  118. package/src/hub/peers.ts +32 -2
  119. package/src/hub/planning.ts +6 -1
  120. package/src/hub/question-desk.ts +201 -0
  121. package/src/hub/question-detach.ts +6 -0
  122. package/src/hub/questions.ts +260 -0
  123. package/src/hub/research-evidence.ts +106 -0
  124. package/src/hub/restart.ts +14 -0
  125. package/src/hub/review-anchor.ts +181 -0
  126. package/src/hub/review-brief.ts +13 -0
  127. package/src/hub/review-comments.ts +115 -0
  128. package/src/hub/review-findings.ts +7 -2
  129. package/src/hub/review-material.ts +76 -0
  130. package/src/hub/routing.ts +3 -1
  131. package/src/hub/settings.ts +3 -0
  132. package/src/hub/task-sweep.ts +2 -0
  133. package/src/hub/tasks.ts +607 -71
  134. package/src/hub/ui.ts +2 -0
  135. package/src/hub/worktree-prs.ts +100 -0
  136. package/src/hub/worktree-shelf.ts +246 -0
  137. package/src/memory/capture.ts +5 -2
  138. package/src/pet/config.ts +25 -13
  139. package/src/pi/extension.ts +43 -3
  140. package/src/rooms/link.ts +209 -21
  141. package/src/rooms/remote-peer.ts +186 -19
  142. package/src/rooms/service.ts +338 -22
  143. package/src/rooms/store.ts +437 -65
  144. package/src/rooms/wire.ts +24 -6
  145. package/src/ui/index.html +271 -17
  146. package/src/ui/needs.ts +10 -4
  147. package/src/ui/office-model.ts +64 -8
  148. package/src/ui/office-space.ts +1 -0
  149. package/src/ui/plugin-questions.ts +56 -0
  150. package/src/ui/plugin-results.ts +81 -0
  151. package/src/ui/plugin.ts +219 -28
  152. package/src/pet/window.html +0 -114
  153. package/src/pet/window.jxa.js +0 -122
  154. package/src/pet/window.ts +0 -154
package/src/rooms/wire.ts CHANGED
@@ -23,7 +23,7 @@ const FP = /^[0-9a-f]{64}$/;
23
23
  export interface RoomMember extends PublicHubIdentity { label: string; exposedPeers: string[] }
24
24
  export interface FrameWire {
25
25
  v: 1; type: "frame"; room: string; epoch: string; sender: string;
26
- recipients: string[]; seq: string; nonce: string; ciphertext: string; signature: string;
26
+ recipients: string[]; seq: string; ephemeral?: true; nonce: string; ciphertext: string; signature: string;
27
27
  }
28
28
  export interface RosterWire {
29
29
  v: 1; type: "roster"; room: string; epoch: string; owner: string;
@@ -107,8 +107,10 @@ function wrap(x: unknown): WrappedEpochKey {
107
107
  return { v: 1, owner: fingerprint(o.owner), recipient: fingerprint(o.recipient), ephemeralKey: b64(o.ephemeralKey, 32), salt: b64(o.salt, 32), nonce: b64(o.nonce, 12), ciphertext: ciphertext(o.ciphertext, 48, 48), signature: b64(o.signature, 64) };
108
108
  }
109
109
  function parseFrame(x: unknown): FrameWire {
110
- const o = object(x, ["v", "type", "room", "epoch", "sender", "recipients", "seq", "nonce", "ciphertext", "signature"]); base(o, "frame");
111
- return { v: 1, type: "frame", room: b64(o.room, 32), epoch: positiveU64(o.epoch), sender: fingerprint(o.sender), recipients: sorted(array(o.recipients, 64, fingerprint, true), x => x), seq: positiveU64(o.seq), nonce: b64(o.nonce, 12), ciphertext: ciphertext(o.ciphertext, 65552), signature: b64(o.signature, 64) };
110
+ const ephemeral = typeof x === "object" && x !== null && Object.hasOwn(x, "ephemeral");
111
+ const o = object(x, ["v", "type", "room", "epoch", "sender", "recipients", "seq", "nonce", "ciphertext", "signature", ...(ephemeral ? ["ephemeral"] : [])]); base(o, "frame");
112
+ if (ephemeral && o.ephemeral !== true) fail();
113
+ return { v: 1, type: "frame", room: b64(o.room, 32), epoch: positiveU64(o.epoch), sender: fingerprint(o.sender), recipients: sorted(array(o.recipients, 64, fingerprint, true), x => x), seq: positiveU64(o.seq), ...(ephemeral ? { ephemeral: true as const } : {}), nonce: b64(o.nonce, 12), ciphertext: ciphertext(o.ciphertext, 65552), signature: b64(o.signature, 64) };
112
114
  }
113
115
  function parseRoster(x: unknown): RosterWire {
114
116
  const o = object(x, ["v", "type", "room", "epoch", "owner", "members", "wraps", "privateNonce", "privateCiphertext", "signature"]); base(o, "roster");
@@ -228,6 +230,7 @@ export interface VerifiedPublicFrame {
228
230
  readonly digest: string;
229
231
  readonly rosterDigest: string;
230
232
  readonly recipients: readonly string[];
233
+ readonly ephemeral?: true;
231
234
  }
232
235
  interface PublicFrameState { wire: Uint8Array }
233
236
  const publicFrames = new WeakMap<VerifiedPublicFrame, PublicFrameState>();
@@ -252,7 +255,7 @@ export async function verifyPublicFrame(input: { wire: Uint8Array; roster: Verif
252
255
  await verifyFrameSignature(frame, sender);
253
256
  const frameDigest = await digest(canonicalBytes(frame));
254
257
  const proof = Object.freeze({ room: frame.room, epoch: frame.epoch, sender: frame.sender, seq: frame.seq,
255
- digest: frameDigest, rosterDigest: roster.digest, recipients: Object.freeze([...frame.recipients]) });
258
+ digest: frameDigest, rosterDigest: roster.digest, ...(frame.ephemeral ? { ephemeral: true as const } : {}), recipients: Object.freeze([...frame.recipients]) });
256
259
  publicFrames.set(proof, { wire });
257
260
  return proof;
258
261
  }
@@ -330,9 +333,10 @@ function frameAad(frame: Pick<FrameWire, "room" | "epoch" | "sender" | "seq">):
330
333
  return enc.encode(`${frame.room}|${frame.epoch}|${frame.sender}|${frame.seq}`);
331
334
  }
332
335
  export async function sealFrame(input: {
333
- epoch: VerifiedEpoch; identity: HubIdentity; recipients: string[]; plaintext: Uint8Array; sequence: SequencePort;
336
+ epoch: VerifiedEpoch; identity: HubIdentity; recipients: string[]; plaintext: Uint8Array; sequence: SequencePort; ephemeral?: true;
334
337
  }): Promise<Uint8Array> {
335
338
  await assertRoomCryptoReady();
339
+ if (input.ephemeral !== undefined && input.ephemeral !== true) fail();
336
340
  const epoch = input.epoch, identity = input.identity, sequence = input.sequence;
337
341
  const current = state(epoch), sender = identity.public.fingerprint;
338
342
  const member = current.roster.members.find(m => m.fingerprint === sender);
@@ -344,7 +348,7 @@ export async function sealFrame(input: {
344
348
  if (!sequence || typeof sequence.reserve !== "function" || typeof sequence.isCurrent !== "function") fail("state_unavailable");
345
349
  const binding = { room: epoch.room, sender, epoch: epoch.epoch, rosterDigest: epoch.digest };
346
350
  const seq = positiveU64(await portCall(() => sequence.reserve({ ...binding })));
347
- const header = { v: 1 as const, type: "frame" as const, room: epoch.room, epoch: epoch.epoch, sender, recipients, seq };
351
+ const header = { v: 1 as const, type: "frame" as const, room: epoch.room, epoch: epoch.epoch, sender, recipients, seq, ...(input.ephemeral ? { ephemeral: true as const } : {}) };
348
352
  const nonce = randomBytes(12);
349
353
  const encrypted = await encryptEpoch({ key: current.key, purpose: "frame", context: frameContext(epoch, sender), aad: frameAad(header), nonce, plaintext });
350
354
  const body = { ...header, nonce: encodeBase64url(nonce), ciphertext: encodeBase64url(encrypted) };
@@ -354,6 +358,20 @@ export async function sealFrame(input: {
354
358
  if (fresh !== true) fail("state_changed");
355
359
  return wire;
356
360
  }
361
+ /** Read a retained local send for resealing, never receiver admission or replay evidence. */
362
+ export async function openOwnFrame(input: { wire: Uint8Array; identity: HubIdentity; epoch: VerifiedEpoch }): Promise<Uint8Array> {
363
+ await assertRoomCryptoReady();
364
+ const frame = decodeFrame(input.wire), current = state(input.epoch), sender = input.identity.public;
365
+ const member = current.roster.members.find(value => value.fingerprint === sender.fingerprint);
366
+ if (frame.ephemeral || frame.room !== input.epoch.room || frame.epoch !== input.epoch.epoch || frame.sender !== sender.fingerprint
367
+ || !member || member.signingKey !== sender.signingKey || member.agreementKey !== sender.agreementKey
368
+ || frame.recipients.some(recipient => !current.roster.members.some(value => value.fingerprint === recipient))) fail("untrusted");
369
+ await verifyFrameSignature(frame, member);
370
+ const plaintext = await decryptEpoch({ key: current.key, purpose: "frame", context: frameContext(input.epoch, frame.sender), aad: frameAad(frame),
371
+ nonce: decodeBase64url(frame.nonce, 12), ciphertext: decodeBase64url(frame.ciphertext) });
372
+ if (plaintext.length > ROOM_WIRE_LIMITS.plaintext) fail("limit");
373
+ return plaintext;
374
+ }
357
375
  export type FrameOpenResult = { status: "accepted"; plaintext: Uint8Array; gap: string } | { status: "duplicate" | "stale" };
358
376
  export async function openFrame(input: {
359
377
  wire: Uint8Array; identity: HubIdentity; current: VerifiedEpoch;