@staix/agent-hub 0.18.0 → 0.19.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 (73) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -4
  3. package/docs/agent-notes/README.md +6 -0
  4. package/docs/agent-notes/adapters.md +10 -8
  5. package/docs/agent-notes/daemon.md +5 -2
  6. package/docs/agent-notes/local-worker.md +2 -1
  7. package/docs/agent-notes/tasks.md +4 -1
  8. package/docs/agent-notes/tests.md +6 -3
  9. package/docs/dogfood.md +9 -3
  10. package/docs/memory.md +1 -1
  11. package/docs/operations.md +66 -28
  12. package/docs/quickstart.md +9 -5
  13. package/docs/security.md +22 -13
  14. package/docs/smoke.md +3 -3
  15. package/docs/specs/2026-09-19-agent-hub-design.md +16 -5
  16. package/docs/verification/2026-10-25-remote-docker-transport.md +10 -0
  17. package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
  18. package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
  19. package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
  20. package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
  21. package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
  22. package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
  23. package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
  24. package/docs/verification/2026-10-27-0.19.0.md +86 -0
  25. package/docs/verification/claude-permission-transport-native.py +187 -0
  26. package/docs/verification/claude-shell-wrapper-host.ts +20 -0
  27. package/docs/verification/claude-shell-wrapper-native.py +145 -0
  28. package/docs/verification/pi-140-model-free.ts +57 -0
  29. package/docs/verification/pi-140-native.mjs +21 -0
  30. package/docs/verified.json +14 -14
  31. package/package.json +2 -2
  32. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  33. package/plugins/agent-hub/server.js +273 -68
  34. package/src/adapters/agy/entry.ts +1 -1
  35. package/src/adapters/claude-channel.ts +7 -3
  36. package/src/adapters/claude-implementer.ts +5 -3
  37. package/src/adapters/compat.ts +32 -18
  38. package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
  39. package/src/adapters/definitions/builtin-metadata.ts +2 -2
  40. package/src/adapters/definitions/native-factories.ts +7 -2
  41. package/src/adapters/definitions/reviewer-variants.ts +2 -2
  42. package/src/adapters/pi.ts +39 -3
  43. package/src/adapters/qwen-dual.ts +25 -0
  44. package/src/adapters/registry-types.ts +6 -2
  45. package/src/adapters/registry.ts +1 -0
  46. package/src/cli/agents.ts +69 -7
  47. package/src/cli/claude-shell-wrapper.ts +54 -0
  48. package/src/cli/main.ts +30 -24
  49. package/src/cli/output.ts +4 -0
  50. package/src/hub/agent-drift.ts +61 -0
  51. package/src/hub/board.ts +14 -2
  52. package/src/hub/check-runner.ts +148 -18
  53. package/src/hub/claude-shell-wrapper.ts +349 -0
  54. package/src/hub/config-edit.ts +2 -2
  55. package/src/hub/config-tier.ts +2 -2
  56. package/src/hub/daemon.ts +197 -35
  57. package/src/hub/host-executable.ts +20 -1
  58. package/src/hub/instances.ts +18 -2
  59. package/src/hub/lifecycle.ts +1 -1
  60. package/src/hub/operational-authority.ts +7 -0
  61. package/src/hub/operational-view.ts +6 -10
  62. package/src/hub/read-only-shell-proof.ts +7 -6
  63. package/src/hub/review-scope.ts +13 -0
  64. package/src/hub/sdlc-metrics.ts +6 -4
  65. package/src/hub/task-shell-command.ts +85 -0
  66. package/src/hub/task-shell-default.ts +78 -0
  67. package/src/hub/task-shell-proof.ts +171 -0
  68. package/src/hub/task-shell-sandbox.ts +61 -0
  69. package/src/hub/tasks.ts +139 -13
  70. package/src/local/deny.ts +91 -11
  71. package/src/local/sandbox.ts +67 -28
  72. package/src/local/shell-environment.ts +10 -0
  73. 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" };
@@ -118,11 +121,12 @@ var require2, processes, childProcess = () => processes ??= require2("node:child
118
121
  const parent = dirname(absolute);
119
122
  return parent === absolute ? absolute : join(canonical(parent), basename(absolute));
120
123
  }
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;
124
+ }, 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], ambientGitTimeout;
122
125
  var init_platform = __esm(() => {
123
126
  require2 = createRequire(import.meta.url);
124
127
  DENY_SEGMENTS = [".maru/secrets", ".agenthub/state", ".agenthub/archive"];
125
128
  DENY_NAMES = ["^\\.env", "\\.pem$", "\\.key$", "^id_rsa", "^id_ed25519", "^credentials(\\.|$)", "^auth\\.json$", "^\\.netrc$", "^\\.npmrc$", "^\\.pypirc$"];
129
+ denyNamePatterns = new Map(DENY_NAMES.map((source) => [source, new RegExp(source)]));
126
130
  SAFE_CONFIG = ["core.fsmonitor=false", "core.hooksPath=/dev/null", "diff.external=", "core.pager=cat", "credential.helper=", "core.askpass="];
127
131
  CREDENTIAL_KEYS = new Set(["credential.helper=", "core.askpass="]);
128
132
  });
@@ -291,13 +295,24 @@ var plain = (text) => text.length <= 64 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
291
295
  var init_compat = __esm(() => {
292
296
  init_project();
293
297
  COMPAT = {
298
+ pi: {
299
+ label: "Pi",
300
+ verified: ["1.4.0"],
301
+ gate: "contract",
302
+ probe: ["pi", "--version"],
303
+ config: "pi.cmd",
304
+ parse: (out) => plain(out.trim()),
305
+ 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.",
306
+ receipt: "docs/verification/2026-10-26-pi-140-compat.md"
307
+ },
294
308
  "claude-implementer": {
295
309
  label: "Claude headless implementer",
296
- verified: ["2.1.309"],
310
+ verified: ["2.1.309", "2.1.310"],
297
311
  gate: "contract",
298
312
  probe: ["claude", "--version"],
299
313
  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."
314
+ 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.",
315
+ receipt: "docs/verification/2026-10-26-headless-claude-shell-context.md"
301
316
  },
302
317
  "codex-review": {
303
318
  label: "Codex reviewer (codex-cli)",
@@ -306,19 +321,21 @@ var init_compat = __esm(() => {
306
321
  probe: ["codex", "--version"],
307
322
  config: "codex_bin",
308
323
  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)."
324
+ 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
+ receipt: "docs/verification/2026-10-24-codex-0164-reviewer-isolation.md"
310
326
  },
311
327
  "kimi-review": {
312
328
  label: "Kimi reviewer (kimi-code)",
313
- verified: ["2.2.0", "2.3.0", "2.3.1"],
329
+ verified: ["2.2.0", "2.3.0", "2.3.1", "2.3.2"],
314
330
  gate: "isolation",
315
331
  probe: ["kimi", "--version"],
316
332
  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."
333
+ 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
+ receipt: "docs/verification/2026-10-26-kimi-232-reviewer-compat.md"
318
335
  },
319
336
  jcode: {
320
337
  label: "Jcode",
321
- verified: ["0.98.1", "0.99.0"],
338
+ verified: ["0.98.1", "0.99.0", "0.99.1"],
322
339
  line: { min: "0.98.0", below: "1.0.0" },
323
340
  gate: "contract",
324
341
  probe: ["jcode", "--quiet", "version", "--json"],
@@ -331,25 +348,28 @@ var init_compat = __esm(() => {
331
348
  return;
332
349
  }
333
350
  },
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."
351
+ 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.",
352
+ receipt: "docs/verification/2026-10-26-jcode-0.99.1.md"
335
353
  },
336
354
  agy: {
337
355
  label: "agy",
338
- verified: ["1.4.2"],
356
+ verified: ["1.4.2", "1.4.7"],
339
357
  line: { min: "1.4.0", below: "1.5.0" },
340
358
  gate: "contract",
341
359
  probe: ["agy", "--version"],
342
360
  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."
361
+ trustNote: "Only 1.4.2 and 1.4.7 are T0-verified; the plugin MCP and hook contract of another version is unverified.",
362
+ receipt: "docs/verification/2026-10-26-agy-1.4.7.md"
344
363
  },
345
364
  qwen: {
346
365
  label: "Qwen Code",
347
- verified: ["0.25.0", "0.26.0"],
366
+ verified: ["0.25.0", "0.26.0", "0.26.1"],
348
367
  gate: "contract",
349
368
  probe: ["qwen", "--version"],
350
369
  config: "qwen_cmd",
351
370
  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."
371
+ 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.",
372
+ receipt: "docs/verification/2026-10-26-qwen-0261-acp.md"
353
373
  },
354
374
  "qwen-dual": {
355
375
  label: "Qwen Code dual-output TUI",
@@ -358,7 +378,8 @@ var init_compat = __esm(() => {
358
378
  probe: ["qwen", "--version"],
359
379
  config: "qwen_cmd",
360
380
  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."
381
+ 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
+ receipt: "docs/verification/2026-10-25-qwen-dual-output-t0.md"
362
383
  }
363
384
  };
364
385
  MAX_BYTES = 64 * 1024;
@@ -4819,7 +4840,10 @@ function qwenDualNamesStateDir(input, stateDir) {
4819
4840
  };
4820
4841
  return seen(input, 0);
4821
4842
  }
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;
4843
+ function isQwenTaskNotification(text) {
4844
+ return text.length <= NOTIFICATION_CAP && QWEN_TASK_NOTIFICATIONS.test(text);
4845
+ }
4846
+ 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
4847
  var init_qwen_dual = __esm(() => {
4824
4848
  init_envelope();
4825
4849
  init_child_process();
@@ -4828,6 +4852,9 @@ var init_qwen_dual = __esm(() => {
4828
4852
  init_qwen_native();
4829
4853
  SUBMIT_CAP = 256 * 1024;
4830
4854
  READ_CAP = 1024 * 1024;
4855
+ 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>`;
4856
+ QWEN_TASK_NOTIFICATIONS = new RegExp(`^${NOTIFICATION}(?:\\n\\n${NOTIFICATION})*$`);
4857
+ NOTIFICATION_CAP = 64 * 1024;
4831
4858
  QwenDualPeer = class QwenDualPeer extends BasePeer {
4832
4859
  opts;
4833
4860
  permissionModeState = "unmanaged";
@@ -5276,6 +5303,13 @@ ${text}`;
5276
5303
  if (!this.stopping && !this.detached && this.state === "offline")
5277
5304
  this.setState("idle");
5278
5305
  }
5306
+ isNotificationEvent(event) {
5307
+ const content = event.message?.content;
5308
+ if (!Array.isArray(content) || content.length !== 1)
5309
+ return false;
5310
+ const block = content[0];
5311
+ return !!block && typeof block === "object" && block.type === "text" && typeof block.text === "string" && isQwenTaskNotification(block.text);
5312
+ }
5279
5313
  promptText(event) {
5280
5314
  const content = event.message?.content;
5281
5315
  if (typeof content === "string")
@@ -5286,6 +5320,8 @@ ${text}`;
5286
5320
  return blocks.length ? blocks.map((block) => block.text).join("") : undefined;
5287
5321
  }
5288
5322
  onUser(event) {
5323
+ if (this.isNotificationEvent(event))
5324
+ return;
5289
5325
  const text = this.promptText(event);
5290
5326
  if (text === undefined)
5291
5327
  return;
@@ -5808,26 +5844,61 @@ var init_path_policy = __esm(() => {
5808
5844
 
5809
5845
  // src/local/deny.ts
5810
5846
  import { basename as basename4, dirname as dirname8 } from "path";
5811
- function hubWriteRegexes(root, gitDirs = [], moduleGit) {
5847
+ function hubWriteRegexes(root, gitDirs = [], moduleGit = [], worktreesRoot) {
5812
5848
  const git = hfsFoldedName(".git");
5813
5849
  const re = (s) => `(regex ${sbplString(s)})`;
5814
5850
  const external = gitDirs.flatMap((d) => [
5815
5851
  `(require-all ${re(`^${sbplEscape(d)}/[^/]+$`)} ${re(`^.*/(${hfsFoldedName("config")}|${hfsFoldedName("hooks")})$`)})`,
5852
+ `(require-all ${re(`^${sbplEscape(d)}/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5816
5853
  `(subpath ${sbplString(`${d}/hooks`)})`,
5817
5854
  `(require-all (subpath ${sbplString(d)}) ${re(`^.*/${hfsFoldedName("commondir")}$`)})`
5818
5855
  ]);
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
- ] : [];
5856
+ const allDirs = moduleGit.flatMap((tree) => tree.dirs ?? []);
5857
+ const modules = moduleGit.flatMap((tree) => {
5858
+ const root = sbplEscape(tree.root);
5859
+ const found = tree.dirs ?? [];
5860
+ const inside = found.map((d) => `(require-not ${re(`^${sbplEscape(d)}(/|$)`)})`).join(" ");
5861
+ const anchored = found.flatMap((d) => {
5862
+ const dir = sbplEscape(d);
5863
+ return [
5864
+ `(require-all ${re(`^${dir}/[^/]+$`)} ${re(`^.*/${hfsFoldedName("config")}$`)})`,
5865
+ `(require-all ${re(`^${dir}/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5866
+ `(require-all ${re(`^${dir}/`)} ${re(hooksUnder(d))})`,
5867
+ `(require-all ${re(`^${dir}/worktrees/[^/]+/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5868
+ `(require-all ${re(`^${dir}/[^/]+$`)} ${re(`^.*/${hfsFoldedName("worktrees")}$`)})`,
5869
+ re(`^${dir}/worktrees/[^/]+$`),
5870
+ `(require-all ${re(`^${dir}/[^/]+$`)} ${re(`^.*/${hfsFoldedName("hooks")}$`)})`,
5871
+ `(require-all ${re(`^${dir}/worktrees/[^/]+/modules/.`)} ${allDirs.filter((f) => f.startsWith(`${d}/worktrees/`)).map((f) => `(require-not ${re(`^${sbplEscape(f)}(/|$)`)})`).join(" ")})`,
5872
+ `(require-all ${re(`^${dir}/worktrees/[^/]+/[^/]+$`)} ${re(`^.*/${hfsFoldedName("modules")}$`)})`
5873
+ ];
5874
+ });
5875
+ return [
5876
+ `(require-all ${re(`^${root}/.`)} ${inside})`,
5877
+ ...anchored,
5878
+ ...tree.entries.map((d) => `(require-all ${re(`^${sbplEscape(dirname8(d))}/[^/]+$`)} ${re(`^.*/${hfsFoldedName(basename4(d))}$`)})`)
5879
+ ];
5880
+ });
5881
+ const siblings = worktreesRoot ? (() => {
5882
+ const w = sbplEscape(worktreesRoot);
5883
+ return [
5884
+ `(require-all ${re(`^${sbplEscape(dirname8(worktreesRoot))}/[^/]+$`)} ${re(`^.*/${hfsFoldedName(basename4(worktreesRoot))}$`)})`,
5885
+ re(`^${w}/[^/]+$`),
5886
+ `(require-all ${re(`^${w}/[^/]+/[^/]+$`)} ${re(`^.*/${hfsFoldedName("config")}$`)})`,
5887
+ `(require-all ${re(`^${w}/[^/]+/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5888
+ `(require-all ${re(`^${w}/[^/]+/`)} ${re(hooksUnder(`${worktreesRoot}/x`))})`,
5889
+ `(require-all ${re(`^${w}/[^/]+/modules/.`)} ${moduleGit.flatMap((tree) => tree.dirs ?? []).filter((d) => d.startsWith(`${worktreesRoot}/`)).map((d) => `(require-not ${re(`^${sbplEscape(d)}(/|$)`)})`).join(" ")})`,
5890
+ `(require-all ${re(`^${w}/[^/]+/[^/]+$`)} ${re(`^.*/${hfsFoldedName("modules")}$`)})`
5891
+ ];
5892
+ })() : [];
5824
5893
  return [
5825
5894
  `(require-all (subpath ${sbplString(root)}) ${re(`^.*/${hfsFoldedName(".agenthub")}(/|$)`)})`,
5826
5895
  re(`^.*/${git}$`),
5827
5896
  re(`^.*/${git}/${hfsFoldedName("config")}$`),
5897
+ `(require-all ${re(`^.*/${git}/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
5828
5898
  re(`^.*/${git}/${hfsFoldedName("hooks")}(/|$)`),
5829
5899
  `(require-all ${re(`^.*/${git}/.*$`)} ${re(`^.*/${hfsFoldedName("commondir")}$`)})`,
5830
5900
  ...modules,
5901
+ ...siblings,
5831
5902
  ...external
5832
5903
  ];
5833
5904
  }
@@ -5842,12 +5913,16 @@ function denyRegexes(root, extra = [], withNames = true) {
5842
5913
  ...extra.filter(Boolean).map((e) => `^${sbplEscape(root)}/.*${sbplEscape(e)}`)
5843
5914
  ].map((re) => `(regex ${sbplString(re)})`);
5844
5915
  }
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}*`;
5916
+ 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) => {
5917
+ const depth = parent.split("/").filter(Boolean).length;
5918
+ return depth > MAX_ANCHOR_DEPTH ? `^.*/${hfsFoldedName("hooks")}(/|$)` : `^${"/[^/]+".repeat(depth)}/${hfsFoldedName("hooks")}(/|$)`;
5919
+ };
5846
5920
  var init_deny = __esm(() => {
5847
5921
  init_platform();
5848
5922
  init_platform();
5849
5923
  HFS_IGNORABLE_POINTS = ["\u200C", "\u200D", "\u200E", "\u200F", "\u202A", "\u202B", "\u202C", "\u202D", "\u202E", "\u206A", "\u206B", "\u206C", "\u206D", "\u206E", "\u206F", "\uFEFF"];
5850
5924
  HFS_IGNORABLE = `(${HFS_IGNORABLE_POINTS.join("|")})`;
5925
+ CONFIG_WORKTREE_NAME = [`^.*/${hfsFoldedName("config")}\\.[^/]*$`, `^.*/[^/]*${hfsFoldedName("worktree")}$`];
5851
5926
  });
5852
5927
 
5853
5928
  // src/local/sandbox.ts
@@ -5871,39 +5946,70 @@ function externalGitDirs(root) {
5871
5946
  `).map((d) => resolve9(root, d));
5872
5947
  return [...new Set(dirs)].filter((d) => !d.startsWith(`${root}/`));
5873
5948
  }
5874
- function moduleGitDirs(root) {
5949
+ function worktreesRoot(root) {
5875
5950
  const out = memoryGitSync(["-C", root, "rev-parse", "--git-common-dir"], { encoding: "utf8" });
5951
+ return out.status === 0 && out.stdout.trim() ? join12(resolve9(root, out.stdout.trim()), "worktrees") : undefined;
5952
+ }
5953
+ function moduleGitDirs(root) {
5954
+ const out = memoryGitSync(["-C", root, "rev-parse", "--git-common-dir", "--absolute-git-dir"], { encoding: "utf8" });
5876
5955
  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;
5956
+ return [];
5957
+ const [commonRaw, ownRaw] = out.stdout.trim().split(`
5958
+ `);
5959
+ if (!commonRaw || !ownRaw)
5960
+ return [];
5961
+ const common = resolve9(root, commonRaw), own = resolve9(root, ownRaw);
5962
+ const roots = new Set([join12(common, "modules"), join12(own, "modules")]);
5963
+ try {
5964
+ for (const e of readdirSync(join12(common, "worktrees"), { withFileTypes: true })) {
5965
+ if (!e.isDirectory())
5966
+ continue;
5967
+ const modules = join12(common, "worktrees", e.name, "modules");
5968
+ if (existsSync6(modules))
5969
+ roots.add(modules);
5888
5970
  }
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;
5971
+ } catch {}
5972
+ const trees = [];
5973
+ const tree = (modulesRoot, nesting) => {
5974
+ const dirs = [];
5975
+ const walk = (dir, depth) => {
5976
+ if (depth > 8)
5977
+ return;
5978
+ let entries;
5979
+ try {
5980
+ entries = readdirSync(dir, { withFileTypes: true });
5981
+ } catch {
5982
+ return;
5983
+ }
5984
+ const names = new Set(entries.map((e) => e.name));
5985
+ if (names.has("HEAD") && names.has("config")) {
5986
+ dirs.push(dir);
5987
+ return;
5988
+ }
5989
+ for (const e of entries)
5990
+ if (e.isDirectory())
5991
+ walk(join12(dir, e.name), depth + 1);
5992
+ };
5993
+ walk(modulesRoot, 0);
5994
+ const found = nesting >= MODULE_DEPTH ? [] : dirs;
5995
+ const entries = new Set([modulesRoot]);
5996
+ for (const d of found)
5997
+ for (let p = d;p.length > modulesRoot.length; p = dirname9(p))
5998
+ entries.add(p);
5999
+ trees.push({ root: modulesRoot, entries: [...entries], dirs: found });
6000
+ for (const d of found) {
6001
+ tree(join12(d, "modules"), nesting + 1);
6002
+ try {
6003
+ for (const e of readdirSync(join12(d, "worktrees"), { withFileTypes: true })) {
6004
+ if (e.isDirectory() && existsSync6(join12(d, "worktrees", e.name, "modules")))
6005
+ tree(join12(d, "worktrees", e.name, "modules"), nesting + 1);
6006
+ }
6007
+ } catch {}
5896
6008
  }
5897
- for (const e of entries)
5898
- if (e.isDirectory())
5899
- walk(join12(dir, e.name), depth + 1);
5900
6009
  };
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] };
6010
+ for (const modulesRoot of roots)
6011
+ tree(modulesRoot, 0);
6012
+ return trees;
5907
6013
  }
5908
6014
  function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5909
6015
  const home = homedir4();
@@ -5911,6 +6017,7 @@ function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5911
6017
  const root = realPath(cwd);
5912
6018
  const gitDirs = externalGitDirs(root);
5913
6019
  const moduleGit = moduleGitDirs(root);
6020
+ const worktrees = worktreesRoot(root);
5914
6021
  const creds = [".ssh", ".aws", ".gnupg", ".config/gh", ".config/gcloud", ".kube", ".docker", ".netrc", ".npmrc", ".omniroute", ".claude", ".codex", ".kimi-code", "Library/Keychains"];
5915
6022
  const dev = developerDir();
5916
6023
  const readable = [root, ...HOME_READABLE.map((p) => join12(home, p)), ...readAllow.map(inHome), ...gitDirs, ...dev ? [dev] : []];
@@ -5937,7 +6044,7 @@ function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
5937
6044
  ...start,
5938
6045
  "(deny file-write*)",
5939
6046
  ...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(" ")})`,
6047
+ `(deny file-write* ${hubWriteRegexes(root, gitDirs, moduleGit, worktrees).join(" ")})`,
5941
6048
  `(deny file-read* file-write* ${creds.map((c) => `(subpath ${q(join12(home, c))})`).join(" ")})`,
5942
6049
  `(deny file-read* file-write* ${denyRegexes(root, deny).join(" ")})`,
5943
6050
  ...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 +6119,7 @@ function proxyEnv(network) {
6012
6119
  const url = `http://127.0.0.1:${network.proxyPort}`;
6013
6120
  return { HTTPS_PROXY: url, https_proxy: url, HTTP_PROXY: url, http_proxy: url, ALL_PROXY: url, all_proxy: url, NO_PROXY: "", no_proxy: "" };
6014
6121
  }
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;
6122
+ 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
6123
  var init_sandbox = __esm(() => {
6017
6124
  init_project();
6018
6125
  init_git();
@@ -6269,6 +6376,54 @@ import { randomUUID as randomUUID4 } from "crypto";
6269
6376
  import { existsSync as existsSync7, mkdirSync as mkdirSync9 } from "fs";
6270
6377
  import { join as join14, resolve as resolve11 } from "path";
6271
6378
  import { spawn as spawn9 } from "child_process";
6379
+ function piVersionAsync(command, cwd, timeoutMs = 1e4) {
6380
+ const [bin, ...prefix] = command;
6381
+ if (!bin)
6382
+ return Promise.resolve(assessVersion("pi", undefined));
6383
+ return new Promise((resolve) => {
6384
+ let proc;
6385
+ try {
6386
+ proc = spawn9(bin, [...prefix, "--version"], { cwd, env: peerChildEnv("pi"), detached: true, stdio: ["ignore", "pipe", "ignore"] });
6387
+ } catch {
6388
+ resolve(assessVersion("pi", undefined));
6389
+ return;
6390
+ }
6391
+ let output = "", bytes = 0, done = false;
6392
+ const finish = async (code) => {
6393
+ if (done)
6394
+ return;
6395
+ done = true;
6396
+ clearTimeout(timer);
6397
+ try {
6398
+ await stopOwnedProcess(proc, { group: true });
6399
+ } catch {
6400
+ resolve(assessVersion("pi", undefined));
6401
+ return;
6402
+ }
6403
+ resolve(assessVersion("pi", code === 0 && bytes <= 64 * 1024 ? COMPAT.pi.parse(output) : undefined));
6404
+ };
6405
+ const timer = setTimeout(() => {
6406
+ finish(null);
6407
+ }, timeoutMs);
6408
+ proc.once("spawn", () => trackGroup(proc));
6409
+ proc.stdout.on("data", (chunk) => {
6410
+ if (done)
6411
+ return;
6412
+ bytes += chunk.length;
6413
+ if (bytes > 64 * 1024) {
6414
+ finish(null);
6415
+ return;
6416
+ }
6417
+ output += chunk.toString("utf8");
6418
+ });
6419
+ proc.once("error", () => {
6420
+ finish(null);
6421
+ });
6422
+ proc.once("close", (code) => {
6423
+ finish(code);
6424
+ });
6425
+ });
6426
+ }
6272
6427
  function ownerStillAlive(pid, signature) {
6273
6428
  const current = processSignature(pid);
6274
6429
  if (current !== undefined)
@@ -6280,11 +6435,12 @@ function ownerStillAlive(pid, signature) {
6280
6435
  return false;
6281
6436
  }
6282
6437
  }
6283
- var PiToolSettlementFailure, PiPromptNotStarted, PiPromptUncertain, TERMINAL_OWNER_MAX_AGE_MS = 1000, PiPeer;
6438
+ var PiToolSettlementFailure, PiPromptNotStarted, PiPromptUncertain, PiVersionRefusal, TERMINAL_OWNER_MAX_AGE_MS = 1000, PiPeer;
6284
6439
  var init_pi = __esm(() => {
6285
6440
  init_launch();
6286
6441
  init_envelope();
6287
6442
  init_child_process();
6443
+ init_compat();
6288
6444
  init_process_signature();
6289
6445
  init_session();
6290
6446
  init_tools();
@@ -6299,8 +6455,14 @@ var init_pi = __esm(() => {
6299
6455
  };
6300
6456
  PiPromptUncertain = class PiPromptUncertain extends Error {
6301
6457
  };
6458
+ PiVersionRefusal = class PiVersionRefusal extends Error {
6459
+ constructor(verdict) {
6460
+ super(verdict.reason);
6461
+ }
6462
+ };
6302
6463
  PiPeer = class PiPeer extends BasePeer {
6303
6464
  opts;
6465
+ preflight;
6304
6466
  hubNative = true;
6305
6467
  proc;
6306
6468
  server;
@@ -6367,9 +6529,10 @@ var init_pi = __esm(() => {
6367
6529
  tuiCommands = new Map;
6368
6530
  tuiQueue = [];
6369
6531
  tuiWaiters = [];
6370
- constructor(id, opts) {
6532
+ constructor(id, opts, preflight) {
6371
6533
  super(id, opts.watchdogMs);
6372
6534
  this.opts = opts;
6535
+ this.preflight = preflight;
6373
6536
  }
6374
6537
  get tuiLaunch() {
6375
6538
  return this._tuiLaunch;
@@ -6615,6 +6778,8 @@ var init_pi = __esm(() => {
6615
6778
  return Response.json({ decisions, ...reservation ? { reservation } : {} });
6616
6779
  }
6617
6780
  async startImpl() {
6781
+ if (this.preflight)
6782
+ await this.preflight();
6618
6783
  this.terminalOwnerObservation = undefined;
6619
6784
  this.stopping = false;
6620
6785
  this.rosterDetached = false;
@@ -10756,7 +10921,7 @@ import { randomUUID as randomUUID8 } from "crypto";
10756
10921
  import { lstatSync as lstatSync11, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
10757
10922
  import { tmpdir as tmpdir5 } from "os";
10758
10923
  import { join as join17 } from "path";
10759
- function claudeImplementerArgs(turn, mcp, settings, prompt) {
10924
+ function claudeImplementerArgs(turn, mcp, settings, prompt, model) {
10760
10925
  return [
10761
10926
  "-p",
10762
10927
  "--output-format",
@@ -10772,6 +10937,7 @@ function claudeImplementerArgs(turn, mcp, settings, prompt) {
10772
10937
  "",
10773
10938
  "--settings",
10774
10939
  settings,
10940
+ ...model ? ["--model", model] : [],
10775
10941
  "--permission-mode",
10776
10942
  "default",
10777
10943
  "--permission-prompts",
@@ -11034,7 +11200,7 @@ var init_claude_implementer = __esm(() => {
11034
11200
  const child = new OwnedPrintChild({
11035
11201
  peer: this.id,
11036
11202
  command: this.command,
11037
- args: claudeImplementerArgs(turn, mcp, settings, this.options.systemPrompt),
11203
+ args: claudeImplementerArgs(turn, mcp, settings, this.options.systemPrompt, this.options.model),
11038
11204
  cwd: this.options.cwd,
11039
11205
  input: group.envs.map(frame).join(`
11040
11206
 
@@ -11166,7 +11332,14 @@ function createPi(ctx) {
11166
11332
  if (ctx.permission === "read-only")
11167
11333
  throw new Error("Pi has no verified read-only permission mapping");
11168
11334
  init_pi();
11169
- return new PiPeer(ctx.instance, ctx.daemon.pi);
11335
+ const options = ctx.daemon.pi;
11336
+ return new PiPeer(ctx.instance, options, async () => {
11337
+ const verdict = await piVersionAsync(options.cmd ?? ["pi"], options.cwd);
11338
+ if (!verdict.ok)
11339
+ throw new PiVersionRefusal(verdict);
11340
+ if (verdict.note)
11341
+ options.log?.(verdict.note);
11342
+ });
11170
11343
  }
11171
11344
  function createLocal(ctx) {
11172
11345
  if (!("local" in ctx.daemon))
@@ -11273,9 +11446,9 @@ var init_reviewer_variants = __esm(() => {
11273
11446
  init_kimi_review();
11274
11447
  init_native_factories();
11275
11448
  init_native_display();
11276
- codexVariant = reviewerVariant("codex", "Codex reviewer", { backend: codexReviewerBackend, defaultPool: true, declaration: "explicit" }, CODEX_DISPLAY);
11449
+ codexVariant = reviewerVariant("codex", "Codex reviewer", { backend: codexReviewerBackend, defaultPool: true, declaration: "explicit", scope: "code" }, CODEX_DISPLAY);
11277
11450
  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);
11451
+ KIMI_REVIEWER_VARIANT = reviewerVariant("kimi", "Kimi reviewer", { backend: kimiReviewerBackend, defaultPool: true, declaration: "explicit", scope: "code" }, KIMI_DISPLAY);
11279
11452
  AGY_REVIEWER_VARIANT = {
11280
11453
  ...reviewerVariant("agy", "agy reviewer", { defaultPool: false, declaration: "automatic", unavailable: "agy is an implementer peer (#485)" }, { palette: { fg: "#6558a6", bg: "#f1effa" } }),
11281
11454
  optional: true
@@ -12760,8 +12933,8 @@ var init_builtin_metadata = __esm(() => {
12760
12933
  trust: { onPrem: false, hubSandboxed: false },
12761
12934
  start: { create: createClaudeImplementer, daemonPort: "claude-implementer", forms: headlessForms, defaultForm: "headless", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
12762
12935
  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" } },
12936
+ instances: { mode: "headless", roles: ["implementer", "verifier"], modelSelect: true },
12937
+ 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
12938
  session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: false, launchKind: "claude-implementer", crashPlan: () => ({ resume: {}, how: "Claude implementer resumes its recorded per-task sessions" }) },
12766
12939
  turnEnd: "adapter",
12767
12940
  budget: { account: "claude" },
@@ -13681,7 +13854,8 @@ function assertNativeCapabilityScope(definition) {
13681
13854
  ["hooks.factsHook", declared(definition.hooks, "factsHook")],
13682
13855
  ["cli.entry", declared(definition.cli, "entry")],
13683
13856
  ["cli.preview", declared(definition.cli, "preview")],
13684
- ["cli.usage", declared(definition.cli, "usage")]
13857
+ ["cli.usage", declared(definition.cli, "usage")],
13858
+ ["instances.modelSelect", declared(definition.instances, "modelSelect")]
13685
13859
  ];
13686
13860
  const field = restricted.find(([, present]) => present)?.[0];
13687
13861
  if (field)
@@ -21491,6 +21665,11 @@ init_platform();
21491
21665
  init_store();
21492
21666
  init_shard();
21493
21667
  init_work_lists();
21668
+ init_capture();
21669
+ init_capture_input();
21670
+ init_config2();
21671
+ init_policy();
21672
+ init_private_spans();
21494
21673
 
21495
21674
  // node_modules/@staix/agent-mem/src/index.ts
21496
21675
  init_project2();
@@ -21502,7 +21681,7 @@ init_store();
21502
21681
  init_sync();
21503
21682
  init_project2();
21504
21683
  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]`;
21684
+ 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
21685
  var MEMORY_USAGE = memoryUsage();
21507
21686
  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
21687
  var GOAL_USAGE = goalUsage();
@@ -21695,7 +21874,7 @@ init_sdlc_metrics();
21695
21874
  // package.json
21696
21875
  var package_default = {
21697
21876
  name: "@staix/agent-hub",
21698
- version: "0.18.0",
21877
+ version: "0.19.0",
21699
21878
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
21700
21879
  license: "MIT",
21701
21880
  type: "module",
@@ -21712,7 +21891,7 @@ var package_default = {
21712
21891
  bun: ">=1.4.2"
21713
21892
  },
21714
21893
  dependencies: {
21715
- "@staix/agent-mem": "0.8.0"
21894
+ "@staix/agent-mem": "0.10.0"
21716
21895
  },
21717
21896
  devDependencies: {
21718
21897
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -21774,6 +21953,9 @@ init_sdlc();
21774
21953
  init_envelope();
21775
21954
  var inFlight = new Set;
21776
21955
 
21956
+ // src/hub/review-scope.ts
21957
+ var isReviewScope = (value) => value === "full" || value === "code";
21958
+
21777
21959
  // src/hub/tasks.ts
21778
21960
  init_review_findings();
21779
21961
  init_roles();
@@ -21915,7 +22097,8 @@ function validateInstances(raw, catalog) {
21915
22097
  const path = `instances.${id}`, entry = fields2(descriptor.value, path);
21916
22098
  const declaredKind = entry.kind?.value;
21917
22099
  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)))
22100
+ const modelSelect = typeof declaredKind === "string" && resolve(declaredKind)?.headlessVariant?.instances?.modelSelect === true;
22101
+ 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
22102
  return fail4(path, "unsupported field");
21920
22103
  const kind = entry.kind?.value;
21921
22104
  if (typeof kind !== "string")
@@ -21990,6 +22173,21 @@ function validateInstances(raw, catalog) {
21990
22173
  return fail4("instances", `at most ${MAX_JCODE_INSTANCES} extra Jcode instances are supported per project`);
21991
22174
  Object.assign(value, jcodeInstanceFields(entry, path));
21992
22175
  }
22176
+ if (entry.model && modelSelect) {
22177
+ if (!headlessWriter)
22178
+ return fail4(`${path}.model`, "only a headless implementer takes a model");
22179
+ const word = entry.model.value;
22180
+ if (typeof word !== "string" || !NATIVE_WORD.test(word))
22181
+ return fail4(`${path}.model`, "must be one bounded printable word without spaces");
22182
+ value.model = word;
22183
+ }
22184
+ if (entry.review_scope) {
22185
+ if (!freshReviewer)
22186
+ return fail4(`${path}.review_scope`, "only a fresh reviewer instance has a review scope");
22187
+ if (!isReviewScope(entry.review_scope.value))
22188
+ return fail4(`${path}.review_scope`, 'must be "code" or "full"');
22189
+ value.review_scope = entry.review_scope.value;
22190
+ }
21993
22191
  if (entry.readOnly)
21994
22192
  value.readOnly = entry.readOnly.value;
21995
22193
  if (entry.roles) {
@@ -30784,8 +30982,15 @@ async function callTool(req) {
30784
30982
  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
30983
  return deny("invalid native permission request");
30786
30984
  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");
30985
+ const res = await hub.request({ t: "permission_request", tool: args?.tool_name, input: args?.input, wrapperProtocol: 1 }, 3605000);
30986
+ if (res.ok !== true || res.behavior !== "allow")
30987
+ return deny(typeof res.message === "string" ? res.message : "permission denied");
30988
+ if (res.wrapperProtocol !== undefined && res.wrapperProtocol !== 1)
30989
+ return deny("unsupported host execution response");
30990
+ const updated = res.wrapperProtocol === 1 ? res.updatedInput : undefined;
30991
+ if (res.wrapperProtocol === 1 && (args.tool_name !== "Bash" || !updated || typeof updated !== "object" || Array.isArray(updated) || typeof updated.command !== "string"))
30992
+ return deny("invalid host execution response");
30993
+ return text5(JSON.stringify({ behavior: "allow", updatedInput: updated ?? args?.input }));
30789
30994
  } catch {
30790
30995
  return deny("hub permission unavailable");
30791
30996
  }