@staix/agent-hub 0.18.0 → 0.20.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 (90) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -4
  3. package/docs/agent-notes/README.md +6 -0
  4. package/docs/agent-notes/adapters.md +13 -10
  5. package/docs/agent-notes/bus.md +1 -1
  6. package/docs/agent-notes/daemon.md +11 -4
  7. package/docs/agent-notes/local-worker.md +3 -1
  8. package/docs/agent-notes/tasks.md +6 -1
  9. package/docs/agent-notes/tests.md +8 -4
  10. package/docs/dogfood.md +13 -4
  11. package/docs/memory.md +1 -1
  12. package/docs/operations.md +98 -28
  13. package/docs/quickstart.md +13 -5
  14. package/docs/sdlc.md +12 -0
  15. package/docs/security.md +24 -15
  16. package/docs/smoke.md +3 -3
  17. package/docs/specs/2026-09-19-agent-hub-design.md +101 -8
  18. package/docs/verification/2026-10-25-remote-docker-transport.md +10 -0
  19. package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
  20. package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
  21. package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
  22. package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
  23. package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
  24. package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
  25. package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
  26. package/docs/verification/2026-10-27-0.19.0.md +86 -0
  27. package/docs/verification/2026-10-27-0.20.0.md +75 -0
  28. package/docs/verification/claude-permission-transport-native.py +187 -0
  29. package/docs/verification/claude-shell-wrapper-host.ts +20 -0
  30. package/docs/verification/claude-shell-wrapper-native.py +145 -0
  31. package/docs/verification/pi-140-model-free.ts +57 -0
  32. package/docs/verification/pi-140-native.mjs +21 -0
  33. package/docs/verified.json +25 -17
  34. package/package.json +2 -2
  35. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  36. package/plugins/agent-hub/server.js +373 -72
  37. package/src/adapters/acp.ts +8 -0
  38. package/src/adapters/agy/entry.ts +1 -1
  39. package/src/adapters/channel-peer.ts +7 -5
  40. package/src/adapters/claude-channel.ts +7 -3
  41. package/src/adapters/claude-implementer.ts +6 -3
  42. package/src/adapters/codex-appserver.ts +11 -1
  43. package/src/adapters/compat.ts +32 -18
  44. package/src/adapters/conformance/enrollment.ts +4 -4
  45. package/src/adapters/conformance/factories.ts +2 -1
  46. package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
  47. package/src/adapters/conformance/kit.ts +10 -0
  48. package/src/adapters/definitions/builtin-metadata.ts +2 -2
  49. package/src/adapters/definitions/native-factories.ts +7 -2
  50. package/src/adapters/definitions/reviewer-variants.ts +2 -2
  51. package/src/adapters/local-worker.ts +2 -0
  52. package/src/adapters/pi.ts +46 -3
  53. package/src/adapters/qwen-dual.ts +25 -0
  54. package/src/adapters/registry-types.ts +6 -2
  55. package/src/adapters/registry.ts +1 -0
  56. package/src/adapters/reviewer-print.ts +1 -0
  57. package/src/cli/agents.ts +69 -7
  58. package/src/cli/claude-shell-wrapper.ts +54 -0
  59. package/src/cli/main.ts +30 -24
  60. package/src/cli/output.ts +4 -0
  61. package/src/hub/agent-drift.ts +61 -0
  62. package/src/hub/board.ts +14 -2
  63. package/src/hub/check-runner.ts +148 -18
  64. package/src/hub/ci-watch.ts +140 -0
  65. package/src/hub/claude-launch-proof.ts +67 -2
  66. package/src/hub/claude-shell-wrapper.ts +349 -0
  67. package/src/hub/config-apply.ts +2 -2
  68. package/src/hub/config-edit.ts +2 -2
  69. package/src/hub/config-tier.ts +2 -2
  70. package/src/hub/config-trust.ts +1 -0
  71. package/src/hub/daemon.ts +282 -113
  72. package/src/hub/host-executable.ts +20 -1
  73. package/src/hub/instances.ts +18 -2
  74. package/src/hub/lifecycle.ts +1 -1
  75. package/src/hub/operational-authority.ts +7 -0
  76. package/src/hub/operational-view.ts +6 -10
  77. package/src/hub/peers.ts +34 -0
  78. package/src/hub/read-only-shell-proof.ts +7 -6
  79. package/src/hub/review-scope.ts +13 -0
  80. package/src/hub/sdlc-metrics.ts +6 -4
  81. package/src/hub/settings.ts +1 -0
  82. package/src/hub/task-shell-command.ts +85 -0
  83. package/src/hub/task-shell-default.ts +79 -0
  84. package/src/hub/task-shell-proof.ts +171 -0
  85. package/src/hub/task-shell-sandbox.ts +61 -0
  86. package/src/hub/tasks.ts +274 -16
  87. package/src/local/deny.ts +112 -11
  88. package/src/local/sandbox.ts +70 -29
  89. package/src/local/shell-environment.ts +10 -0
  90. package/src/ui/index.html +1 -0
@@ -68,7 +68,10 @@ function realPath(path) {
68
68
  }
69
69
  function isDenied(path, extra = []) {
70
70
  const norm = path.split(sep).join("/");
71
- return DENY_SEGMENTS.some((s) => hasSegment(norm, s)) || DENY_NAMES.some((re) => new RegExp(re).test(basename(norm))) || extra.some((e) => e && norm.includes(e));
71
+ if (DENY_SEGMENTS.some((s) => hasSegment(norm, s)))
72
+ return true;
73
+ const name = basename(norm);
74
+ return DENY_NAMES.some((re) => (denyNamePatterns.get(re) ?? new RegExp(re)).test(name)) || extra.some((e) => e && norm.includes(e));
72
75
  }
73
76
  function memoryGitEnv(env = process.env, options = {}) {
74
77
  const result = { ...env, GIT_TERMINAL_PROMPT: "0" };
@@ -82,7 +85,8 @@ function memoryGitEnv(env = process.env, options = {}) {
82
85
  }
83
86
  function memoryGitSync(args, options) {
84
87
  const argv = memoryGitArgv(args);
85
- return childProcess().spawnSync(argv[0], argv.slice(1), { ...ambientGitTimeout !== undefined ? { timeout: ambientGitTimeout, killSignal: "SIGKILL" } : {}, ...options, env: memoryGitEnv(options.env ?? process.env) });
88
+ const timeout = gitTimeoutContext?.getStore();
89
+ return childProcess().spawnSync(argv[0], argv.slice(1), { ...timeout !== undefined ? { timeout, killSignal: "SIGKILL" } : {}, ...options, env: memoryGitEnv(options.env ?? process.env) });
86
90
  }
87
91
  function processSignature(pid) {
88
92
  try {
@@ -118,11 +122,12 @@ var require2, processes, childProcess = () => processes ??= require2("node:child
118
122
  const parent = dirname(absolute);
119
123
  return parent === absolute ? absolute : join(canonical(parent), basename(absolute));
120
124
  }
121
- }, DENY_SEGMENTS, DENY_NAMES, hasSegment = (rel, seg) => `/${rel}/`.includes(`/${seg}/`), SAFE_CONFIG, CREDENTIAL_KEYS, memoryGitArgv = (args, opts = {}) => ["git", "--no-pager", ...SAFE_CONFIG.filter((c) => !opts.credentials || !CREDENTIAL_KEYS.has(c)).flatMap((c) => ["-c", c]), ...args], ambientGitTimeout;
125
+ }, DENY_SEGMENTS, DENY_NAMES, denyNamePatterns, hasSegment = (rel, seg) => `/${rel}/`.includes(`/${seg}/`), SAFE_CONFIG, CREDENTIAL_KEYS, memoryGitArgv = (args, opts = {}) => ["git", "--no-pager", ...SAFE_CONFIG.filter((c) => !opts.credentials || !CREDENTIAL_KEYS.has(c)).flatMap((c) => ["-c", c]), ...args], gitTimeoutContext;
122
126
  var init_platform = __esm(() => {
123
127
  require2 = createRequire(import.meta.url);
124
128
  DENY_SEGMENTS = [".maru/secrets", ".agenthub/state", ".agenthub/archive"];
125
129
  DENY_NAMES = ["^\\.env", "\\.pem$", "\\.key$", "^id_rsa", "^id_ed25519", "^credentials(\\.|$)", "^auth\\.json$", "^\\.netrc$", "^\\.npmrc$", "^\\.pypirc$"];
130
+ denyNamePatterns = new Map(DENY_NAMES.map((source) => [source, new RegExp(source)]));
126
131
  SAFE_CONFIG = ["core.fsmonitor=false", "core.hooksPath=/dev/null", "diff.external=", "core.pager=cat", "credential.helper=", "core.askpass="];
127
132
  CREDENTIAL_KEYS = new Set(["credential.helper=", "core.askpass="]);
128
133
  });
@@ -291,13 +296,24 @@ var plain = (text) => text.length <= 64 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
291
296
  var init_compat = __esm(() => {
292
297
  init_project();
293
298
  COMPAT = {
299
+ pi: {
300
+ label: "Pi",
301
+ verified: ["1.4.0"],
302
+ gate: "contract",
303
+ probe: ["pi", "--version"],
304
+ config: "pi.cmd",
305
+ parse: (out) => plain(out.trim()),
306
+ trustNote: "The model-free extension load, RPC session identity and managed tool receipt path were checked on 1.4.0. Permissions remain host-owned; account-backed inference and another version's extension contract are unverified.",
307
+ receipt: "docs/verification/2026-10-26-pi-140-compat.md"
308
+ },
294
309
  "claude-implementer": {
295
310
  label: "Claude headless implementer",
296
- verified: ["2.1.309"],
311
+ verified: ["2.1.309", "2.1.310"],
297
312
  gate: "contract",
298
313
  probe: ["claude", "--version"],
299
314
  parse: (out) => plain(out.trim().replace(/ \(Claude Code\)$/, "")),
300
- trustNote: "The native prompt-tool allow, deny, MCP error/disconnect and hook-failure fallthrough have an isolated receipt. No dontAsk/bypass fallback exists; another version's permission behavior is unverified."
315
+ trustNote: "The native prompt-tool transport and host-wrapper rewrite, environment isolation and replay contract have isolated receipts. No dontAsk/bypass fallback exists; another version's permission or rewrite behavior is unverified.",
316
+ receipt: "docs/verification/2026-10-26-headless-claude-shell-context.md"
301
317
  },
302
318
  "codex-review": {
303
319
  label: "Codex reviewer (codex-cli)",
@@ -306,19 +322,21 @@ var init_compat = __esm(() => {
306
322
  probe: ["codex", "--version"],
307
323
  config: "codex_bin",
308
324
  parse: (out) => plain(out.trim().replace(/^codex-cli /, "")),
309
- trustNote: "Every launch still proves the MCP inventory, Apps/plugins/hooks disabled, sandbox read/write denial and the binary fence; a failed leg still refuses. Trust covers only the source-level claims those probes cannot see (docs/verification/2026-10-19-reviewer-isolation.md)."
325
+ trustNote: "Every launch still proves the MCP inventory, Apps/plugins/hooks disabled, sandbox read/write denial and the binary fence; a failed leg still refuses. Trust covers only the source-level claims those probes cannot see (docs/verification/2026-10-19-reviewer-isolation.md).",
326
+ receipt: "docs/verification/2026-10-24-codex-0164-reviewer-isolation.md"
310
327
  },
311
328
  "kimi-review": {
312
329
  label: "Kimi reviewer (kimi-code)",
313
- verified: ["2.2.0", "2.3.0", "2.3.1"],
330
+ verified: ["2.2.0", "2.3.0", "2.3.1", "2.3.2"],
314
331
  gate: "isolation",
315
332
  probe: ["kimi", "--version"],
316
333
  parse: (out) => plain(out.trim()),
317
- trustNote: "Every launch still builds the private config, exact two-tool agent file and task-bound MCP; the native tool allowlist behaviour of an untested version is not re-proven."
334
+ trustNote: "Every launch still builds the private config, exact two-tool agent file and task-bound MCP; the native tool allowlist behaviour of an untested version is not re-proven.",
335
+ receipt: "docs/verification/2026-10-26-kimi-232-reviewer-compat.md"
318
336
  },
319
337
  jcode: {
320
338
  label: "Jcode",
321
- verified: ["0.98.1", "0.99.0"],
339
+ verified: ["0.98.1", "0.99.0", "0.99.1"],
322
340
  line: { min: "0.98.0", below: "1.0.0" },
323
341
  gate: "contract",
324
342
  probe: ["jcode", "--quiet", "version", "--json"],
@@ -331,25 +349,28 @@ var init_compat = __esm(() => {
331
349
  return;
332
350
  }
333
351
  },
334
- trustNote: "The ACP, serve and MCP contract was verified on 0.98.1 and steering on 0.99.0; an untested version may change them."
352
+ trustNote: "The ACP, serve and MCP contract was verified on 0.98.1 and 0.99.1 and steering on 0.99.0 and 0.99.1; an untested version may change them.",
353
+ receipt: "docs/verification/2026-10-26-jcode-0.99.1.md"
335
354
  },
336
355
  agy: {
337
356
  label: "agy",
338
- verified: ["1.4.2"],
357
+ verified: ["1.4.2", "1.4.7"],
339
358
  line: { min: "1.4.0", below: "1.5.0" },
340
359
  gate: "contract",
341
360
  probe: ["agy", "--version"],
342
361
  parse: (out) => plain(out.trim()),
343
- trustNote: "Only 1.4.2 is T0-verified; the plugin MCP and hook contract of another version is unverified."
362
+ trustNote: "Only 1.4.2 and 1.4.7 are T0-verified; the plugin MCP and hook contract of another version is unverified.",
363
+ receipt: "docs/verification/2026-10-26-agy-1.4.7.md"
344
364
  },
345
365
  qwen: {
346
366
  label: "Qwen Code",
347
- verified: ["0.25.0", "0.26.0"],
367
+ verified: ["0.25.0", "0.26.0", "0.26.1"],
348
368
  gate: "contract",
349
369
  probe: ["qwen", "--version"],
350
370
  config: "qwen_cmd",
351
371
  parse: (out) => plain(out.trim()),
352
- trustNote: "Native ACP identity and mode ids have dated receipts; 0.26.0 permission callbacks were checked without inference. Another version may change those contracts."
372
+ trustNote: "Native ACP identity, mode ids and permission callbacks were re-checked without inference on 0.26.1; its two ACP fixes were judged not to change the hub contract. Another version may change those contracts.",
373
+ receipt: "docs/verification/2026-10-26-qwen-0261-acp.md"
353
374
  },
354
375
  "qwen-dual": {
355
376
  label: "Qwen Code dual-output TUI",
@@ -358,7 +379,8 @@ var init_compat = __esm(() => {
358
379
  probe: ["qwen", "--version"],
359
380
  config: "qwen_cmd",
360
381
  parse: (out) => plain(out.trim()),
361
- trustNote: "The dual-output file contract (submit and confirmation_response input, user/assistant/stream_event/control events, session_start identity) was verified on 0.26.1 only. A hub deny ends the turn without any Qwen event, and no external stop of a running turn exists; another version may change either."
382
+ trustNote: "The dual-output file contract (submit and confirmation_response input, user/assistant/stream_event/control events, session_start identity) was verified on 0.26.1 only. A hub deny ends the turn without any Qwen event, and no external stop of a running turn exists; another version may change either.",
383
+ receipt: "docs/verification/2026-10-25-qwen-dual-output-t0.md"
362
384
  }
363
385
  };
364
386
  MAX_BYTES = 64 * 1024;
@@ -1394,6 +1416,7 @@ var init_reviewer_print = __esm(() => {
1394
1416
  ReviewerPrintPeer = class ReviewerPrintPeer extends BasePeer {
1395
1417
  opts;
1396
1418
  hubNative = true;
1419
+ hubOwned = true;
1397
1420
  generation = 0;
1398
1421
  child;
1399
1422
  childCleanup;
@@ -2922,6 +2945,9 @@ var init_acp = __esm(() => {
2922
2945
  };
2923
2946
  AcpPeer = class AcpPeer extends BasePeer {
2924
2947
  opts;
2948
+ deliveryOnly = true;
2949
+ hubOwned = true;
2950
+ form = "headless";
2925
2951
  permissionMode;
2926
2952
  nativePolicy;
2927
2953
  modeUnknown = false;
@@ -3037,6 +3063,10 @@ var init_acp = __esm(() => {
3037
3063
  recoveryMetadata() {
3038
3064
  return { launch: { kind: "acp", ...this.opts.launchModel ? { model: this.opts.launchModel } : {} }, ...this.sessionId ? { sessionId: this.sessionId } : {} };
3039
3065
  }
3066
+ get nativeSession() {
3067
+ const session = this.recoveryMetadata().sessionId;
3068
+ return typeof session === "string" ? session : undefined;
3069
+ }
3040
3070
  async start() {
3041
3071
  if (this.opts.requireOwnedGroup && this.proc)
3042
3072
  throw new Error("Configured ACP still has a process; stop it before starting another");
@@ -3643,6 +3673,7 @@ var init_codex_appserver = __esm(() => {
3643
3673
  TRACKED = new Set(["thread/start", "thread/resume"]);
3644
3674
  CodexPeer = class CodexPeer extends BasePeer {
3645
3675
  opts;
3676
+ frontendProxy;
3646
3677
  permissionMode;
3647
3678
  actionWaits = new Map;
3648
3679
  get actionGated() {
@@ -3687,6 +3718,17 @@ var init_codex_appserver = __esm(() => {
3687
3718
  super(id, opts.watchdogMs);
3688
3719
  this.opts = opts;
3689
3720
  this.permissionMode = opts.permissionMode ?? "ask";
3721
+ const owner = this;
3722
+ this.frontendProxy = {
3723
+ get url() {
3724
+ return owner.proxyUrl;
3725
+ },
3726
+ get attached() {
3727
+ return owner.proxyAttached;
3728
+ },
3729
+ setStartupPermissionMode: this.setStartupPermissionMode.bind(this),
3730
+ clearPermissionMode: this.clearPermissionMode.bind(this)
3731
+ };
3690
3732
  }
3691
3733
  recoveryMetadata() {
3692
3734
  return {
@@ -3697,6 +3739,9 @@ var init_codex_appserver = __esm(() => {
3697
3739
  get thread() {
3698
3740
  return this.threadId;
3699
3741
  }
3742
+ get nativeSession() {
3743
+ return this.threadId || undefined;
3744
+ }
3700
3745
  get proxyUrl() {
3701
3746
  return `ws://127.0.0.1:${this.server?.port ?? this.opts.proxyPort}`;
3702
3747
  }
@@ -4303,6 +4348,9 @@ ${renderDigest(envs, false)}`;
4303
4348
  link.up.send(JSON.stringify({ method: "thread/revert", id, params: { threadId: this.threadId, beforeTurnId: turnId } }));
4304
4349
  });
4305
4350
  }
4351
+ revertTurn(nativeTurn) {
4352
+ return this.revert(nativeTurn);
4353
+ }
4306
4354
  readUsage() {
4307
4355
  const link = this.link;
4308
4356
  if (!this.opts.onUsage || !link || link.up.readyState !== WebSocket.OPEN)
@@ -4819,7 +4867,10 @@ function qwenDualNamesStateDir(input, stateDir) {
4819
4867
  };
4820
4868
  return seen(input, 0);
4821
4869
  }
4822
- var SUBMIT_CAP, READ_CAP, ANSWERED_CAP = 256, QWEN_STALLED_NOTE = "stalled: Qwen has said nothing for a while; the hub cannot stop a Qwen turn, press Esc in the Qwen terminal", QWEN_DETACHED_NOTE = "detached: the hub cannot read Qwen's event file; Qwen keeps working in its terminal and the hub reattaches when the file is readable again", DIR = "qwen-dual", QwenDualPeer;
4870
+ function isQwenTaskNotification(text) {
4871
+ return text.length <= NOTIFICATION_CAP && QWEN_TASK_NOTIFICATIONS.test(text);
4872
+ }
4873
+ var SUBMIT_CAP, READ_CAP, ANSWERED_CAP = 256, QWEN_STALLED_NOTE = "stalled: Qwen has said nothing for a while; the hub cannot stop a Qwen turn, press Esc in the Qwen terminal", QWEN_DETACHED_NOTE = "detached: the hub cannot read Qwen's event file; Qwen keeps working in its terminal and the hub reattaches when the file is readable again", DIR = "qwen-dual", NOTIFICATION, QWEN_TASK_NOTIFICATIONS, NOTIFICATION_CAP, QwenDualPeer;
4823
4874
  var init_qwen_dual = __esm(() => {
4824
4875
  init_envelope();
4825
4876
  init_child_process();
@@ -4828,6 +4879,9 @@ var init_qwen_dual = __esm(() => {
4828
4879
  init_qwen_native();
4829
4880
  SUBMIT_CAP = 256 * 1024;
4830
4881
  READ_CAP = 1024 * 1024;
4882
+ NOTIFICATION = String.raw`<task-notification>\n<task-id>bg_[0-9a-f]{4,32}<\/task-id>\n<kind>[a-z_-]{1,32}<\/kind>\n<status>[a-z_-]{1,32}<\/status>\n(?:(?!<task-notification>|<\/task-notification>)[\s\S])*<\/task-notification>`;
4883
+ QWEN_TASK_NOTIFICATIONS = new RegExp(`^${NOTIFICATION}(?:\\n\\n${NOTIFICATION})*$`);
4884
+ NOTIFICATION_CAP = 64 * 1024;
4831
4885
  QwenDualPeer = class QwenDualPeer extends BasePeer {
4832
4886
  opts;
4833
4887
  permissionModeState = "unmanaged";
@@ -5276,6 +5330,13 @@ ${text}`;
5276
5330
  if (!this.stopping && !this.detached && this.state === "offline")
5277
5331
  this.setState("idle");
5278
5332
  }
5333
+ isNotificationEvent(event) {
5334
+ const content = event.message?.content;
5335
+ if (!Array.isArray(content) || content.length !== 1)
5336
+ return false;
5337
+ const block = content[0];
5338
+ return !!block && typeof block === "object" && block.type === "text" && typeof block.text === "string" && isQwenTaskNotification(block.text);
5339
+ }
5279
5340
  promptText(event) {
5280
5341
  const content = event.message?.content;
5281
5342
  if (typeof content === "string")
@@ -5286,6 +5347,8 @@ ${text}`;
5286
5347
  return blocks.length ? blocks.map((block) => block.text).join("") : undefined;
5287
5348
  }
5288
5349
  onUser(event) {
5350
+ if (this.isNotificationEvent(event))
5351
+ return;
5289
5352
  const text = this.promptText(event);
5290
5353
  if (text === undefined)
5291
5354
  return;
@@ -5808,29 +5871,73 @@ var init_path_policy = __esm(() => {
5808
5871
 
5809
5872
  // src/local/deny.ts
5810
5873
  import { basename as basename4, dirname as dirname8 } from "path";
5811
- function hubWriteRegexes(root, gitDirs = [], moduleGit) {
5874
+ function hubWriteRegexes(root, gitDirs = [], moduleGit = [], worktreesRoot) {
5812
5875
  const git = hfsFoldedName(".git");
5813
5876
  const re = (s) => `(regex ${sbplString(s)})`;
5814
5877
  const external = gitDirs.flatMap((d) => [
5815
5878
  `(require-all ${re(`^${sbplEscape(d)}/[^/]+$`)} ${re(`^.*/(${hfsFoldedName("config")}|${hfsFoldedName("hooks")})$`)})`,
5879
+ `(require-all ${re(`^${sbplEscape(d)}/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5816
5880
  `(subpath ${sbplString(`${d}/hooks`)})`,
5817
5881
  `(require-all (subpath ${sbplString(d)}) ${re(`^.*/${hfsFoldedName("commondir")}$`)})`
5818
5882
  ]);
5819
- const modules = moduleGit ? [
5820
- `(require-all ${re(`^${sbplEscape(moduleGit.root)}/(.*/)?${hfsFoldedName("config")}$`)} (require-not ${re(`^${sbplEscape(moduleGit.root)}/(.*/)?(refs|logs)/`)}))`,
5821
- `(require-all ${re(`^${sbplEscape(moduleGit.root)}/(.*/)?${hfsFoldedName("hooks")}(/|$)`)} (require-not ${re(`^${sbplEscape(moduleGit.root)}/(.*/)?(refs|logs)/`)}))`,
5822
- ...moduleGit.entries.map((d) => re(`^${sbplEscape(dirname8(d))}/${hfsFoldedName(basename4(d))}$`))
5823
- ] : [];
5883
+ const allDirs = moduleGit.flatMap((tree) => tree.dirs ?? []);
5884
+ const modules = moduleGit.flatMap((tree) => {
5885
+ const root = sbplEscape(tree.root);
5886
+ const found = tree.dirs ?? [];
5887
+ const inside = found.map((d) => `(require-not ${re(`^${sbplEscape(d)}(/|$)`)})`).join(" ");
5888
+ const anchored = found.flatMap((d) => {
5889
+ const dir = sbplEscape(d);
5890
+ return [
5891
+ `(require-all ${re(`^${dir}/[^/]+$`)} ${re(`^.*/${hfsFoldedName("config")}$`)})`,
5892
+ `(require-all ${re(`^${dir}/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5893
+ `(require-all ${re(`^${dir}/`)} ${re(hooksUnder(d))})`,
5894
+ `(require-all ${re(`^${dir}/worktrees/[^/]+/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5895
+ `(require-all ${re(`^${dir}/[^/]+$`)} ${re(`^.*/${hfsFoldedName("worktrees")}$`)})`,
5896
+ re(`^${dir}/worktrees/[^/]+$`),
5897
+ `(require-all ${re(`^${dir}/[^/]+$`)} ${re(`^.*/${hfsFoldedName("hooks")}$`)})`,
5898
+ `(require-all ${re(`^${dir}/worktrees/[^/]+/modules/.`)} ${allDirs.filter((f) => f.startsWith(`${d}/worktrees/`)).map((f) => `(require-not ${re(`^${sbplEscape(f)}(/|$)`)})`).join(" ")})`,
5899
+ `(require-all ${re(`^${dir}/worktrees/[^/]+/[^/]+$`)} ${re(`^.*/${hfsFoldedName("modules")}$`)})`
5900
+ ];
5901
+ });
5902
+ return [
5903
+ `(require-all ${re(`^${root}/.`)} ${inside})`,
5904
+ ...anchored,
5905
+ ...tree.entries.map((d) => `(require-all ${re(`^${sbplEscape(dirname8(d))}/[^/]+$`)} ${re(`^.*/${hfsFoldedName(basename4(d))}$`)})`)
5906
+ ];
5907
+ });
5908
+ const siblings = worktreesRoot ? (() => {
5909
+ const w = sbplEscape(worktreesRoot);
5910
+ return [
5911
+ `(require-all ${re(`^${sbplEscape(dirname8(worktreesRoot))}/[^/]+$`)} ${re(`^.*/${hfsFoldedName(basename4(worktreesRoot))}$`)})`,
5912
+ re(`^${w}/[^/]+$`),
5913
+ `(require-all ${re(`^${w}/[^/]+/[^/]+$`)} ${re(`^.*/${hfsFoldedName("config")}$`)})`,
5914
+ `(require-all ${re(`^${w}/[^/]+/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5915
+ `(require-all ${re(`^${w}/[^/]+/`)} ${re(hooksUnder(`${worktreesRoot}/x`))})`,
5916
+ `(require-all ${re(`^${w}/[^/]+/modules/.`)} ${moduleGit.flatMap((tree) => tree.dirs ?? []).filter((d) => d.startsWith(`${worktreesRoot}/`)).map((d) => `(require-not ${re(`^${sbplEscape(d)}(/|$)`)})`).join(" ")})`,
5917
+ `(require-all ${re(`^${w}/[^/]+/[^/]+$`)} ${re(`^.*/${hfsFoldedName("modules")}$`)})`
5918
+ ];
5919
+ })() : [];
5824
5920
  return [
5825
5921
  `(require-all (subpath ${sbplString(root)}) ${re(`^.*/${hfsFoldedName(".agenthub")}(/|$)`)})`,
5826
5922
  re(`^.*/${git}$`),
5827
5923
  re(`^.*/${git}/${hfsFoldedName("config")}$`),
5924
+ `(require-all ${re(`^.*/${git}/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5828
5925
  re(`^.*/${git}/${hfsFoldedName("hooks")}(/|$)`),
5829
5926
  `(require-all ${re(`^.*/${git}/.*$`)} ${re(`^.*/${hfsFoldedName("commondir")}$`)})`,
5830
5927
  ...modules,
5928
+ ...siblings,
5831
5929
  ...external
5832
5930
  ];
5833
5931
  }
5932
+ function objectStoreRules(common) {
5933
+ const objects = `${common}/objects`, o = sbplEscape(objects);
5934
+ const re = (s) => `(regex ${sbplString(s)})`;
5935
+ return [
5936
+ `(deny file-write* (subpath ${sbplString(`${objects}/info`)}) (subpath ${sbplString(`${objects}/pack`)}) ${[objects, `${objects}/info`, `${objects}/pack`].map((p) => `(literal ${sbplString(p)})`).join(" ")})`,
5937
+ `(deny file-write-unlink ${re(`^${o}/[0-9a-f][0-9a-f]$`)})`,
5938
+ `(deny file-write-data file-write-unlink file-write-mode file-write-flags ${re(`^${o}/[0-9a-f][0-9a-f]/[0-9a-f]+$`)})`
5939
+ ];
5940
+ }
5834
5941
  function denyRegexes(root, extra = [], withNames = true) {
5835
5942
  const names = DENY_NAMES.map((re) => {
5836
5943
  const body = re.startsWith("^") ? re.slice(1) : `[^/]*${re}`;
@@ -5842,12 +5949,16 @@ function denyRegexes(root, extra = [], withNames = true) {
5842
5949
  ...extra.filter(Boolean).map((e) => `^${sbplEscape(root)}/.*${sbplEscape(e)}`)
5843
5950
  ].map((re) => `(regex ${sbplString(re)})`);
5844
5951
  }
5845
- var sbplEscape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), sbplString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`, HFS_IGNORABLE_POINTS, HFS_IGNORABLE, hfsFoldedName = (name) => `${HFS_IGNORABLE}*${[...name].map((c) => sbplEscape(c)).join(`${HFS_IGNORABLE}*`)}${HFS_IGNORABLE}*`;
5952
+ var sbplEscape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), sbplString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`, HFS_IGNORABLE_POINTS, HFS_IGNORABLE, hfsFoldedName = (name) => `${HFS_IGNORABLE}*${[...name].map((c) => sbplEscape(c)).join(`${HFS_IGNORABLE}*`)}${HFS_IGNORABLE}*`, CONFIG_WORKTREE_NAME, MAX_ANCHOR_DEPTH = 80, hooksUnder = (parent) => {
5953
+ const depth = parent.split("/").filter(Boolean).length;
5954
+ return depth > MAX_ANCHOR_DEPTH ? `^.*/${hfsFoldedName("hooks")}(/|$)` : `^${"/[^/]+".repeat(depth)}/${hfsFoldedName("hooks")}(/|$)`;
5955
+ };
5846
5956
  var init_deny = __esm(() => {
5847
5957
  init_platform();
5848
5958
  init_platform();
5849
5959
  HFS_IGNORABLE_POINTS = ["\u200C", "\u200D", "\u200E", "\u200F", "\u202A", "\u202B", "\u202C", "\u202D", "\u202E", "\u206A", "\u206B", "\u206C", "\u206D", "\u206E", "\u206F", "\uFEFF"];
5850
5960
  HFS_IGNORABLE = `(${HFS_IGNORABLE_POINTS.join("|")})`;
5961
+ CONFIG_WORKTREE_NAME = [`^.*/${hfsFoldedName("config")}\\.[^/]*$`, `^.*/[^/]*${hfsFoldedName("worktree")}$`];
5851
5962
  });
5852
5963
 
5853
5964
  // src/local/sandbox.ts
@@ -5871,39 +5982,70 @@ function externalGitDirs(root) {
5871
5982
  `).map((d) => resolve9(root, d));
5872
5983
  return [...new Set(dirs)].filter((d) => !d.startsWith(`${root}/`));
5873
5984
  }
5874
- function moduleGitDirs(root) {
5985
+ function worktreesRoot(root) {
5875
5986
  const out = memoryGitSync(["-C", root, "rev-parse", "--git-common-dir"], { encoding: "utf8" });
5987
+ return out.status === 0 && out.stdout.trim() ? join12(resolve9(root, out.stdout.trim()), "worktrees") : undefined;
5988
+ }
5989
+ function moduleGitDirs(root) {
5990
+ const out = memoryGitSync(["-C", root, "rev-parse", "--git-common-dir", "--absolute-git-dir"], { encoding: "utf8" });
5876
5991
  if (out.status !== 0)
5877
- return;
5878
- const modulesRoot = join12(resolve9(root, out.stdout.trim()), "modules");
5879
- const dirs = [];
5880
- const walk = (dir, depth) => {
5881
- if (depth > 8)
5882
- return;
5883
- let entries;
5884
- try {
5885
- entries = readdirSync(dir, { withFileTypes: true });
5886
- } catch {
5887
- return;
5992
+ return [];
5993
+ const [commonRaw, ownRaw] = out.stdout.trim().split(`
5994
+ `);
5995
+ if (!commonRaw || !ownRaw)
5996
+ return [];
5997
+ const common = resolve9(root, commonRaw), own = resolve9(root, ownRaw);
5998
+ const roots = new Set([join12(common, "modules"), join12(own, "modules")]);
5999
+ try {
6000
+ for (const e of readdirSync(join12(common, "worktrees"), { withFileTypes: true })) {
6001
+ if (!e.isDirectory())
6002
+ continue;
6003
+ const modules = join12(common, "worktrees", e.name, "modules");
6004
+ if (existsSync6(modules))
6005
+ roots.add(modules);
5888
6006
  }
5889
- const names = new Set(entries.map((e) => e.name));
5890
- if (names.has("HEAD") && names.has("config")) {
5891
- dirs.push(dir);
5892
- const nested = join12(dir, "modules");
5893
- if (names.has("modules"))
5894
- walk(nested, depth + 1);
5895
- return;
6007
+ } catch {}
6008
+ const trees = [];
6009
+ const tree = (modulesRoot, nesting) => {
6010
+ const dirs = [];
6011
+ const walk = (dir, depth) => {
6012
+ if (depth > 8)
6013
+ return;
6014
+ let entries;
6015
+ try {
6016
+ entries = readdirSync(dir, { withFileTypes: true });
6017
+ } catch {
6018
+ return;
6019
+ }
6020
+ const names = new Set(entries.map((e) => e.name));
6021
+ if (names.has("HEAD") && names.has("config")) {
6022
+ dirs.push(dir);
6023
+ return;
6024
+ }
6025
+ for (const e of entries)
6026
+ if (e.isDirectory())
6027
+ walk(join12(dir, e.name), depth + 1);
6028
+ };
6029
+ walk(modulesRoot, 0);
6030
+ const found = nesting >= MODULE_DEPTH ? [] : dirs;
6031
+ const entries = new Set([modulesRoot]);
6032
+ for (const d of found)
6033
+ for (let p = d;p.length > modulesRoot.length; p = dirname9(p))
6034
+ entries.add(p);
6035
+ trees.push({ root: modulesRoot, entries: [...entries], dirs: found });
6036
+ for (const d of found) {
6037
+ tree(join12(d, "modules"), nesting + 1);
6038
+ try {
6039
+ for (const e of readdirSync(join12(d, "worktrees"), { withFileTypes: true })) {
6040
+ if (e.isDirectory() && existsSync6(join12(d, "worktrees", e.name, "modules")))
6041
+ tree(join12(d, "worktrees", e.name, "modules"), nesting + 1);
6042
+ }
6043
+ } catch {}
5896
6044
  }
5897
- for (const e of entries)
5898
- if (e.isDirectory())
5899
- walk(join12(dir, e.name), depth + 1);
5900
6045
  };
5901
- walk(modulesRoot, 0);
5902
- const entries = new Set([modulesRoot]);
5903
- for (const d of dirs)
5904
- for (let p = d;p.length > modulesRoot.length; p = dirname9(p))
5905
- entries.add(p);
5906
- return { root: modulesRoot, entries: [...entries] };
6046
+ for (const modulesRoot of roots)
6047
+ tree(modulesRoot, 0);
6048
+ return trees;
5907
6049
  }
5908
6050
  function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5909
6051
  const home = homedir4();
@@ -5911,6 +6053,7 @@ function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5911
6053
  const root = realPath(cwd);
5912
6054
  const gitDirs = externalGitDirs(root);
5913
6055
  const moduleGit = moduleGitDirs(root);
6056
+ const worktrees = worktreesRoot(root);
5914
6057
  const creds = [".ssh", ".aws", ".gnupg", ".config/gh", ".config/gcloud", ".kube", ".docker", ".netrc", ".npmrc", ".omniroute", ".claude", ".codex", ".kimi-code", "Library/Keychains"];
5915
6058
  const dev = developerDir();
5916
6059
  const readable = [root, ...HOME_READABLE.map((p) => join12(home, p)), ...readAllow.map(inHome), ...gitDirs, ...dev ? [dev] : []];
@@ -5937,7 +6080,8 @@ function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5937
6080
  ...start,
5938
6081
  "(deny file-write*)",
5939
6082
  ...readOnly.length ? ['(allow file-write* (literal "/dev/null"))'] : [`(allow file-write* (subpath ${q(root)}) (regex #"^/dev/") ${gitDirs.map((d) => `(subpath ${q(d)})`).join(" ")})`],
5940
- `(deny file-write* ${hubWriteRegexes(root, gitDirs, moduleGit).join(" ")})`,
6083
+ `(deny file-write* ${hubWriteRegexes(root, gitDirs, moduleGit, worktrees).join(" ")})`,
6084
+ ...worktrees ? objectStoreRules(dirname9(worktrees)) : [],
5941
6085
  `(deny file-read* file-write* ${creds.map((c) => `(subpath ${q(join12(home, c))})`).join(" ")})`,
5942
6086
  `(deny file-read* file-write* ${denyRegexes(root, deny).join(" ")})`,
5943
6087
  ...network ? [`(allow file-read* ${CA_BUNDLES.map((p) => `(literal ${q(p)})`).join(" ")} (regex ${q("/certifi/cacert\\.pem$")}))`, `(deny file-read* file-write* ${places})`] : []
@@ -6012,7 +6156,7 @@ function proxyEnv(network) {
6012
6156
  const url = `http://127.0.0.1:${network.proxyPort}`;
6013
6157
  return { HTTPS_PROXY: url, https_proxy: url, HTTP_PROXY: url, http_proxy: url, ALL_PROXY: url, all_proxy: url, NO_PROXY: "", no_proxy: "" };
6014
6158
  }
6015
- var SANDBOX_EXEC = "/usr/bin/sandbox-exec", OUTPUT_CAP = 20000, sandboxAvailable = () => process.platform === "darwin" && existsSync6(SANDBOX_EXEC), q, HOME_READABLE, CA_BUNDLES, SYSTEM_READABLE, MACH_SERVICES, NETWORK_MACH_SERVICES, PROXY_MACH_SERVICES;
6159
+ var SANDBOX_EXEC = "/usr/bin/sandbox-exec", OUTPUT_CAP = 20000, sandboxAvailable = () => process.platform === "darwin" && existsSync6(SANDBOX_EXEC), q, HOME_READABLE, CA_BUNDLES, MODULE_DEPTH = 8, SYSTEM_READABLE, MACH_SERVICES, NETWORK_MACH_SERVICES, PROXY_MACH_SERVICES;
6016
6160
  var init_sandbox = __esm(() => {
6017
6161
  init_project();
6018
6162
  init_git();
@@ -6269,6 +6413,54 @@ import { randomUUID as randomUUID4 } from "crypto";
6269
6413
  import { existsSync as existsSync7, mkdirSync as mkdirSync9 } from "fs";
6270
6414
  import { join as join14, resolve as resolve11 } from "path";
6271
6415
  import { spawn as spawn9 } from "child_process";
6416
+ function piVersionAsync(command, cwd, timeoutMs = 1e4) {
6417
+ const [bin, ...prefix] = command;
6418
+ if (!bin)
6419
+ return Promise.resolve(assessVersion("pi", undefined));
6420
+ return new Promise((resolve) => {
6421
+ let proc;
6422
+ try {
6423
+ proc = spawn9(bin, [...prefix, "--version"], { cwd, env: peerChildEnv("pi"), detached: true, stdio: ["ignore", "pipe", "ignore"] });
6424
+ } catch {
6425
+ resolve(assessVersion("pi", undefined));
6426
+ return;
6427
+ }
6428
+ let output = "", bytes = 0, done = false;
6429
+ const finish = async (code) => {
6430
+ if (done)
6431
+ return;
6432
+ done = true;
6433
+ clearTimeout(timer);
6434
+ try {
6435
+ await stopOwnedProcess(proc, { group: true });
6436
+ } catch {
6437
+ resolve(assessVersion("pi", undefined));
6438
+ return;
6439
+ }
6440
+ resolve(assessVersion("pi", code === 0 && bytes <= 64 * 1024 ? COMPAT.pi.parse(output) : undefined));
6441
+ };
6442
+ const timer = setTimeout(() => {
6443
+ finish(null);
6444
+ }, timeoutMs);
6445
+ proc.once("spawn", () => trackGroup(proc));
6446
+ proc.stdout.on("data", (chunk) => {
6447
+ if (done)
6448
+ return;
6449
+ bytes += chunk.length;
6450
+ if (bytes > 64 * 1024) {
6451
+ finish(null);
6452
+ return;
6453
+ }
6454
+ output += chunk.toString("utf8");
6455
+ });
6456
+ proc.once("error", () => {
6457
+ finish(null);
6458
+ });
6459
+ proc.once("close", (code) => {
6460
+ finish(code);
6461
+ });
6462
+ });
6463
+ }
6272
6464
  function ownerStillAlive(pid, signature) {
6273
6465
  const current = processSignature(pid);
6274
6466
  if (current !== undefined)
@@ -6280,11 +6472,12 @@ function ownerStillAlive(pid, signature) {
6280
6472
  return false;
6281
6473
  }
6282
6474
  }
6283
- var PiToolSettlementFailure, PiPromptNotStarted, PiPromptUncertain, TERMINAL_OWNER_MAX_AGE_MS = 1000, PiPeer;
6475
+ var PiToolSettlementFailure, PiPromptNotStarted, PiPromptUncertain, PiVersionRefusal, TERMINAL_OWNER_MAX_AGE_MS = 1000, PiPeer;
6284
6476
  var init_pi = __esm(() => {
6285
6477
  init_launch();
6286
6478
  init_envelope();
6287
6479
  init_child_process();
6480
+ init_compat();
6288
6481
  init_process_signature();
6289
6482
  init_session();
6290
6483
  init_tools();
@@ -6299,9 +6492,16 @@ var init_pi = __esm(() => {
6299
6492
  };
6300
6493
  PiPromptUncertain = class PiPromptUncertain extends Error {
6301
6494
  };
6495
+ PiVersionRefusal = class PiVersionRefusal extends Error {
6496
+ constructor(verdict) {
6497
+ super(verdict.reason);
6498
+ }
6499
+ };
6302
6500
  PiPeer = class PiPeer extends BasePeer {
6303
6501
  opts;
6502
+ preflight;
6304
6503
  hubNative = true;
6504
+ hubOwned = true;
6305
6505
  proc;
6306
6506
  server;
6307
6507
  buffer = "";
@@ -6367,9 +6567,10 @@ var init_pi = __esm(() => {
6367
6567
  tuiCommands = new Map;
6368
6568
  tuiQueue = [];
6369
6569
  tuiWaiters = [];
6370
- constructor(id, opts) {
6570
+ constructor(id, opts, preflight) {
6371
6571
  super(id, opts.watchdogMs);
6372
6572
  this.opts = opts;
6573
+ this.preflight = preflight;
6373
6574
  }
6374
6575
  get tuiLaunch() {
6375
6576
  return this._tuiLaunch;
@@ -6414,6 +6615,10 @@ var init_pi = __esm(() => {
6414
6615
  const file = this.sessionFile && (exists || !empty) ? this.sessionFile : undefined;
6415
6616
  return { launch: { kind: "pi", cwd: this.opts.cwd, mode: this.opts.mode, backend: this.opts.backend, ...this.opts.model ? { model: this.opts.model } : {}, ...file ? { sessionFile: file } : {} }, ...this.sessionId ? { sessionId: this.sessionId } : {}, ...file ? { sessionFile: file } : {} };
6416
6617
  }
6618
+ get nativeSession() {
6619
+ const session = this.recoveryMetadata().sessionId;
6620
+ return typeof session === "string" ? session : undefined;
6621
+ }
6417
6622
  async captureResume() {
6418
6623
  if (this.state === "offline") {
6419
6624
  if (this.canReuseVerifiedEmptyResume())
@@ -6615,6 +6820,8 @@ var init_pi = __esm(() => {
6615
6820
  return Response.json({ decisions, ...reservation ? { reservation } : {} });
6616
6821
  }
6617
6822
  async startImpl() {
6823
+ if (this.preflight)
6824
+ await this.preflight();
6618
6825
  this.terminalOwnerObservation = undefined;
6619
6826
  this.stopping = false;
6620
6827
  this.rosterDetached = false;
@@ -6776,6 +6983,9 @@ var init_pi = __esm(() => {
6776
6983
  get mode() {
6777
6984
  return this.opts.mode;
6778
6985
  }
6986
+ get form() {
6987
+ return this.opts.mode;
6988
+ }
6779
6989
  async detachForRoster() {
6780
6990
  if (this.opts.mode !== "tui")
6781
6991
  throw new Error("Pi roster detach requires a personal TUI");
@@ -10154,6 +10364,8 @@ var init_local_worker = __esm(() => {
10154
10364
  LocalPeer = class LocalPeer extends BasePeer {
10155
10365
  opts;
10156
10366
  hubNative = true;
10367
+ deliveryOnly = true;
10368
+ hubOwned = true;
10157
10369
  history = [];
10158
10370
  sessionId = `agent-hub-local-${randomUUID6()}`;
10159
10371
  routes;
@@ -10756,7 +10968,7 @@ import { randomUUID as randomUUID8 } from "crypto";
10756
10968
  import { lstatSync as lstatSync11, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
10757
10969
  import { tmpdir as tmpdir5 } from "os";
10758
10970
  import { join as join17 } from "path";
10759
- function claudeImplementerArgs(turn, mcp, settings, prompt) {
10971
+ function claudeImplementerArgs(turn, mcp, settings, prompt, model) {
10760
10972
  return [
10761
10973
  "-p",
10762
10974
  "--output-format",
@@ -10772,6 +10984,7 @@ function claudeImplementerArgs(turn, mcp, settings, prompt) {
10772
10984
  "",
10773
10985
  "--settings",
10774
10986
  settings,
10987
+ ...model ? ["--model", model] : [],
10775
10988
  "--permission-mode",
10776
10989
  "default",
10777
10990
  "--permission-prompts",
@@ -10849,6 +11062,7 @@ var init_claude_implementer = __esm(() => {
10849
11062
  ClaudeImplementerPeer = class ClaudeImplementerPeer extends BasePeer {
10850
11063
  options;
10851
11064
  hubNative = true;
11065
+ hubOwned = true;
10852
11066
  generation = 0;
10853
11067
  owned;
10854
11068
  running;
@@ -11034,7 +11248,7 @@ var init_claude_implementer = __esm(() => {
11034
11248
  const child = new OwnedPrintChild({
11035
11249
  peer: this.id,
11036
11250
  command: this.command,
11037
- args: claudeImplementerArgs(turn, mcp, settings, this.options.systemPrompt),
11251
+ args: claudeImplementerArgs(turn, mcp, settings, this.options.systemPrompt, this.options.model),
11038
11252
  cwd: this.options.cwd,
11039
11253
  input: group.envs.map(frame).join(`
11040
11254
 
@@ -11166,7 +11380,14 @@ function createPi(ctx) {
11166
11380
  if (ctx.permission === "read-only")
11167
11381
  throw new Error("Pi has no verified read-only permission mapping");
11168
11382
  init_pi();
11169
- return new PiPeer(ctx.instance, ctx.daemon.pi);
11383
+ const options = ctx.daemon.pi;
11384
+ return new PiPeer(ctx.instance, options, async () => {
11385
+ const verdict = await piVersionAsync(options.cmd ?? ["pi"], options.cwd);
11386
+ if (!verdict.ok)
11387
+ throw new PiVersionRefusal(verdict);
11388
+ if (verdict.note)
11389
+ options.log?.(verdict.note);
11390
+ });
11170
11391
  }
11171
11392
  function createLocal(ctx) {
11172
11393
  if (!("local" in ctx.daemon))
@@ -11273,9 +11494,9 @@ var init_reviewer_variants = __esm(() => {
11273
11494
  init_kimi_review();
11274
11495
  init_native_factories();
11275
11496
  init_native_display();
11276
- codexVariant = reviewerVariant("codex", "Codex reviewer", { backend: codexReviewerBackend, defaultPool: true, declaration: "explicit" }, CODEX_DISPLAY);
11497
+ codexVariant = reviewerVariant("codex", "Codex reviewer", { backend: codexReviewerBackend, defaultPool: true, declaration: "explicit", scope: "code" }, CODEX_DISPLAY);
11277
11498
  CODEX_REVIEWER_VARIANT = { ...codexVariant, start: { ...codexVariant.start, create: createCodexReviewer } };
11278
- KIMI_REVIEWER_VARIANT = reviewerVariant("kimi", "Kimi reviewer", { backend: kimiReviewerBackend, defaultPool: true, declaration: "explicit" }, KIMI_DISPLAY);
11499
+ KIMI_REVIEWER_VARIANT = reviewerVariant("kimi", "Kimi reviewer", { backend: kimiReviewerBackend, defaultPool: true, declaration: "explicit", scope: "code" }, KIMI_DISPLAY);
11279
11500
  AGY_REVIEWER_VARIANT = {
11280
11501
  ...reviewerVariant("agy", "agy reviewer", { defaultPool: false, declaration: "automatic", unavailable: "agy is an implementer peer (#485)" }, { palette: { fg: "#6558a6", bg: "#f1effa" } }),
11281
11502
  optional: true
@@ -12760,8 +12981,8 @@ var init_builtin_metadata = __esm(() => {
12760
12981
  trust: { onPrem: false, hubSandboxed: false },
12761
12982
  start: { create: createClaudeImplementer, daemonPort: "claude-implementer", forms: headlessForms, defaultForm: "headless", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
12762
12983
  identity: { mcp: "env", currentChild: true },
12763
- instances: { mode: "headless", roles: ["implementer", "verifier"] },
12764
- permission: { grants: { ask: "every write, edit and shell command asks", "ask-when-needed": "file tools inside the owned worktree may run automatically; shell and protected paths ask" } },
12984
+ instances: { mode: "headless", roles: ["implementer", "verifier"], modelSelect: true },
12985
+ permission: { grants: { ask: "every write, edit and shell command asks", "ask-when-needed": "file tools, proven read-only shell commands and bound task commands inside the owned worktree may run automatically; protected paths and other commands ask" } },
12765
12986
  session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: false, launchKind: "claude-implementer", crashPlan: () => ({ resume: {}, how: "Claude implementer resumes its recorded per-task sessions" }) },
12766
12987
  turnEnd: "adapter",
12767
12988
  budget: { account: "claude" },
@@ -13681,7 +13902,8 @@ function assertNativeCapabilityScope(definition) {
13681
13902
  ["hooks.factsHook", declared(definition.hooks, "factsHook")],
13682
13903
  ["cli.entry", declared(definition.cli, "entry")],
13683
13904
  ["cli.preview", declared(definition.cli, "preview")],
13684
- ["cli.usage", declared(definition.cli, "usage")]
13905
+ ["cli.usage", declared(definition.cli, "usage")],
13906
+ ["instances.modelSelect", declared(definition.instances, "modelSelect")]
13685
13907
  ];
13686
13908
  const field = restricted.find(([, present]) => present)?.[0];
13687
13909
  if (field)
@@ -13821,8 +14043,49 @@ var init_lifecycle = __esm(() => {
13821
14043
  init_shard();
13822
14044
  });
13823
14045
 
14046
+ // node_modules/@staix/agent-mem/src/memory/search-score.ts
14047
+ function sql(expr, rank) {
14048
+ const get = (part) => sql(part, rank);
14049
+ switch (expr.op) {
14050
+ case "rank":
14051
+ return rank;
14052
+ case "tier":
14053
+ return `CASE tier ${Object.entries(TIER_WEIGHTS).map(([name, value]) => `WHEN '${name}' THEN ${real(value)}`).join(" ")} ELSE ${real(DEFAULT_TIER_WEIGHT)} END`;
14054
+ case "age":
14055
+ return `((julianday(?) - julianday(at)) / ${real(MONTH_DAYS)})`;
14056
+ case "constant":
14057
+ return real(expr.value);
14058
+ case "negative":
14059
+ return `(-(${get(expr.value)}))`;
14060
+ case "reciprocal":
14061
+ return `(1.0 / ${get(expr.value)})`;
14062
+ case "add":
14063
+ return `(${get(expr.left)} + ${get(expr.right)})`;
14064
+ case "multiply":
14065
+ return expr.right.op === "reciprocal" ? `(${get(expr.left)} / ${get(expr.right.value)})` : `(${get(expr.left)} * ${get(expr.right)})`;
14066
+ case "max":
14067
+ return `max(${get(expr.left)}, ${get(expr.right)})`;
14068
+ }
14069
+ }
14070
+ function rankedScoreSql(rank = "bm25(docs)") {
14071
+ return sql(SCORE, rank);
14072
+ }
14073
+ var MONTH_DAYS = 30, TIER_WEIGHTS, DEFAULT_TIER_WEIGHT = 0.9, TIER_FACTOR, DENOMINATOR, RECENCY_FACTOR, SCORE, real = (value) => Number.isInteger(value) ? `${value}.0` : String(value);
14074
+ var init_search_score = __esm(() => {
14075
+ TIER_WEIGHTS = { long: 1, capture: 0.5 };
14076
+ TIER_FACTOR = { op: "tier" };
14077
+ DENOMINATOR = { op: "add", left: { op: "constant", value: 1 }, right: { op: "max", left: { op: "constant", value: 0 }, right: { op: "age" } } };
14078
+ RECENCY_FACTOR = { op: "reciprocal", value: DENOMINATOR };
14079
+ SCORE = { op: "multiply", left: { op: "multiply", left: { op: "negative", value: { op: "rank" } }, right: TIER_FACTOR }, right: RECENCY_FACTOR };
14080
+ });
14081
+
13824
14082
  // node_modules/@staix/agent-mem/src/memory/search-fts.ts
13825
- var init_search_fts = () => {};
14083
+ var SCREENED_SCORE, SCREENED_SUBSTRING_SCORE;
14084
+ var init_search_fts = __esm(() => {
14085
+ init_search_score();
14086
+ SCREENED_SCORE = rankedScoreSql();
14087
+ SCREENED_SUBSTRING_SCORE = rankedScoreSql("-1.0");
14088
+ });
13826
14089
 
13827
14090
  // node_modules/@staix/agent-mem/src/memory/capture-input.ts
13828
14091
  var init_capture_input = __esm(() => {
@@ -14078,6 +14341,7 @@ var init_store = __esm(() => {
14078
14341
  init_machine();
14079
14342
  init_lifecycle();
14080
14343
  init_machine();
14344
+ init_search_score();
14081
14345
  init_search_fts();
14082
14346
  init_shard();
14083
14347
  init_capture();
@@ -21170,7 +21434,8 @@ var MACHINE_LOCAL = [
21170
21434
  "local.read_allow",
21171
21435
  "local.bash_network",
21172
21436
  "local.network_allow",
21173
- "terminal.open"
21437
+ "terminal.open",
21438
+ "tasks.auto_pull"
21174
21439
  ];
21175
21440
  var ALL_MACHINE_LOCAL = [...MACHINE_LOCAL, "action_policy", "protect_instruction_files", "qwen_cmd", "jcode_cmd", "jcode_home", "jcode_provider_profile", "adapters", "adapter_modules", "text_projection", "instances"];
21176
21441
  function configRefusal(cwd, name) {
@@ -21491,6 +21756,11 @@ init_platform();
21491
21756
  init_store();
21492
21757
  init_shard();
21493
21758
  init_work_lists();
21759
+ init_capture();
21760
+ init_capture_input();
21761
+ init_config2();
21762
+ init_policy();
21763
+ init_private_spans();
21494
21764
 
21495
21765
  // node_modules/@staix/agent-mem/src/index.ts
21496
21766
  init_project2();
@@ -21502,7 +21772,7 @@ init_store();
21502
21772
  init_sync();
21503
21773
  init_project2();
21504
21774
  var AMEM_NAMES = { memory: "amem", goal: "amem goal" };
21505
- var memoryUsage = (name = AMEM_NAMES.memory) => `usage: ${name} [list] [--tier short|long] [--kind <kind>] [--peer <p>] [--machine <id>] [--since <iso>] [--limit N] | search <words> [--history] [--as-of <UTC>] | show <id> [--history] [--as-of <UTC>] | graph <id|node> [--hops 1|2] [--history] [--as-of <UTC>] | promote|demote|forget <id> | supersede <old> <new> | rebuild | sync [--remote <url>] | machine [--label <name>] | import claude-mem [--db <path>] [--limit N] [--restart] | dedupe [--apply] | suggestions | confirm|reject <suggestion id> [--json]`;
21775
+ var memoryUsage = (name = AMEM_NAMES.memory) => `usage: ${name} [list] [--tier short|long] [--kind <kind>] [--peer <p>] [--machine <id>] [--since <iso>] [--limit N] | search <words> [--history] [--as-of <UTC>] | show <id> [--history] [--as-of <UTC>] | graph <id|node> [--hops 1|2] [--history] [--as-of <UTC>] | promote|demote|forget <id> | supersede <old> <new> | rebuild | sync [--remote <url>] | machine [--label <name>] | import claude-mem [--db <path>] [--limit N] [--restart] [--capture-days N] | dedupe [--apply] | suggestions | confirm|reject <suggestion id> [--json]`;
21506
21776
  var MEMORY_USAGE = memoryUsage();
21507
21777
  var goalUsage = (name = AMEM_NAMES.goal) => `usage: ${name} [list] [--all] | show <id> | new <title> [--milestone <text>]... [--depends <id>]... [--shared] | link <id> <task id> | state <id> open|active|blocked|done|dropped | done <id> | take <id> [--force] [--json]`;
21508
21778
  var GOAL_USAGE = goalUsage();
@@ -21536,7 +21806,7 @@ var tool2 = (name, description, inputSchema, readOnlyHint = true) => ({
21536
21806
  annotations: { readOnlyHint, destructiveHint: false, openWorldHint: false }
21537
21807
  });
21538
21808
  var MCP_TOOLS = [
21539
- tool2("amem_search", "Search screened memory in this project. Capture results are recall, not curated evidence.", object6({ query: string(1000), limit, since: string(40), includeCapture: { type: "boolean" } }, ["query"])),
21809
+ tool2("amem_search", "Search screened memory in this project. Capture results are recall, not curated evidence.", object6({ query: string(1000), limit, since: string(40), includeCapture: { type: "boolean" }, explain: { type: "boolean" } }, ["query"])),
21540
21810
  tool2("amem_evidence", "Return a bounded bundle of cited, live curated entries for a question. Untrusted memory data only: no answer or model call. Capture, repo-only notes, pending candidates and model answers are not evidence.", object6({ question: string(1000), limit: { type: "integer", minimum: 1, maximum: 20 }, maxBytes: { type: "integer", minimum: 256, maximum: 8192 } }, ["question"])),
21541
21811
  tool2("amem_show", "Show a screened memory entry in this project by its exact id or unique prefix.", object6({ id: string(80) }, ["id"])),
21542
21812
  tool2("amem_timeline", "Read screened memory near an entry or time in this project.", object6({ id: string(80), since: string(40), limit })),
@@ -21695,7 +21965,7 @@ init_sdlc_metrics();
21695
21965
  // package.json
21696
21966
  var package_default = {
21697
21967
  name: "@staix/agent-hub",
21698
- version: "0.18.0",
21968
+ version: "0.20.0",
21699
21969
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
21700
21970
  license: "MIT",
21701
21971
  type: "module",
@@ -21712,7 +21982,7 @@ var package_default = {
21712
21982
  bun: ">=1.4.2"
21713
21983
  },
21714
21984
  dependencies: {
21715
- "@staix/agent-mem": "0.8.0"
21985
+ "@staix/agent-mem": "0.11.0"
21716
21986
  },
21717
21987
  devDependencies: {
21718
21988
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -21774,6 +22044,9 @@ init_sdlc();
21774
22044
  init_envelope();
21775
22045
  var inFlight = new Set;
21776
22046
 
22047
+ // src/hub/review-scope.ts
22048
+ var isReviewScope = (value) => value === "full" || value === "code";
22049
+
21777
22050
  // src/hub/tasks.ts
21778
22051
  init_review_findings();
21779
22052
  init_roles();
@@ -21825,6 +22098,10 @@ init_registry();
21825
22098
  // src/hub/task-sweep.ts
21826
22099
  var GROOMING_CODES = new Set(["tests", "spec", "roster", "pull-only", "not-attached", "excluded", "owner-admission", "offline", "paused", "failing", "pii", "policy", "cancelled", "unavailable", "runner", "approvals", "quota", "dependency", "no-writes"]);
21827
22100
 
22101
+ // src/hub/ci-watch.ts
22102
+ init_sdlc();
22103
+ var CI_WATCH_DEADLINE_MS = 45 * 60000;
22104
+
21828
22105
  // src/hub/tasks.ts
21829
22106
  init_sdlc();
21830
22107
  init_routing();
@@ -21834,6 +22111,7 @@ var QUIET_EVENTS = new Set(["answer", "reviewer changed", "idle sweep", "memory
21834
22111
  QUIET_EVENTS.add("ready held");
21835
22112
  QUIET_EVENTS.add("plan observed");
21836
22113
  QUIET_EVENTS.add("planner changed");
22114
+ QUIET_EVENTS.add("shell branch bound");
21837
22115
  var OWNERSHIP_EVENTS = new Set(["assigned", "escalated", "reassigned", "unassigned"]);
21838
22116
  var GROOM_FYI_WINDOW_MS = 10 * 60000;
21839
22117
 
@@ -21915,7 +22193,8 @@ function validateInstances(raw, catalog) {
21915
22193
  const path = `instances.${id}`, entry = fields2(descriptor.value, path);
21916
22194
  const declaredKind = entry.kind?.value;
21917
22195
  const jcodeFields = typeof declaredKind === "string" && resolve(declaredKind)?.instances?.nativeHome === true;
21918
- if (Object.keys(entry).some((key) => !["kind", "roles", "start_mode", "permission_mode", "worktree", "review_mode", "readOnly", ...jcodeFields ? JCODE_FIELDS : []].includes(key)))
22196
+ const modelSelect = typeof declaredKind === "string" && resolve(declaredKind)?.headlessVariant?.instances?.modelSelect === true;
22197
+ if (Object.keys(entry).some((key) => !["kind", "roles", "start_mode", "permission_mode", "worktree", "review_mode", "readOnly", "review_scope", ...jcodeFields ? JCODE_FIELDS : [], ...modelSelect ? ["model"] : []].includes(key)))
21919
22198
  return fail4(path, "unsupported field");
21920
22199
  const kind = entry.kind?.value;
21921
22200
  if (typeof kind !== "string")
@@ -21990,6 +22269,21 @@ function validateInstances(raw, catalog) {
21990
22269
  return fail4("instances", `at most ${MAX_JCODE_INSTANCES} extra Jcode instances are supported per project`);
21991
22270
  Object.assign(value, jcodeInstanceFields(entry, path));
21992
22271
  }
22272
+ if (entry.model && modelSelect) {
22273
+ if (!headlessWriter)
22274
+ return fail4(`${path}.model`, "only a headless implementer takes a model");
22275
+ const word = entry.model.value;
22276
+ if (typeof word !== "string" || !NATIVE_WORD.test(word))
22277
+ return fail4(`${path}.model`, "must be one bounded printable word without spaces");
22278
+ value.model = word;
22279
+ }
22280
+ if (entry.review_scope) {
22281
+ if (!freshReviewer)
22282
+ return fail4(`${path}.review_scope`, "only a fresh reviewer instance has a review scope");
22283
+ if (!isReviewScope(entry.review_scope.value))
22284
+ return fail4(`${path}.review_scope`, 'must be "code" or "full"');
22285
+ value.review_scope = entry.review_scope.value;
22286
+ }
21993
22287
  if (entry.readOnly)
21994
22288
  value.readOnly = entry.readOnly.value;
21995
22289
  if (entry.roles) {
@@ -30784,8 +31078,15 @@ async function callTool(req) {
30784
31078
  if (typeof args?.tool_name !== "string" || !/^[A-Za-z][A-Za-z0-9_.:/-]{0,127}$/.test(args.tool_name) || !args.input || typeof args.input !== "object" || Array.isArray(args.input) || typeof args.tool_use_id !== "string" || !args.tool_use_id.length || args.tool_use_id.length > 256)
30785
31079
  return deny("invalid native permission request");
30786
31080
  try {
30787
- const res = await hub.request({ t: "permission_request", tool: args?.tool_name, input: args?.input }, 3605000);
30788
- return res.ok === true && res.behavior === "allow" ? text5(JSON.stringify({ behavior: "allow", updatedInput: args?.input })) : deny(typeof res.message === "string" ? res.message : "permission denied");
31081
+ const res = await hub.request({ t: "permission_request", tool: args?.tool_name, input: args?.input, wrapperProtocol: 1 }, 3605000);
31082
+ if (res.ok !== true || res.behavior !== "allow")
31083
+ return deny(typeof res.message === "string" ? res.message : "permission denied");
31084
+ if (res.wrapperProtocol !== undefined && res.wrapperProtocol !== 1)
31085
+ return deny("unsupported host execution response");
31086
+ const updated = res.wrapperProtocol === 1 ? res.updatedInput : undefined;
31087
+ if (res.wrapperProtocol === 1 && (args.tool_name !== "Bash" || !updated || typeof updated !== "object" || Array.isArray(updated) || typeof updated.command !== "string"))
31088
+ return deny("invalid host execution response");
31089
+ return text5(JSON.stringify({ behavior: "allow", updatedInput: updated ?? args?.input }));
30789
31090
  } catch {
30790
31091
  return deny("hub permission unavailable");
30791
31092
  }