@staix/agent-hub 0.17.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 (109) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +4 -4
  3. package/docs/agent-notes/README.md +11 -0
  4. package/docs/agent-notes/adapters.md +10 -6
  5. package/docs/agent-notes/benchmarks.md +1 -0
  6. package/docs/agent-notes/daemon.md +6 -2
  7. package/docs/agent-notes/local-worker.md +3 -1
  8. package/docs/agent-notes/tasks.md +11 -2
  9. package/docs/agent-notes/tests.md +7 -3
  10. package/docs/dogfood.md +22 -2
  11. package/docs/memory.md +1 -1
  12. package/docs/operations.md +162 -35
  13. package/docs/profiles.md +36 -0
  14. package/docs/quickstart.md +15 -5
  15. package/docs/sdlc.md +29 -3
  16. package/docs/security.md +44 -13
  17. package/docs/smoke.md +3 -3
  18. package/docs/specs/2026-09-19-agent-hub-design.md +31 -10
  19. package/docs/verification/2026-10-25-0.18.0.md +76 -0
  20. package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
  21. package/docs/verification/2026-10-25-remote-docker-transport.md +56 -0
  22. package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
  23. package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
  24. package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
  25. package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
  26. package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
  27. package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
  28. package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
  29. package/docs/verification/2026-10-27-0.19.0.md +86 -0
  30. package/docs/verification/claude-permission-transport-native.py +187 -0
  31. package/docs/verification/claude-shell-wrapper-host.ts +20 -0
  32. package/docs/verification/claude-shell-wrapper-native.py +145 -0
  33. package/docs/verification/pi-140-model-free.ts +57 -0
  34. package/docs/verification/pi-140-native.mjs +21 -0
  35. package/docs/verification/profiles/YYYY-MM-DD-general.md +46 -0
  36. package/docs/verification/profiles/YYYY-MM-DD-research.md +61 -0
  37. package/docs/verification/profiles/YYYY-MM-DD-writing.md +46 -0
  38. package/docs/verified.json +24 -18
  39. package/package.json +2 -2
  40. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  41. package/plugins/agent-hub/server.js +364 -98
  42. package/src/adapters/agy/entry.ts +1 -1
  43. package/src/adapters/claude-channel.ts +7 -3
  44. package/src/adapters/claude-implementer.ts +5 -3
  45. package/src/adapters/compat.ts +32 -18
  46. package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
  47. package/src/adapters/definitions/builtin-metadata.ts +3 -3
  48. package/src/adapters/definitions/native-factories.ts +7 -2
  49. package/src/adapters/definitions/reviewer-variants.ts +2 -2
  50. package/src/adapters/local-worker.ts +5 -3
  51. package/src/adapters/pi.ts +39 -3
  52. package/src/adapters/qwen-dual.ts +25 -0
  53. package/src/adapters/registry-types.ts +6 -2
  54. package/src/adapters/registry.ts +1 -0
  55. package/src/cli/agents.ts +69 -7
  56. package/src/cli/claude-shell-wrapper.ts +54 -0
  57. package/src/cli/console-state.ts +9 -2
  58. package/src/cli/help.ts +29 -28
  59. package/src/cli/identity.ts +87 -6
  60. package/src/cli/main.ts +198 -71
  61. package/src/cli/output.ts +11 -2
  62. package/src/cli/sdlc-output.ts +242 -0
  63. package/src/cli/task-output.ts +212 -0
  64. package/src/cli/upgrade-output.ts +57 -0
  65. package/src/hub/agent-drift.ts +61 -0
  66. package/src/hub/board.ts +31 -2
  67. package/src/hub/check-runner.ts +385 -25
  68. package/src/hub/claude-shell-wrapper.ts +349 -0
  69. package/src/hub/conductor.ts +5 -0
  70. package/src/hub/config-apply.ts +16 -7
  71. package/src/hub/config-edit-host.ts +1 -1
  72. package/src/hub/config-edit.ts +2 -2
  73. package/src/hub/config-tier.ts +3 -3
  74. package/src/hub/daemon.ts +530 -87
  75. package/src/hub/dogfood-profile.ts +248 -0
  76. package/src/hub/dogfood-read.ts +77 -14
  77. package/src/hub/dogfood.ts +5 -1
  78. package/src/hub/events.ts +1 -1
  79. package/src/hub/host-executable.ts +21 -1
  80. package/src/hub/hub-tools.ts +2 -2
  81. package/src/hub/instances.ts +18 -2
  82. package/src/hub/lifecycle.ts +1 -1
  83. package/src/hub/operational-authority.ts +67 -0
  84. package/src/hub/operational-view.ts +137 -0
  85. package/src/hub/orphan-process.ts +69 -0
  86. package/src/hub/permission-mode.ts +1 -1
  87. package/src/hub/read-only-shell-proof.ts +81 -0
  88. package/src/hub/read-only-shell.ts +162 -0
  89. package/src/hub/review-scope.ts +13 -0
  90. package/src/hub/reviewer-pool-config.ts +3 -2
  91. package/src/hub/reviewer-pool.ts +3 -0
  92. package/src/hub/routing.ts +23 -6
  93. package/src/hub/sdlc-merge.ts +299 -0
  94. package/src/hub/sdlc-metrics.ts +6 -4
  95. package/src/hub/sdlc-mirror.ts +129 -0
  96. package/src/hub/sdlc.ts +58 -12
  97. package/src/hub/settings.ts +11 -5
  98. package/src/hub/snapshots.ts +60 -0
  99. package/src/hub/task-repository.ts +31 -5
  100. package/src/hub/task-shell-command.ts +85 -0
  101. package/src/hub/task-shell-default.ts +78 -0
  102. package/src/hub/task-shell-proof.ts +171 -0
  103. package/src/hub/task-shell-sandbox.ts +61 -0
  104. package/src/hub/tasks.ts +378 -27
  105. package/src/local/deny.ts +91 -11
  106. package/src/local/sandbox.ts +67 -28
  107. package/src/local/shell-environment.ts +10 -0
  108. package/src/local/tools.ts +17 -4
  109. package/src/ui/index.html +3 -1
@@ -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,7 @@ 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), { ...options, env: memoryGitEnv(options.env ?? process.env) });
88
+ return childProcess().spawnSync(argv[0], argv.slice(1), { ...ambientGitTimeout !== undefined ? { timeout: ambientGitTimeout, killSignal: "SIGKILL" } : {}, ...options, env: memoryGitEnv(options.env ?? process.env) });
86
89
  }
87
90
  function processSignature(pid) {
88
91
  try {
@@ -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];
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();
@@ -6154,9 +6261,26 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
6154
6261
  return "error: empty command";
6155
6262
  if (command.length > 4000)
6156
6263
  return "error: command longer than 4000 characters; put it in a script file with write, then run that";
6157
- const refused = approvalError(await ctx.permit(`bash: ${command}`, "bash", ctx.signal, undefined, undefined, undefined, a), "error: the user did not approve this command");
6158
- if (refused)
6159
- return refused;
6264
+ const input = Object.freeze({ ...a, command });
6265
+ for (let attempt = 0;; attempt++) {
6266
+ const refused = approvalError(await ctx.permit(`bash: ${command}`, "bash", ctx.signal, undefined, undefined, undefined, input, attempt ? { forceAsk: true } : undefined), "error: the user did not approve this command");
6267
+ if (refused)
6268
+ return refused;
6269
+ if (ctx.signal?.aborted)
6270
+ return "error: turn cancelled before shell execution";
6271
+ let check = "valid";
6272
+ try {
6273
+ check = ctx.shellExecutionCheck?.(input) ?? "valid";
6274
+ } catch {
6275
+ check = "ask";
6276
+ }
6277
+ if (check === "aborted" || ctx.signal?.aborted)
6278
+ return "error: turn cancelled before shell execution";
6279
+ if (check === "valid")
6280
+ break;
6281
+ if (attempt)
6282
+ return "error: read-only shell proof remained unstable; no command was executed";
6283
+ }
6160
6284
  const res = await sandboxedExec(["/bin/bash", "-c", command], { cwd: ctx.cwd, profile: ctx.sandboxProfile, readOnlyData: !!ctx.readOnly?.length, timeoutMs: (Number(a.timeout_s) || 120) * 1000, ...ctx.sandboxEnv ? { env: ctx.sandboxEnv } : {}, ...ctx.signal ? { signal: ctx.signal } : {} });
6161
6285
  try {
6162
6286
  ctx.onProcessExit?.(res.code);
@@ -6220,7 +6344,7 @@ var init_tools = __esm(() => {
6220
6344
  fn("read", "Read a text file inside the project. Returns numbered lines.", { path: str, offset: { type: "integer", minimum: 1 }, limit: { type: "integer", minimum: 1 } }, ["path"]),
6221
6345
  fn("write", "Create or overwrite a file inside the project. Needs the user's approval.", { path: str, content: str }, ["path", "content"]),
6222
6346
  fn("edit", "Replace one exact, unique occurrence of `old` with `new` in a file. Needs the user's approval.", { path: str, old: str, new: str }, ["path", "old", "new"]),
6223
- fn("bash", "Run a shell command in the project directory, sandboxed: no writes outside the project, no credential reads, no network unless the hub enables it. Needs the user's approval.", { command: str, timeout_s: { type: "integer", minimum: 1, maximum: 600 } }, ["command"]),
6347
+ fn("bash", "Run a shell command in the project directory, sandboxed: no writes outside the project, no credential reads, no network unless the hub enables it. Follows hub permission policy: proven bounded read-only commands may run automatically in ask-when-needed; other commands ask.", { command: str, timeout_s: { type: "integer", minimum: 1, maximum: 600 } }, ["command"]),
6224
6348
  fn("git", "Run git in the project. status, diff, log, show, ls-files, blame, rev-parse run freely; add, commit, restore, checkout, switch, stash, mv, rm, branch, merge, rebase, cherry-pick, revert, tag need approval; everything else (push, fetch, config, remote ...) is refused.", { args: { type: "array", items: str, minItems: 1 } }, ["args"]),
6225
6349
  fn("hub_send", "Send a message to the other agents now, before your turn ends. Conclusions only. Your final answer is shared anyway; use this for something that cannot wait.", { text: str, to: { type: "array", items: str } }, ["text"])
6226
6350
  ];
@@ -6252,6 +6376,54 @@ import { randomUUID as randomUUID4 } from "crypto";
6252
6376
  import { existsSync as existsSync7, mkdirSync as mkdirSync9 } from "fs";
6253
6377
  import { join as join14, resolve as resolve11 } from "path";
6254
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
+ }
6255
6427
  function ownerStillAlive(pid, signature) {
6256
6428
  const current = processSignature(pid);
6257
6429
  if (current !== undefined)
@@ -6263,11 +6435,12 @@ function ownerStillAlive(pid, signature) {
6263
6435
  return false;
6264
6436
  }
6265
6437
  }
6266
- var PiToolSettlementFailure, PiPromptNotStarted, PiPromptUncertain, TERMINAL_OWNER_MAX_AGE_MS = 1000, PiPeer;
6438
+ var PiToolSettlementFailure, PiPromptNotStarted, PiPromptUncertain, PiVersionRefusal, TERMINAL_OWNER_MAX_AGE_MS = 1000, PiPeer;
6267
6439
  var init_pi = __esm(() => {
6268
6440
  init_launch();
6269
6441
  init_envelope();
6270
6442
  init_child_process();
6443
+ init_compat();
6271
6444
  init_process_signature();
6272
6445
  init_session();
6273
6446
  init_tools();
@@ -6282,8 +6455,14 @@ var init_pi = __esm(() => {
6282
6455
  };
6283
6456
  PiPromptUncertain = class PiPromptUncertain extends Error {
6284
6457
  };
6458
+ PiVersionRefusal = class PiVersionRefusal extends Error {
6459
+ constructor(verdict) {
6460
+ super(verdict.reason);
6461
+ }
6462
+ };
6285
6463
  PiPeer = class PiPeer extends BasePeer {
6286
6464
  opts;
6465
+ preflight;
6287
6466
  hubNative = true;
6288
6467
  proc;
6289
6468
  server;
@@ -6350,9 +6529,10 @@ var init_pi = __esm(() => {
6350
6529
  tuiCommands = new Map;
6351
6530
  tuiQueue = [];
6352
6531
  tuiWaiters = [];
6353
- constructor(id, opts) {
6532
+ constructor(id, opts, preflight) {
6354
6533
  super(id, opts.watchdogMs);
6355
6534
  this.opts = opts;
6535
+ this.preflight = preflight;
6356
6536
  }
6357
6537
  get tuiLaunch() {
6358
6538
  return this._tuiLaunch;
@@ -6598,6 +6778,8 @@ var init_pi = __esm(() => {
6598
6778
  return Response.json({ decisions, ...reservation ? { reservation } : {} });
6599
6779
  }
6600
6780
  async startImpl() {
6781
+ if (this.preflight)
6782
+ await this.preflight();
6601
6783
  this.terminalOwnerObservation = undefined;
6602
6784
  this.stopping = false;
6603
6785
  this.rosterDetached = false;
@@ -10017,7 +10199,7 @@ var init_hub_tools = __esm(() => {
10017
10199
  tool("hub_release_list", "Read public release scope and planned/actual dates for phased planning. Configured planners only.", {}),
10018
10200
  tool("hub_task_propose", "Put a piece of work on the shared task board. The hub assigns an owner by class (routing.toml) unless you name one. Classes: plan, implement, bulk_edit, test, review, summarize, triage. Name the class when you know it; without one the hub tries to pick it. Name yourself as owner to claim work nobody assigned you: it starts in progress. Put the paths the work touches in refs, set refs.repo for work in another repository, and when you claim work, a plan; the hub says when they overlap another open task.", { title: str2, class: { type: "string", enum: ["plan", "implement", "bulk_edit", "test", "review", "summarize", "triage"] }, detail: str2, role: { type: "string", description: "catalog role id for this task class; guidance only, no routing or authority change" }, refs, plan, owner: { type: "string", description: "peer id; yours to claim the work, omit to let the hub route it. With after, the owner is reserved: offered the task first once it is ready" }, after: { type: "array", items: { type: "integer" }, description: "ids of tasks that must be approved first; until then this one waits, offered to nobody, and cannot be claimed" }, backlog: { type: "boolean", description: "stage unoffered work; owner is only a reservation, even when naming yourself" }, urgent: { type: "boolean", description: "hand it over even when its owner is paused for quota only briefly" }, goal: { type: "string", description: "id of this project's long-term task (ahub goal list) the work is part of" } }, ["title"]),
10019
10201
  tool("hub_task_accept", "Take a task that was assigned to you, with your plan for it. Set refs.repo for work in another repository; acceptance may correct only that repository metadata.", { id, plan: acceptPlan, refs: { ...refs, properties: { repo: refs.properties.repo } } }, ["id"]),
10020
- tool("hub_task_cancel", "Cancel unaccepted proposed/backlog work you proposed as its configured planner; accepted work is person-only. Keeps history and does not stop a native turn or delete files.", { id, reason: str2, superseded_by: str2, revision: { type: "integer", minimum: 0 } }, ["id", "reason"]),
10202
+ tool("hub_task_cancel", "Cancel proposed/backlog work you proposed as its configured planner, including work an owner accepted and then declined while it has no execution, branch, commit or PR evidence; work with any such evidence is person-only. Keeps history and does not stop a native turn or delete files.", { id, reason: str2, superseded_by: str2, revision: { type: "integer", minimum: 0 } }, ["id", "reason"]),
10021
10203
  tool("hub_task_decline", "Pass on a task assigned to you; the hub offers it to the next peer. A plain decline is a lasting refusal: you are never offered this task again. Pass capacity: true when you only have no room right now: you are skipped for this move only, and a later reroute or planner pull may offer it to you again.", { id, reason: str2, capacity: { type: "boolean" } }, ["id"]),
10022
10204
  tool("hub_task_done", "Mark your task finished. It goes to its reviewer with your summary and refs; no duplicate hub_send chat is needed. When the project configures a check for its class, the hub runs it first and the result comes as a task message: a failed check keeps the task with you.", { id, summary: { type: "string", description: "what changed, why, and the check you ran with its result" }, refs, deliverables: deliverablesSchema }, ["id", "summary"]),
10023
10205
  tool("hub_sdlc_record", "Record artifact ids or claimed local checks on your task. This grants no authority or external-check evidence. Only its owner/reviewer or the person may record; only its reviewer/person may name a reviewed head or phase checklist. Phase evidence refs are content hashes, not external verification.", { id, receipt: { type: "object", properties: { deliverables: deliverablesSchema, stage: { type: "string", enum: ["spec", "plan", "implement", "review", "verify", "merge", "release"] }, repo: str2, issue: id, pr: id, localHead: str2, localResult: { type: "string", enum: ["passed", "failed", "unknown"] }, reviewedHead: str2, phase: { type: "object", properties: { id: str2, head: str2, tasks: { type: "array", maxItems: 64, items: { type: "object", properties: { id, after: { type: "array", maxItems: 16, items: id } }, required: ["id", "after"], additionalProperties: false } }, criteria: { type: "array", maxItems: 32, items: { type: "object", properties: { id: str2, required: { type: "boolean" }, result: { type: "string", enum: ["verified", "failed", "unverified"] }, refs: { type: "array", maxItems: 8, items: str2 } }, required: ["id", "required", "result", "refs"], additionalProperties: false } } }, required: ["id", "tasks", "criteria"], additionalProperties: false } }, additionalProperties: false } }, ["id", "receipt"]),
@@ -10041,7 +10223,7 @@ var init_hub_tools = __esm(() => {
10041
10223
  TASK_TOOL_NAMES = new Set(TASK_TOOLS.map((t) => t.name));
10042
10224
  CONDUCTOR_TOOLS = [
10043
10225
  tool("hub_status", "Inspect public team state, holds, quota windows, task counts and pending approval peer/tool/age. Only the configured conductor may use this tool.", {}),
10044
- tool("hub_task_show", "Read a task's public view and history, its done summary and check result included. PII tasks remain stubs. Only the conductor and the task's current owner and reviewer may use this tool.", { id }, ["id"]),
10226
+ tool("hub_task_show", "Read a task's public view and history, its done summary and check result included. PII tasks remain stubs. Only the conductor, the configured planner and the task's current owner and reviewer may use this tool.", { id }, ["id"]),
10045
10227
  tool("hub_task_assign", "Name an owner (default role) or reviewer. Only the conductor may name a reviewer through this tool. A proposer may redirect an unaccepted proposed task's owner; a task that still waits reserves that owner. Pull-only peers receive work when named while attached or when they claim it; a peer that cannot take the task is refused with the reason, and the task is unchanged. With an explicit capabilities list, the conductor needs assign capability for either role, the proposer only to give ownership to another peer.", { id, peer: str2, role: { type: "string", enum: ["owner", "reviewer"] } }, ["id", "peer"]),
10046
10228
  tool("hub_task_escalate", "Escalate a task through the normal task flow, as the conductor. Requires assign capability when explicitly listed.", { id }, ["id"]),
10047
10229
  tool("hub_peer_start", "Start local, kimi, qwen, jcode, pi, claude or codex in the start mode a person set for it; you cannot choose the mode. A headless peer starts at once. A TUI peer (claude, and pi and codex by default) opens in a terminal with a fixed command; when the hub cannot open a terminal the call fails with the command for the person to run.", { peer: { type: "string", enum: CONDUCTOR_START_PEERS } }, ["peer"]),
@@ -10367,11 +10549,12 @@ var init_local_worker = __esm(() => {
10367
10549
  return `(stopped after ${maxSteps} steps) ${progress.last}`.trim();
10368
10550
  }
10369
10551
  toolContext(envs, turnSignal, pii, reply) {
10552
+ const executionGeneration = this.turn;
10370
10553
  return {
10371
10554
  cwd: this.opts.cwd,
10372
10555
  deny: this.opts.tools.deny,
10373
10556
  readOnly: this.opts.tools.readOnly,
10374
- permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input) => {
10557
+ permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input, policy) => {
10375
10558
  if (turnSignal.aborted)
10376
10559
  return "aborted";
10377
10560
  const generation = this.turn;
@@ -10400,7 +10583,7 @@ var init_local_worker = __esm(() => {
10400
10583
  };
10401
10584
  const onAbort = () => finish("aborted");
10402
10585
  turnSignal.addEventListener("abort", onAbort, { once: true });
10403
- this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input).then(finish, (error) => {
10586
+ this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input, policy).then(finish, (error) => {
10404
10587
  turnSignal.removeEventListener("abort", onAbort);
10405
10588
  reject(error);
10406
10589
  });
@@ -10413,6 +10596,7 @@ var init_local_worker = __esm(() => {
10413
10596
  observe({ source: "expired", answerEpoch: this.approvalAnswerEpoch ?? 0, eligibleExpiry: true });
10414
10597
  return picked;
10415
10598
  },
10599
+ shellExecutionCheck: (input) => turnSignal.aborted || executionGeneration !== this.turn ? "aborted" : this.opts.tools.shellExecutionCheck?.(input) ?? "valid",
10416
10600
  sandboxProfile: this.sandboxProfile,
10417
10601
  sandboxEnv: { ...proxyEnv(this.opts.tools.bashNetwork ?? false), AGENTHUB_PEER_ID: this.id },
10418
10602
  signal: turnSignal,
@@ -10737,7 +10921,7 @@ import { randomUUID as randomUUID8 } from "crypto";
10737
10921
  import { lstatSync as lstatSync11, mkdtempSync as mkdtempSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
10738
10922
  import { tmpdir as tmpdir5 } from "os";
10739
10923
  import { join as join17 } from "path";
10740
- function claudeImplementerArgs(turn, mcp, settings, prompt) {
10924
+ function claudeImplementerArgs(turn, mcp, settings, prompt, model) {
10741
10925
  return [
10742
10926
  "-p",
10743
10927
  "--output-format",
@@ -10753,6 +10937,7 @@ function claudeImplementerArgs(turn, mcp, settings, prompt) {
10753
10937
  "",
10754
10938
  "--settings",
10755
10939
  settings,
10940
+ ...model ? ["--model", model] : [],
10756
10941
  "--permission-mode",
10757
10942
  "default",
10758
10943
  "--permission-prompts",
@@ -11015,7 +11200,7 @@ var init_claude_implementer = __esm(() => {
11015
11200
  const child = new OwnedPrintChild({
11016
11201
  peer: this.id,
11017
11202
  command: this.command,
11018
- args: claudeImplementerArgs(turn, mcp, settings, this.options.systemPrompt),
11203
+ args: claudeImplementerArgs(turn, mcp, settings, this.options.systemPrompt, this.options.model),
11019
11204
  cwd: this.options.cwd,
11020
11205
  input: group.envs.map(frame).join(`
11021
11206
 
@@ -11147,7 +11332,14 @@ function createPi(ctx) {
11147
11332
  if (ctx.permission === "read-only")
11148
11333
  throw new Error("Pi has no verified read-only permission mapping");
11149
11334
  init_pi();
11150
- 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
+ });
11151
11343
  }
11152
11344
  function createLocal(ctx) {
11153
11345
  if (!("local" in ctx.daemon))
@@ -11254,9 +11446,9 @@ var init_reviewer_variants = __esm(() => {
11254
11446
  init_kimi_review();
11255
11447
  init_native_factories();
11256
11448
  init_native_display();
11257
- 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);
11258
11450
  CODEX_REVIEWER_VARIANT = { ...codexVariant, start: { ...codexVariant.start, create: createCodexReviewer } };
11259
- 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);
11260
11452
  AGY_REVIEWER_VARIANT = {
11261
11453
  ...reviewerVariant("agy", "agy reviewer", { defaultPool: false, declaration: "automatic", unavailable: "agy is an implementer peer (#485)" }, { palette: { fg: "#6558a6", bg: "#f1effa" } }),
11262
11454
  optional: true
@@ -12704,7 +12896,7 @@ var init_builtin_metadata = __esm(() => {
12704
12896
  headlessForms = Object.freeze(["headless"]);
12705
12897
  hubGrants = {
12706
12898
  ask: "every write, edit and shell command asks",
12707
- "ask-when-needed": "write and edit inside the project run without asking; shell and git writes still ask",
12899
+ "ask-when-needed": "write, edit and proven bounded read-only shell commands inside the project run without asking; other shell commands and git writes still ask",
12708
12900
  "never-ask": "every tool runs without asking, inside the hub sandbox and path guard"
12709
12901
  };
12710
12902
  CLAUDE_REVIEWER_VARIANT = {
@@ -12741,8 +12933,8 @@ var init_builtin_metadata = __esm(() => {
12741
12933
  trust: { onPrem: false, hubSandboxed: false },
12742
12934
  start: { create: createClaudeImplementer, daemonPort: "claude-implementer", forms: headlessForms, defaultForm: "headless", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
12743
12935
  identity: { mcp: "env", currentChild: true },
12744
- instances: { mode: "headless", roles: ["implementer", "verifier"] },
12745
- 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" } },
12746
12938
  session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: false, launchKind: "claude-implementer", crashPlan: () => ({ resume: {}, how: "Claude implementer resumes its recorded per-task sessions" }) },
12747
12939
  turnEnd: "adapter",
12748
12940
  budget: { account: "claude" },
@@ -13662,7 +13854,8 @@ function assertNativeCapabilityScope(definition) {
13662
13854
  ["hooks.factsHook", declared(definition.hooks, "factsHook")],
13663
13855
  ["cli.entry", declared(definition.cli, "entry")],
13664
13856
  ["cli.preview", declared(definition.cli, "preview")],
13665
- ["cli.usage", declared(definition.cli, "usage")]
13857
+ ["cli.usage", declared(definition.cli, "usage")],
13858
+ ["instances.modelSelect", declared(definition.instances, "modelSelect")]
13666
13859
  ];
13667
13860
  const field = restricted.find(([, present]) => present)?.[0];
13668
13861
  if (field)
@@ -14087,6 +14280,14 @@ var init_work_lists = __esm(() => {
14087
14280
  forbidden = new Set(["__proto__", "constructor", "prototype"]);
14088
14281
  });
14089
14282
 
14283
+ // node_modules/@staix/agent-mem/src/memory/candidate-pending.ts
14284
+ var CANDIDATE_AGE_MS, CANDIDATE_SHOWN_GRACE_MS, CANDIDATE_DUE_MS;
14285
+ var init_candidate_pending = __esm(() => {
14286
+ CANDIDATE_AGE_MS = 30 * 86400000;
14287
+ CANDIDATE_SHOWN_GRACE_MS = 7 * 86400000;
14288
+ CANDIDATE_DUE_MS = 7 * 86400000;
14289
+ });
14290
+
14090
14291
  // node_modules/@staix/agent-mem/src/memory/generation-sync.ts
14091
14292
  var zero;
14092
14293
  var init_generation_sync = __esm(() => {
@@ -14118,14 +14319,6 @@ var init_sync = __esm(() => {
14118
14319
  liveGroups = new Set;
14119
14320
  });
14120
14321
 
14121
- // node_modules/@staix/agent-mem/src/memory/candidate-pending.ts
14122
- var CANDIDATE_AGE_MS, CANDIDATE_SHOWN_GRACE_MS, CANDIDATE_DUE_MS;
14123
- var init_candidate_pending = __esm(() => {
14124
- CANDIDATE_AGE_MS = 30 * 86400000;
14125
- CANDIDATE_SHOWN_GRACE_MS = 7 * 86400000;
14126
- CANDIDATE_DUE_MS = 7 * 86400000;
14127
- });
14128
-
14129
14322
  // node_modules/@staix/agent-mem/src/memory/path-glob.ts
14130
14323
  var init_path_glob = __esm(() => {
14131
14324
  init_project2();
@@ -21423,6 +21616,37 @@ init_policy();
21423
21616
  init_screen();
21424
21617
  init_shard();
21425
21618
  init_work_lists();
21619
+ // node_modules/@staix/agent-mem/src/memory/project-import.ts
21620
+ init_bridge();
21621
+ init_private_spans();
21622
+ init_shard();
21623
+ init_store();
21624
+ var PROJECT_IMPORT_MAX_BYTES = 64 * 1024 * 1024;
21625
+ var PROJECT_IMPORT_MAX_LINE_BYTES = 64 * 1024;
21626
+ var PROJECT_IMPORT_FUTURE_SKEW_MS = 5 * 60 * 1000;
21627
+ var NOTE_KEYS = new Set(["schema", "id", "at", "machine", "op", "project", "scope", "tier", "kind", "peer", "title", "text", "task", "about", "supersedes", "promotedFrom", "source", "expires"]);
21628
+ // node_modules/@staix/agent-mem/src/memory/external-import.ts
21629
+ init_aliases();
21630
+
21631
+ // node_modules/@staix/agent-mem/src/memory/candidates.ts
21632
+ init_candidate_pending();
21633
+
21634
+ // node_modules/@staix/agent-mem/src/memory/note-add.ts
21635
+ init_store();
21636
+
21637
+ // node_modules/@staix/agent-mem/src/memory/candidates.ts
21638
+ init_home();
21639
+ init_platform();
21640
+ init_private_spans();
21641
+ var CANDIDATE_RETENTION = { count: 500, ageMs: 30 * 86400000 };
21642
+ var COUNTER_RETENTION = { count: 1e4, ageMs: 90 * 86400000 };
21643
+
21644
+ // node_modules/@staix/agent-mem/src/memory/external-import.ts
21645
+ init_candidate_pending();
21646
+ init_platform();
21647
+ init_private_spans();
21648
+ init_screen();
21649
+ var EXTERNAL_IMPORT_LIMITS = { maxFiles: 256, maxDirectories: 512, maxEntriesPerDirectory: 512, maxDepth: 16, maxFileBytes: 1024 * 1024, maxCandidatesPerRun: 100, candidateTextChars: 3000, titleChars: 200 };
21426
21650
  // node_modules/@staix/agent-mem/src/memory/cross-project.ts
21427
21651
  init_search_fts();
21428
21652
  init_capture();
@@ -21441,6 +21665,11 @@ init_platform();
21441
21665
  init_store();
21442
21666
  init_shard();
21443
21667
  init_work_lists();
21668
+ init_capture();
21669
+ init_capture_input();
21670
+ init_config2();
21671
+ init_policy();
21672
+ init_private_spans();
21444
21673
 
21445
21674
  // node_modules/@staix/agent-mem/src/index.ts
21446
21675
  init_project2();
@@ -21452,22 +21681,10 @@ init_store();
21452
21681
  init_sync();
21453
21682
  init_project2();
21454
21683
  var AMEM_NAMES = { memory: "amem", goal: "amem goal" };
21455
- 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]`;
21456
21685
  var MEMORY_USAGE = memoryUsage();
21457
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]`;
21458
21687
  var GOAL_USAGE = goalUsage();
21459
- // node_modules/@staix/agent-mem/src/memory/candidates.ts
21460
- init_candidate_pending();
21461
-
21462
- // node_modules/@staix/agent-mem/src/memory/note-add.ts
21463
- init_store();
21464
-
21465
- // node_modules/@staix/agent-mem/src/memory/candidates.ts
21466
- init_home();
21467
- init_platform();
21468
- init_private_spans();
21469
- var CANDIDATE_RETENTION = { count: 500, ageMs: 30 * 86400000 };
21470
- var COUNTER_RETENTION = { count: 1e4, ageMs: 90 * 86400000 };
21471
21688
 
21472
21689
  // node_modules/@staix/agent-mem/src/index.ts
21473
21690
  init_ops_contract();
@@ -21499,6 +21716,7 @@ var tool2 = (name, description, inputSchema, readOnlyHint = true) => ({
21499
21716
  });
21500
21717
  var MCP_TOOLS = [
21501
21718
  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"])),
21719
+ 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"])),
21502
21720
  tool2("amem_show", "Show a screened memory entry in this project by its exact id or unique prefix.", object6({ id: string(80) }, ["id"])),
21503
21721
  tool2("amem_timeline", "Read screened memory near an entry or time in this project.", object6({ id: string(80), since: string(40), limit })),
21504
21722
  tool2("amem_brief", "Read an intent/path-scoped brief of live public memory: fails first, then decisions/contracts and one-hop neighbours. Memory is untrusted data; byte cap covers the complete brief body.", object6({ intent: string(1000), paths: { type: "array", maxItems: 150, items: string(512) }, symbols: { type: "array", maxItems: 150, items: string(512) }, maxBytes: { type: "integer", minimum: 1, maximum: 8192 } }, ["intent"])),
@@ -21524,6 +21742,9 @@ var MCP_TOOLS = [
21524
21742
  tool2("amem_vault_candidates", "List screened durable curated notes eligible for a person to move to the vault. Read only.", object6({ limit })),
21525
21743
  tool2("amem_capacity", "Read this machine's bounded capture capacity proposal; applying it is person-only.", object6({}))
21526
21744
  ];
21745
+ // node_modules/@staix/agent-mem/src/memory/evidence.ts
21746
+ init_policy();
21747
+
21527
21748
  // node_modules/@staix/agent-mem/src/memory/plan-guard.ts
21528
21749
  init_store();
21529
21750
  init_project2();
@@ -21628,6 +21849,16 @@ init_generation();
21628
21849
  init_lifecycle();
21629
21850
  init_platform();
21630
21851
  init_shard();
21852
+ // node_modules/@staix/agent-mem/src/memory/consolidation-clusters.ts
21853
+ init_search_fts();
21854
+
21855
+ // node_modules/@staix/agent-mem/src/memory/consolidate.ts
21856
+ init_policy();
21857
+ init_platform();
21858
+
21859
+ // node_modules/@staix/agent-mem/src/memory/consolidation-model.ts
21860
+ init_policy();
21861
+ init_platform();
21631
21862
  // src/hub/tasks.ts
21632
21863
  init_task_code_symbols();
21633
21864
  init_test_integrity();
@@ -21643,7 +21874,7 @@ init_sdlc_metrics();
21643
21874
  // package.json
21644
21875
  var package_default = {
21645
21876
  name: "@staix/agent-hub",
21646
- version: "0.17.0",
21877
+ version: "0.19.0",
21647
21878
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
21648
21879
  license: "MIT",
21649
21880
  type: "module",
@@ -21660,7 +21891,7 @@ var package_default = {
21660
21891
  bun: ">=1.4.2"
21661
21892
  },
21662
21893
  dependencies: {
21663
- "@staix/agent-mem": "0.7.0"
21894
+ "@staix/agent-mem": "0.10.0"
21664
21895
  },
21665
21896
  devDependencies: {
21666
21897
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -21714,6 +21945,17 @@ init_plan_conformance();
21714
21945
  init_review_findings();
21715
21946
  init_sdlc();
21716
21947
 
21948
+ // src/hub/sdlc-merge.ts
21949
+ init_review_findings();
21950
+ init_sdlc();
21951
+
21952
+ // src/hub/sdlc-mirror.ts
21953
+ init_envelope();
21954
+ var inFlight = new Set;
21955
+
21956
+ // src/hub/review-scope.ts
21957
+ var isReviewScope = (value) => value === "full" || value === "code";
21958
+
21717
21959
  // src/hub/tasks.ts
21718
21960
  init_review_findings();
21719
21961
  init_roles();
@@ -21748,6 +21990,7 @@ init_project();
21748
21990
 
21749
21991
  // src/hub/task-repository.ts
21750
21992
  init_git();
21993
+ init_host_executable();
21751
21994
  init_project();
21752
21995
  init_spec_read();
21753
21996
  var GIT_MAX_BYTES = 16 * 1024;
@@ -21854,7 +22097,8 @@ function validateInstances(raw, catalog) {
21854
22097
  const path = `instances.${id}`, entry = fields2(descriptor.value, path);
21855
22098
  const declaredKind = entry.kind?.value;
21856
22099
  const jcodeFields = typeof declaredKind === "string" && resolve(declaredKind)?.instances?.nativeHome === true;
21857
- 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)))
21858
22102
  return fail4(path, "unsupported field");
21859
22103
  const kind = entry.kind?.value;
21860
22104
  if (typeof kind !== "string")
@@ -21929,6 +22173,21 @@ function validateInstances(raw, catalog) {
21929
22173
  return fail4("instances", `at most ${MAX_JCODE_INSTANCES} extra Jcode instances are supported per project`);
21930
22174
  Object.assign(value, jcodeInstanceFields(entry, path));
21931
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
+ }
21932
22191
  if (entry.readOnly)
21933
22192
  value.readOnly = entry.readOnly.value;
21934
22193
  if (entry.roles) {
@@ -30723,8 +30982,15 @@ async function callTool(req) {
30723
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)
30724
30983
  return deny("invalid native permission request");
30725
30984
  try {
30726
- const res = await hub.request({ t: "permission_request", tool: args?.tool_name, input: args?.input }, 3605000);
30727
- 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 }));
30728
30994
  } catch {
30729
30995
  return deny("hub permission unavailable");
30730
30996
  }